/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package util

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"

	pb "github.com/go-chassis/cari/discovery"
	"github.com/little-cui/etcdadpt"

	"github.com/apache/servicecomb-service-center/datasource"
	"github.com/apache/servicecomb-service-center/datasource/etcd/path"
	"github.com/apache/servicecomb-service-center/datasource/etcd/sd"
	"github.com/apache/servicecomb-service-center/datasource/etcd/state/kvstore"
	"github.com/apache/servicecomb-service-center/datasource/etcd/sync"
	"github.com/apache/servicecomb-service-center/pkg/log"
	"github.com/apache/servicecomb-service-center/pkg/util"
	"github.com/apache/servicecomb-service-center/server/config"
)

/*
get Service by service id
*/
func GetServiceWithRev(ctx context.Context, domain string, id string, rev int64) (*pb.MicroService, error) {
	key := path.GenerateServiceKey(domain, id)
	serviceResp, err := sd.Service().Search(ctx,
		etcdadpt.WithStrKey(key),
		etcdadpt.WithRev(rev))
	if err != nil {
		return nil, err
	}
	if len(serviceResp.Kvs) == 0 {
		return nil, datasource.ErrNoData
	}
	return serviceResp.Kvs[0].Value.(*pb.MicroService), nil
}

func GetService(ctx context.Context, domainProject string, serviceID string) (*pb.MicroService, error) {
	key := path.GenerateServiceKey(domainProject, serviceID)
	opts := append(FromContext(ctx), etcdadpt.WithStrKey(key))
	serviceResp, err := sd.Service().Search(ctx, opts...)
	if err != nil {
		return nil, err
	}
	if len(serviceResp.Kvs) == 0 {
		return nil, datasource.ErrNoData
	}
	return serviceResp.Kvs[0].Value.(*pb.MicroService), nil
}

func getServicesRawData(ctx context.Context, domainProject string) ([]*kvstore.KeyValue, error) {
	key := path.GenerateServiceKey(domainProject, "")
	opts := append(FromContext(ctx),
		etcdadpt.WithStrKey(key),
		etcdadpt.WithPrefix())
	resp, err := sd.Service().Search(ctx, opts...)
	if err != nil {
		return nil, err
	}
	return resp.Kvs, err
}

// GetAllServicesAcrossDomainProject get services of all domains, projects
// the map's key is domainProject
func GetAllServicesAcrossDomainProject(ctx context.Context) (map[string][]*pb.MicroService, error) {
	key := path.GetServiceRootKey("")
	opts := append(FromContext(ctx),
		etcdadpt.WithStrKey(key),
		etcdadpt.WithPrefix())
	serviceResp, err := sd.Service().Search(ctx, opts...)
	if err != nil {
		return nil, err
	}

	services := make(map[string][]*pb.MicroService)
	if len(serviceResp.Kvs) == 0 {
		return services, nil
	}

	for _, value := range serviceResp.Kvs {
		prefix := util.BytesToStringWithNoCopy(value.Key)
		parts := strings.Split(prefix, path.SPLIT)
		if len(parts) != 7 {
			continue
		}
		domainProject := parts[4] + path.SPLIT + parts[5]
		microService, ok := value.Value.(*pb.MicroService)
		if !ok {
			log.Error(fmt.Sprintf("backend key[%s]'s value is not type *pb.MicroService", prefix), nil)
			continue
		}
		services[domainProject] = append(services[domainProject], microService)
	}
	return services, nil
}

func GetServicesByDomainProject(ctx context.Context, domainProject string) ([]*pb.MicroService, error) {
	kvs, err := getServicesRawData(ctx, domainProject)
	if err != nil {
		return nil, err
	}
	services := []*pb.MicroService{}
	for _, kv := range kvs {
		services = append(services, kv.Value.(*pb.MicroService))
	}
	return services, nil
}

func GetServiceID(ctx context.Context, key *pb.MicroServiceKey) (serviceID string, err error) {
	serviceID, err = searchServiceID(ctx, key)
	if err != nil {
		return
	}
	if len(serviceID) == 0 {
		// 别名查询
		log.Debug(fmt.Sprintf("could not search microservice[%s/%s/%s/%s] id by 'serviceName', now try 'alias'",
			key.Environment, key.AppId, key.ServiceName, key.Version))
		return searchServiceIDFromAlias(ctx, key)
	}
	return
}

func searchServiceID(ctx context.Context, key *pb.MicroServiceKey) (string, error) {
	opts := append(FromContext(ctx), etcdadpt.WithStrKey(path.GenerateServiceIndexKey(key)))
	resp, err := sd.ServiceIndex().Search(ctx, opts...)
	if err != nil {
		return "", err
	}
	if len(resp.Kvs) == 0 {
		return "", nil
	}
	return resp.Kvs[0].Value.(string), nil
}

func searchServiceIDFromAlias(ctx context.Context, key *pb.MicroServiceKey) (string, error) {
	opts := append(FromContext(ctx), etcdadpt.WithStrKey(path.GenerateServiceAliasKey(key)))
	resp, err := sd.ServiceAlias().Search(ctx, opts...)
	if err != nil {
		return "", err
	}
	if len(resp.Kvs) == 0 {
		return "", nil
	}
	return resp.Kvs[0].Value.(string), nil
}

func GetServiceAllVersions(ctx context.Context, key *pb.MicroServiceKey, alias bool) (*kvstore.Response, error) {
	copyKey := *key
	copyKey.Version = ""
	var (
		prefix  string
		indexer kvstore.Indexer
	)
	if alias {
		prefix = path.GenerateServiceAliasKey(&copyKey)
		indexer = sd.ServiceAlias()
	} else {
		prefix = path.GenerateServiceIndexKey(&copyKey)
		indexer = sd.ServiceIndex()
	}
	opts := append(FromContext(ctx),
		etcdadpt.WithStrKey(prefix),
		etcdadpt.WithPrefix())
	resp, err := indexer.Search(ctx, opts...)
	return resp, err
}

// FindServiceIds return serviceIDs match the key, the existence of the micro-service without consider of version
func FindServiceIds(ctx context.Context, key *pb.MicroServiceKey, matchVersion bool) ([]string, bool, error) {
	resp, err := GetServiceAllVersions(ctx, key, false)
	if err != nil {
		return nil, false, err
	}
	fromEtcdKey := path.GetInfoFromSvcIndexKV
	kvs := resp.Kvs
	if len(kvs) == 0 {
		resp, err := GetServiceAllVersions(ctx, key, true)
		if err != nil {
			return nil, false, err
		}
		fromEtcdKey = path.GetInfoFromSvcAliasKV
		kvs = resp.Kvs
	}
	var (
		etcdKeys   [][]byte
		serviceIDs []string
	)
	for _, kv := range kvs {
		etcdKeys = append(etcdKeys, kv.Key)
		serviceIDs = append(serviceIDs, kv.Value.(string))
	}
	if !matchVersion {
		return serviceIDs, len(serviceIDs) > 0, nil
	}
	for i, etcdKey := range etcdKeys {
		serviceKey := fromEtcdKey(etcdKey)
		if serviceKey.Version == key.Version {
			return []string{serviceIDs[i]}, len(serviceIDs) > 0, nil
		}
	}
	return nil, len(serviceIDs) > 0, nil
}

func ServiceExist(ctx context.Context, domainProject string, serviceID string) bool {
	opts := append(FromContext(ctx),
		etcdadpt.WithStrKey(path.GenerateServiceKey(domainProject, serviceID)),
		etcdadpt.WithCountOnly())
	resp, err := sd.Service().Search(ctx, opts...)
	if err != nil || resp.Count == 0 {
		return false
	}
	return true
}

func GetAllServiceUtil(ctx context.Context) ([]*pb.MicroService, error) {
	domainProject := util.ParseDomainProject(ctx)
	services, err := GetServicesByDomainProject(ctx, domainProject)
	if err != nil {
		return nil, err
	}
	return services, nil
}

func UpdateService(ctx context.Context, domainProject string, serviceID string, service *pb.MicroService) ([]etcdadpt.OpOptions, error) {
	opts := make([]etcdadpt.OpOptions, 0)
	key := path.GenerateServiceKey(domainProject, serviceID)
	data, err := json.Marshal(service)
	if err != nil {
		log.Error("marshal service file failed", err)
		return opts, err
	}
	opt := etcdadpt.OpPut(etcdadpt.WithStrKey(key), etcdadpt.WithValue(data))
	opts = append(opts, opt)
	syncOpts, err := sync.GenUpdateOpts(ctx, datasource.ResourceKV, data, sync.WithOpts(map[string]string{"key": key}))
	if err != nil {
		log.Error("fail to create update opts", err)
		return opts, err
	}
	opts = append(opts, syncOpts...)
	return opts, nil
}

func GetOneDomainProjectServiceCount(ctx context.Context, domainProject string) (int64, error) {
	key := path.GetServiceRootKey(domainProject) + path.SPLIT
	opts := append(FromContext(ctx),
		etcdadpt.WithStrKey(key),
		etcdadpt.WithCountOnly(),
		etcdadpt.WithPrefix())
	resp, err := sd.Service().Search(ctx, opts...)
	if err != nil {
		return 0, err
	}
	return resp.Count, nil
}

func GetOneDomainProjectInstanceCount(ctx context.Context, domainProject string) (int64, error) {
	key := path.GetInstanceRootKey(domainProject) + path.SPLIT
	opts := append(FromContext(ctx),
		etcdadpt.WithStrKey(key),
		etcdadpt.WithCountOnly(),
		etcdadpt.WithPrefix())
	resp, err := sd.Instance().Search(ctx, opts...)
	if err != nil {
		return 0, err
	}
	return resp.Count, nil
}

func GetGlobalInstanceCount(ctx context.Context) (int64, error) {
	serviceIDs, err := GetGlobalServiceIDs(ctx)
	if err != nil {
		return 0, err
	}
	var count int64
	for _, serviceID := range serviceIDs {
		n, err := GetInstanceCountOfOneService(ctx, datasource.RegistryDomainProject, serviceID)
		if err != nil {
			return 0, err
		}
		count += n
	}
	return count, nil
}

func GetGlobalServiceIDs(ctx context.Context) ([]string, error) {
	var serviceIDs []string
	for name := range datasource.GlobalServiceNames {
		key := path.GenerateServiceIndexKey(&pb.MicroServiceKey{
			Tenant:      datasource.RegistryDomainProject,
			Environment: getGlobalEnvironment(),
			AppId:       datasource.RegistryAppID,
			ServiceName: name,
		})
		opts := append(FromContext(ctx),
			etcdadpt.WithStrKey(key),
			etcdadpt.WithPrefix())
		resp, err := sd.ServiceIndex().Search(ctx, opts...)
		if err != nil {
			return nil, err
		}
		for _, id := range resp.Kvs {
			serviceIDs = append(serviceIDs, id.Value.(string))
		}
	}
	return serviceIDs, nil
}

func GetGlobalServiceCount(ctx context.Context) (int64, error) {
	var count int64
	for name := range datasource.GlobalServiceNames {
		key := path.GenerateServiceIndexKey(&pb.MicroServiceKey{
			Tenant:      datasource.RegistryDomainProject,
			Environment: getGlobalEnvironment(),
			AppId:       datasource.RegistryAppID,
			ServiceName: name,
		})
		opts := append(FromContext(ctx),
			etcdadpt.WithStrKey(key),
			etcdadpt.WithCountOnly(),
			etcdadpt.WithPrefix())
		resp, err := sd.ServiceIndex().Search(ctx, opts...)
		if err != nil {
			return 0, err
		}
		count += resp.Count
	}
	return count, nil
}

func getGlobalEnvironment() string {
	env := pb.ENV_PROD
	if config.GetProfile().IsDev() {
		env = pb.ENV_DEV
	}
	return env
}
