package metacache

import (
	"context"
	"fmt"
	"io"
	"strconv"
	"sync"

	"github.com/pingcap/log"
	"go.uber.org/zap"
	"gopkg.in/yaml.v2"
	"sigs.k8s.io/controller-runtime/pkg/client"

	o11yv1 "github.com/tidbcloud/observability/api/regionalserver/resourceworker/v1"
	"github.com/tidbcloud/observability/o11ydiagnosis/pkg/storage"
)

const (
	annotationApplicationID = "observability.pingcap.com/applicationID"
)

type MetaInfo struct {
	O11yID      string
	VMAccountID int32
	VMProjectID int32
}

type MetaCache interface {
	GetMetaInfo(ctx context.Context, env, region, tenant, project, cluster string, nextGen bool, byoc bool) (*MetaInfo, error)
}

// Global singleton metacache manager
var (
	globalMetaCache     MetaCache
	globalMetaCacheOnce sync.Once
)

// GetOrCreateMetaCache returns a singleton MetaCache instance
// Since kubeCli and store are typically the same instance across the application,
// we use a global singleton pattern
func GetOrCreateMetaCache(kubecli client.Client, store storage.Storage) MetaCache {
	globalMetaCacheOnce.Do(func() {
		globalMetaCache = &metaCache{
			kubecli: kubecli,
			cache:   sync.Map{},
			store:   store,
		}
		log.Info("Created global singleton MetaCache")
	})
	return globalMetaCache
}

// NewMetaCache creates a new MetaCache instance (for backward compatibility)
// Deprecated: Use GetOrCreateMetaCache instead for singleton pattern
func NewMetaCache(kubecli client.Client, store storage.Storage) MetaCache {
	return GetOrCreateMetaCache(kubecli, store)
}

type metaCache struct {
	kubecli client.Client
	cache   sync.Map
	store   storage.Storage
}

type ClusterYAML struct {
	ClusterBaseInfo struct {
		ID string `yaml:"id"`
	} `yaml:"clusterbaseinfo"`
	VMAccountID string `yaml:"vmaccountid"`
	VMProjectID string `yaml:"vmprojectid"`
}

func (m *metaCache) GetMetaInfo(ctx context.Context, env, region, tenant, project, cluster string, nextGen bool, byoc bool) (*MetaInfo, error) {
	v, ok := m.cache.Load(cluster)
	if ok {
		return v.(*MetaInfo), nil
	}
	o11ycluster, err := m.doGetCluster(ctx, env, tenant, cluster, nextGen, byoc)
	if err != nil {
		return nil, err
	}
	if o11ycluster == nil {
		// Determine the bucket based on environment
		bucket := storage.BucketName(env, region, false, nextGen)
		envpath := ""
		if env == "prod" {
			if nextGen {
				envpath = "prod-"
			} else {
				envpath = ""
			}
		} else {
			envpath = env + "-"
		}
		nextGenPath := ""
		if nextGen {
			nextGenPath = "-nextgen"
			project = "0"
		}
		key := fmt.Sprintf("archived/%stidbcloud%s/%s/%s/%s/cluster.yaml", envpath, nextGenPath, tenant, project, cluster)
		log.Info("Cluster not found, try to get archived cluster", zap.String("key", key))
		object, err := m.store.Object(ctx, bucket, key)
		if err != nil {
			if err == storage.ErrNotFound {
				log.Info("GetMetaInfo for archived failed", zap.Error(err))
				return nil, err
			}
			log.Error("GetMetaInfo for archived failed", zap.Error(err))
			return nil, err
		}
		defer object.Reader.Close()
		content, err := io.ReadAll(object.Reader)
		if err != nil {
			log.Error("Failed to read object content", zap.Error(err))
			return nil, err
		}
		var clusterYaml ClusterYAML
		if err := yaml.Unmarshal(content, &clusterYaml); err != nil {
			log.Error("Failed to unmarshal object content", zap.Error(err))
			return nil, err
		}
		if clusterYaml.ClusterBaseInfo.ID == "" {
			log.Warn("Cluster YAML found but ID is empty",
				zap.String("tenant", tenant),
				zap.String("cluster", cluster))
			return nil, nil
		}
		vmAccountID, err := strconv.ParseInt(clusterYaml.VMAccountID, 10, 32)
		if err != nil {
			log.Error("Failed to convert VM account ID", zap.Error(err))
			return nil, fmt.Errorf("invalid VM account ID: %v", err)
		}
		vmProjectID, err := strconv.ParseInt(clusterYaml.VMProjectID, 10, 32)
		if err != nil {
			log.Error("Failed to convert VM project ID", zap.Error(err))
			return nil, fmt.Errorf("invalid VM project ID: %v", err)
		}
		info := &MetaInfo{
			O11yID:      clusterYaml.ClusterBaseInfo.ID,
			VMAccountID: int32(vmAccountID),
			VMProjectID: int32(vmProjectID),
		}
		m.cache.Store(cluster, info)
		return info, nil
	}
	info := &MetaInfo{
		O11yID:      o11ycluster.Name,
		VMAccountID: o11ycluster.Spec.VictoriaAccount.AccountID,
		VMProjectID: o11ycluster.Spec.VictoriaAccount.ProjectID,
	}
	m.cache.Store(cluster, info)
	return info, nil
}

func (m *metaCache) doGetCluster(ctx context.Context, env, tenant, cluster string, nextGen bool, byoc bool) (*o11yv1.Cluster, error) {
	clusters := &o11yv1.ClusterList{}
	if err := m.kubecli.List(ctx, clusters, client.InNamespace(getNamespace(env, tenant, nextGen, byoc)), client.MatchingLabels{
		annotationApplicationID: cluster,
	}); err != nil {
		return nil, err
	}
	if len(clusters.Items) != 1 {
		return nil, nil
	}
	return &clusters.Items[0], nil
}

func getNamespace(env, tenant string, nextGen bool, byoc bool) string {
	if byoc {
		return "byoc-" + tenant
	}
	if nextGen {
		return fmt.Sprintf("%s-tidbcloud-nextgen-%s", env, tenant)
	}
	switch env {
	case "prod":
		return "tidbcloud-" + tenant
	case "staging":
		return "staging-tidbcloud-" + tenant
	case "dev":
		fallthrough
	default:
		return "dev-tidbcloud-" + tenant
	}
}
