package resourcepoolcpulimit

import (
	"context"
	"slices"
	"strings"

	"github.com/tidbcloud/cluster-service-ng/internal/dataplane-exporter/pkg/source/kubernetes"
)

const (
	tenantLabelKey                     = "tags.tidbcloud.com/tenant"
	sharedPoolNamespacePrefix          = "tidb"
	sharedPoolVCPUCeilingResourceQuota = "sharedpool-vcpu-ceiling"
	dataplaneAdminNamespace            = "tidb-admin"
)

type Source interface {
	ListNamespaces(ctx context.Context) ([]kubernetes.Namespace, error)
	ListResourceQuotas(ctx context.Context) ([]kubernetes.ResourceQuota, error)
}

type LiveReader struct {
	source Source
}

func NewLiveReader(source Source) *LiveReader {
	return &LiveReader{source: source}
}

func (r *LiveReader) Read(ctx context.Context) (Snapshot, error) {
	namespaces, err := r.source.ListNamespaces(ctx)
	if err != nil {
		return Snapshot{}, err
	}

	resourceQuotas, err := r.source.ListResourceQuotas(ctx)
	if err != nil {
		return Snapshot{}, err
	}

	resourceQuotaByNamespace := make(map[string]kubernetes.ResourceQuota, len(resourceQuotas))
	for _, resourceQuota := range resourceQuotas {
		if resourceQuota.Name != sharedPoolVCPUCeilingResourceQuota {
			continue
		}
		resourceQuotaByNamespace[resourceQuota.Namespace] = resourceQuota
	}

	samples := make([]Sample, 0, len(namespaces))
	for _, namespace := range namespaces {
		// Namespace enumeration is the source of truth for which shared pools
		// exist. ResourceQuota is optional because vcpusLimit=0 deletes the quota,
		// but we still want to export that pool with CPULimit=0 and keep tenant
		// context from namespace labels.
		//
		// We intentionally do not read tags.tidbcloud.com/shared-pool here even
		// though that would be the better canonical source when present. Current
		// shared-pool namespace creation goes through pkg/k8s.EnsureNamespace(...),
		// which only guarantees the generic service-type label and does not stamp a
		// shared-pool label on the namespace. In production that means the label is
		// not reliable for shared-pool namespaces, so we derive the pool ID from the
		// namespace naming rule tidb<sharedPoolId> instead. If namespace labeling is
		// tightened later, prefer the explicit shared-pool label over name parsing.
		resourcePool, ok := sharedPoolIDFromNamespace(namespace.Name)
		if !ok {
			continue
		}

		sample := Sample{
			Tenant:       namespace.Labels[tenantLabelKey],
			ResourcePool: resourcePool,
			Namespace:    namespace.Name,
		}
		if resourceQuota, ok := resourceQuotaByNamespace[namespace.Name]; ok {
			sample.CPULimit = resourceQuota.HardCPU
		}
		samples = append(samples, sample)
	}

	slices.SortFunc(samples, func(a, b Sample) int {
		return strings.Compare(sampleMetricKey(a.Tenant, a.ResourcePool, a.Namespace), sampleMetricKey(b.Tenant, b.ResourcePool, b.Namespace))
	})

	return Snapshot{Samples: samples}, nil
}

func sampleMetricKey(tenant, resourcePool, namespace string) string {
	return strings.Join([]string{tenant, resourcePool, namespace}, "|")
}

func sharedPoolIDFromNamespace(namespace string) (string, bool) {
	// Dataplane infra components, including this exporter, run in tidb-admin.
	// It shares the "tidb" prefix but is not a shared-pool namespace.
	//
	// This parser is a fallback for today's dataplane namespaces, not a general
	// namespace classifier. It depends on domain/entity.GetDataPlaneNamespace(),
	// which formats shared-pool namespaces as tidb<sharedPoolId>.
	if namespace == dataplaneAdminNamespace {
		return "", false
	}

	if !strings.HasPrefix(namespace, sharedPoolNamespacePrefix) {
		return "", false
	}

	sharedPoolID := strings.TrimPrefix(namespace, sharedPoolNamespacePrefix)
	if sharedPoolID == "" {
		return "", false
	}

	return sharedPoolID, true
}
