// This file reconstructs the TiDB tier active at one historical decision point.
package evaluation

import (
	"fmt"
	"math"
	"time"

	"k8s.io/apimachinery/pkg/api/resource"
)

const (
	tiDBGOMAXPROCSMetric  = "tidb_gomaxprocs_threads_by_instance"
	tiDBMemoryQuotaMetric = "tidb_memory_quota_bytes_by_instance"
	tiDBInstanceLabel     = "instance"
	bytesPerMiB           = 1024 * 1024
	tiDBTierMaxAge        = 120 * time.Second
)

type historicalTiDBTierKey struct {
	GOMAXPROCS int64
	QuotaMiB   uint64
}

type historicalTiDBTier struct {
	NodeSpec      string
	CPURequest    string
	MemoryRequest string
}

// These request capacities are the pre-2026-08-20 CSNG resource contract.
// The 16C64G entry is also verified against historical kube-state-metrics.
var historicalTiDBTiers = map[historicalTiDBTierKey]historicalTiDBTier{
	{GOMAXPROCS: 3, QuotaMiB: 4812}:   {NodeSpec: "4C8G", CPURequest: "3", MemoryRequest: "2793Mi"},
	{GOMAXPROCS: 3, QuotaMiB: 13208}:  {NodeSpec: "4C16G", CPURequest: "3", MemoryRequest: "10350Mi"},
	{GOMAXPROCS: 7, QuotaMiB: 13208}:  {NodeSpec: "8C16G", CPURequest: "7", MemoryRequest: "10350Mi"},
	{GOMAXPROCS: 7, QuotaMiB: 27585}:  {NodeSpec: "8C32G", CPURequest: "7", MemoryRequest: "22713Mi"},
	{GOMAXPROCS: 15, QuotaMiB: 58387}: {NodeSpec: "16C64G", CPURequest: "15", MemoryRequest: "51667Mi"},
}

// TiDBTierObservation contains the logical node spec and the exact per-pod
// resource requests that the historical Policy must use as tier capacity.
type TiDBTierObservation struct {
	NodeSpec      string
	CPURequest    resource.Quantity
	MemoryRequest resource.Quantity
}

// ResolveTiDBTierAt reconstructs one tier from the latest active per-instance
// GOMAXPROCS and TiDB memory-quota samples available at the decision time.
func ResolveTiDBTierAt(metrics []MetricSeries, at time.Time, maxAge time.Duration) (*TiDBTierObservation, error) {
	if at.IsZero() || maxAge <= 0 {
		return nil, fmt.Errorf("evaluation: TiDB tier decision time and max age are required")
	}

	gomaxByInstance, err := latestMetricValuesByInstance(metrics, tiDBGOMAXPROCSMetric, at, maxAge)
	if err != nil {
		return nil, err
	}
	quotaByInstance, err := latestMetricValuesByInstance(metrics, tiDBMemoryQuotaMetric, at, maxAge)
	if err != nil {
		return nil, err
	}
	if len(gomaxByInstance) == 0 {
		return nil, fmt.Errorf("evaluation: TiDB tier metric %q is missing at %s", tiDBGOMAXPROCSMetric, at.Format(timeLayout))
	}

	var resolved *TiDBTierObservation
	for instance, gomax := range gomaxByInstance {
		quota, ok := quotaByInstance[instance]
		if !ok {
			return nil, fmt.Errorf("evaluation: TiDB tier memory quota is missing for instance %q at %s", instance, at.Format(timeLayout))
		}
		observed, err := reconstructTiDBTier(gomax, quota)
		if err != nil {
			return nil, fmt.Errorf("evaluation: TiDB tier instance %q: %w", instance, err)
		}
		if resolved == nil {
			resolved = observed
			continue
		}
		if !sameTiDBTier(resolved, observed) {
			return nil, fmt.Errorf("evaluation: multiple TiDB tiers are active at %s: %s and %s", at.Format(timeLayout), resolved.NodeSpec, observed.NodeSpec)
		}
	}
	return resolved, nil
}

// ResolveStableTiDBTier requires the same uniquely reconstructed tier at every
// replay point. A vertical change must be split into a separate replay window.
func ResolveStableTiDBTier(metrics []MetricSeries, times []time.Time, maxAge time.Duration) (*TiDBTierObservation, error) {
	if len(times) == 0 {
		return nil, fmt.Errorf("evaluation: TiDB tier replay times are required")
	}

	var stable *TiDBTierObservation
	for i := range times {
		observed, err := ResolveTiDBTierAt(metrics, times[i], maxAge)
		if err != nil {
			return nil, err
		}
		if stable == nil {
			stable = observed
			continue
		}
		if !sameTiDBTier(stable, observed) {
			return nil, fmt.Errorf("evaluation: TiDB tier changes within replay window at %s: %s to %s", times[i].Format(timeLayout), stable.NodeSpec, observed.NodeSpec)
		}
	}
	return stable, nil
}

func latestMetricValuesByInstance(metrics []MetricSeries, name string, at time.Time, maxAge time.Duration) (map[string]float64, error) {
	values := make(map[string]float64)
	for i := range metrics {
		series := &metrics[i]
		if series.Name != name {
			continue
		}
		instance := series.Labels[tiDBInstanceLabel]
		if instance == "" {
			return nil, fmt.Errorf("evaluation: TiDB tier metric %q lacks instance label", name)
		}
		sample, ok := latestMetricSampleAt(series.Samples, at)
		if !ok || at.Sub(sample.Timestamp) > maxAge {
			continue
		}
		if _, duplicate := values[instance]; duplicate {
			return nil, fmt.Errorf("evaluation: TiDB tier metric %q duplicates instance %q", name, instance)
		}
		values[instance] = sample.Value
	}
	return values, nil
}

func latestMetricSampleAt(samples []MetricSample, at time.Time) (MetricSample, bool) {
	for i := len(samples) - 1; i >= 0; i-- {
		if !samples[i].Timestamp.After(at) {
			return samples[i], true
		}
	}
	return MetricSample{}, false
}

func reconstructTiDBTier(gomax, quotaBytes float64) (*TiDBTierObservation, error) {
	threads := math.Round(gomax)
	if threads < 1 || math.Abs(gomax-threads) > 1e-9 {
		return nil, fmt.Errorf("GOMAXPROCS %.6g is not a positive integer", gomax)
	}
	quota := math.Round(quotaBytes)
	if quota < bytesPerMiB || math.Abs(quotaBytes-quota) > 0 || int64(quota)%bytesPerMiB != 0 {
		return nil, fmt.Errorf("memory quota %.6g is not a positive whole MiB value", quotaBytes)
	}

	key := historicalTiDBTierKey{GOMAXPROCS: int64(threads), QuotaMiB: uint64(quota) / bytesPerMiB}
	historical, ok := historicalTiDBTiers[key]
	if !ok {
		return nil, fmt.Errorf("GOMAXPROCS %d and memory quota %dMi do not map to a known historical TiDB tier", key.GOMAXPROCS, key.QuotaMiB)
	}

	return &TiDBTierObservation{
		NodeSpec:      historical.NodeSpec,
		CPURequest:    resource.MustParse(historical.CPURequest),
		MemoryRequest: resource.MustParse(historical.MemoryRequest),
	}, nil
}

func sameTiDBTier(left, right *TiDBTierObservation) bool {
	return left.NodeSpec == right.NodeSpec &&
		left.CPURequest.Cmp(right.CPURequest) == 0 &&
		left.MemoryRequest.Cmp(right.MemoryRequest) == 0
}
