package metadb

import (
	"context"
	"fmt"
	"math/rand"
	"strconv"
	"sync"
	"time"

	"github.com/patrickmn/go-cache"
	"github.com/pingcap/log"
	dbaasconfig "github.com/tidbcloud/metrics-server/central/config"
	"github.com/tidbcloud/metrics-server/central/db"
	"github.com/tidbcloud/metrics-server/central/db/entity"
	"github.com/tidbcloud/metrics-server/central/model/account/centralmod"
	"github.com/tidbcloud/metrics-server/central/utils"
	"github.com/tidbcloud/metrics-server/idl/pbgen/proto/core"
	"github.com/tidbcloud/metrics-server/internal/grpc/grpcli"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

const (
	// addiontional status enum
	ClusterStatus_CLUSTER_STATUS_PAUSED core.ClusterStatus = 18
)

// getIntegrations gets ids of projects whose integration type is matched and status is active.
func getIntegrations(integrationTypes []entity.IntegrationType) ([]entity.Integration, error) {
	is := make([]entity.Integration, 0)
	err := db.Transaction(func(tx *gorm.DB) error {
		q := entity.NewIntegrationQuerySet(tx).
			ProviderTypeIn(integrationTypes...).
			// We should include entity.IntegrationStatusError since it also means the integration is active.
			StatusIn(entity.IntegrationStatusActive, entity.IntegrationStatusError)
		if len(dbaasconfig.Global().O11yGlobalServerConfig.AllowList) > 0 {
			q = q.ProjectIDIn(dbaasconfig.Global().O11yGlobalServerConfig.AllowList...)
		} else if len(dbaasconfig.Global().O11yGlobalServerConfig.BlockList) > 0 {
			q = q.ProjectIDNotIn(dbaasconfig.Global().O11yGlobalServerConfig.BlockList...)
		}
		return q.All(&is)
	})
	if err != nil {
		return nil, err
	}
	return is, nil
}

func unpackEntityIDs(entityIDs []entity.ID) []uint64 {
	res := make([]uint64, len(entityIDs))
	for i, id := range entityIDs {
		res[i] = id.Uint64()
	}
	return res
}

func getClusters(projectIds []uint64) (map[uint64]*core.Cluster, error) {
	resp, err := grpcli.ListClusters(context.TODO(), &core.ListClustersReq{
		Filter: &core.ClusterFilter{
			ProjectIds: projectIds,
			// Statuses:   []core.ClusterStatus{core.ClusterStatus_CLUSTER_STATUS_NORMAL, core.ClusterStatus_CLUSTER_STATUS_SCALING},
			Deleted:   utils.BoolPtr(false),
			IsDevTier: utils.BoolPtr(false),
		},
	})
	if err != nil {
		return nil, fmt.Errorf("list clusters failed: %w", err)
	}
	clusters := make(map[uint64]*core.Cluster)
	for _, c := range resp.Clusters {
		if !c.IsDevTier && StatusExpected(c.Status) {
			log.Info(fmt.Sprintf("status %s found for cluster %s", c.Status.String(), c.Name))
			clusters[c.Id] = c
		}
	}
	return clusters, nil
}

func StatusExpected(status core.ClusterStatus) bool {
	// filter out all the status but normal and scaling since there are other enums not covered in the core api
	return status != core.ClusterStatus_CLUSTER_STATUS_INITIALIZING &&
		status != core.ClusterStatus_CLUSTER_STATUS_PREPARING &&
		status != core.ClusterStatus_CLUSTER_STATUS_LAUNCHING &&
		status != core.ClusterStatus_CLUSTER_STATUS_RECOVERING &&
		status != core.ClusterStatus_CLUSTER_STATUS_DELETED &&
		status != core.ClusterStatus_CLUSTER_STATUS_DELETECOMPLETE &&
		status != core.ClusterStatus_CLUSTER_STATUS_CLEARED &&
		status != ClusterStatus_CLUSTER_STATUS_PAUSED
}

type localCache struct {
	lock *sync.RWMutex
	data *cache.Cache
}

func (c localCache) get(key string) (interface{}, bool) {
	c.lock.Lock()
	defer c.lock.Unlock()

	v, found := c.data.Get(key)
	if found && v != nil {
		return v, true
	}

	return nil, false
}

func (c localCache) set(key string, value interface{}, expire time.Duration) error {
	err := c.data.Add(key, value, expire)
	if err != nil {
		log.Error("add key value to cache failed", zap.String("key", key), zap.Any("value", value), zap.Error(err))
		return err
	}

	return nil
}

var (
	projectCache = localCache{
		lock: &sync.RWMutex{},
		data: cache.New(86400*time.Second, 1*time.Minute),
	}
)

func getProjectNameFromCache(projectID uint64) (string, bool) {
	value, exists := projectCache.get(strconv.FormatUint(projectID, 10))
	if !exists {
		return "", false
	}

	projectName, ok := value.(string)
	if !ok || len(projectName) == 0 {
		return "", false
	}

	return projectName, true
}

func setProjectNameFromCache(projectID uint64, projectName string) error {
	randDuration := rand.Intn(100)
	expiration := 15*time.Minute + time.Duration(randDuration)*time.Millisecond

	return projectCache.set(strconv.FormatUint(projectID, 10), projectName, expiration)
}

func getProjectName(ctx context.Context, projectID uint64) (string, error) {
	projectName, exists := getProjectNameFromCache(projectID)
	if exists {
		log.Info("get project name from cache", zap.Uint64("projectID", projectID),
			zap.String("projectName", projectName), zap.Bool("cache_exists", exists))
		return projectName, nil
	}

	project, err := centralmod.GetProjectByID(context.Background(), projectID)
	if err != nil {
		return "", err
	}

	_ = setProjectNameFromCache(projectID, project.Name)

	log.Info("get project name from rpc and set to cache", zap.Uint64("projectID", projectID),
		zap.String("projectName", project.Name), zap.Bool("cache_exists", exists))

	return project.Name, nil
}
