// This file loads experiment configuration and constructs the selected production Policy replay.
package evaluation

import (
	"context"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
	"time"

	"sigs.k8s.io/yaml"

	"github.com/tidbcloud/cluster-service-ng/internal/capacity-planner/api/v1alpha1"
	productionpolicy "github.com/tidbcloud/cluster-service-ng/internal/capacity-planner/pkg/policy"
	"github.com/tidbcloud/cluster-service-ng/internal/capacity-planner/pkg/replicas"
	"github.com/tidbcloud/cluster-service-ng/internal/capacity-planner/pkg/tier"
)

const maxReplayExperimentSpecBytes = 1 << 20

type ReplayPolicyMetricSpec struct {
	SourceMetric string                        `json:"sourceMetric"`
	Resource     v1alpha1.ResourceMetricSource `json:"resource"`
}

type ReplayPolicySpec struct {
	Type          v1alpha1.AutoscalingPolicy `json:"type"`
	TierTemplates string                     `json:"tierTemplates"`
	Replicas      *v1alpha1.ReplicasSpec     `json:"replicas,omitempty"`
	Tiers         v1alpha1.Tiers             `json:"tiers"`
	Metrics       []ReplayPolicyMetricSpec   `json:"metrics"`
}

// ReplayExperimentTarget selects the logical cluster to resolve or fetch.
type ReplayExperimentTarget struct {
	ClusterID string `json:"clusterId"`
}

// ReplayExperimentWindow selects the inclusive historical range to evaluate.
type ReplayExperimentWindow struct {
	Start time.Time `json:"start"`
	End   time.Time `json:"end"`
}

type ReplayExperimentSpec struct {
	Version                   string                 `json:"version"`
	ID                        string                 `json:"id"`
	CasesDir                  string                 `json:"casesDir"`
	Component                 string                 `json:"component"`
	MetricsProfile            string                 `json:"metricsProfile"`
	Target                    ReplayExperimentTarget `json:"target"`
	Window                    ReplayExperimentWindow `json:"window"`
	DataStepSeconds           int64                  `json:"dataStepSeconds"`
	EvaluationIntervalSeconds int64                  `json:"evaluationIntervalSeconds"`
	ObservedReplicasMetric    string                 `json:"observedReplicasMetric"`
	HPADesiredReplicasMetric  string                 `json:"hpaDesiredReplicasMetric,omitempty"`
	Policy                    ReplayPolicySpec       `json:"policy"`
}

type ReplayExperiment struct {
	Spec              ReplayExperimentSpec
	Digest            string
	CasesDir          string
	TierTemplatesPath string
}

func DecodeReplayExperimentSpec(r io.Reader) (*ReplayExperimentSpec, []byte, error) {
	data, err := io.ReadAll(io.LimitReader(r, maxReplayExperimentSpecBytes+1))
	if err != nil {
		return nil, nil, fmt.Errorf("evaluation: read replay experiment: %w", err)
	}
	if len(data) > maxReplayExperimentSpecBytes {
		return nil, nil, fmt.Errorf("evaluation: replay experiment exceeds %d bytes", maxReplayExperimentSpecBytes)
	}

	var spec ReplayExperimentSpec
	if err := yaml.UnmarshalStrict(data, &spec); err != nil {
		return nil, nil, fmt.Errorf("evaluation: decode replay experiment: %w", err)
	}
	if err := validateReplayExperimentSpec(&spec); err != nil {
		return nil, nil, err
	}
	return &spec, data, nil
}

func LoadReplayExperiment(path string) (*ReplayExperiment, error) {
	file, err := os.Open(path)
	if err != nil {
		return nil, fmt.Errorf("evaluation: open replay experiment %q: %w", path, err)
	}
	spec, data, decodeErr := DecodeReplayExperimentSpec(file)
	closeErr := file.Close()
	if decodeErr != nil {
		return nil, decodeErr
	}
	if closeErr != nil {
		return nil, fmt.Errorf("evaluation: close replay experiment %q: %w", path, closeErr)
	}

	baseDir := filepath.Dir(path)
	return &ReplayExperiment{
		Spec:              *spec,
		Digest:            digestBytes(data),
		CasesDir:          resolveExperimentPath(baseDir, spec.CasesDir),
		TierTemplatesPath: resolveExperimentPath(baseDir, spec.Policy.TierTemplates),
	}, nil
}

// BuildReplayPolicy selects the one configured tier matching historical
// evidence before constructing the production CostOptimized policy.
func BuildReplayPolicy(experiment *ReplayExperiment, observed *TiDBTierObservation) (productionpolicy.Policy, []PolicyMetricMapping, error) {
	if experiment == nil {
		return nil, nil, fmt.Errorf("evaluation: replay experiment is required")
	}
	if observed == nil {
		return nil, nil, fmt.Errorf("evaluation: observed TiDB tier is required")
	}
	if err := validateReplayExperimentSpec(&experiment.Spec); err != nil {
		return nil, nil, err
	}
	if experiment.Spec.Policy.Type != v1alpha1.AutoscalingPolicyCostOptimized {
		return nil, nil, fmt.Errorf("evaluation: policy %q is unsupported", experiment.Spec.Policy.Type)
	}

	templates, err := tier.LoadTemplatesFile(experiment.TierTemplatesPath)
	if err != nil {
		return nil, nil, fmt.Errorf("evaluation: load tier templates: %w", err)
	}
	tiers, err := tier.New(templates, &experiment.Spec.Policy.Tiers)
	if err != nil {
		return nil, nil, fmt.Errorf("evaluation: materialize policy tiers: %w", err)
	}
	selectedTier, err := selectObservedTiDBTier(tiers, observed)
	if err != nil {
		return nil, nil, err
	}

	calculators := make([]replicas.Calculator, 0, len(experiment.Spec.Policy.Metrics))
	mappings := make([]PolicyMetricMapping, 0, len(experiment.Spec.Policy.Metrics))
	for i := range experiment.Spec.Policy.Metrics {
		input := &experiment.Spec.Policy.Metrics[i]
		name := string(input.Resource.Name)
		target := &input.Resource.Target
		calculator, err := replicas.NewTargetUtilizationCalculator(name, *target.AverageUtilization)
		if err != nil {
			return nil, nil, fmt.Errorf("evaluation: policy metrics[%d]: %w", i, err)
		}
		calculators = append(calculators, calculator)
		mappings = append(mappings, PolicyMetricMapping{SourceMetric: input.SourceMetric, PolicyMetric: name})
	}

	candidate, err := productionpolicy.NewCostOptimized([]tier.Tier{selectedTier}, calculators, experiment.Spec.Policy.Replicas)
	if err != nil {
		return nil, nil, fmt.Errorf("evaluation: create CostOptimized policy: %w", err)
	}
	return candidate, mappings, nil
}

func selectObservedTiDBTier(candidates []tier.Tier, observed *TiDBTierObservation) (tier.Tier, error) {
	var selected tier.Tier
	for i := range candidates {
		cpu, hasCPU := candidates[i].Capacity("cpu")
		memory, hasMemory := candidates[i].Capacity("memory")
		if !hasCPU || !hasMemory || cpu.Cmp(observed.CPURequest) != 0 || memory.Cmp(observed.MemoryRequest) != 0 {
			continue
		}
		if selected != nil {
			return nil, fmt.Errorf("evaluation: multiple configured tiers match observed TiDB tier %s", observed.NodeSpec)
		}
		selected = candidates[i]
	}
	if selected == nil {
		return nil, fmt.Errorf("evaluation: no configured tier matches observed TiDB tier %s with CPU %s and memory %s", observed.NodeSpec, observed.CPURequest.String(), observed.MemoryRequest.String())
	}
	return selected, nil
}

func RunReplayExperiment(ctx context.Context, experiment *ReplayExperiment, policyRevision string) (*ReplayReport, error) {
	return RunReplayExperimentWithOptions(ctx, experiment, policyRevision, ReplayDatasetResolveOptions{})
}

func RunReplayExperimentWithOptions(ctx context.Context, experiment *ReplayExperiment, policyRevision string, resolveOptions ReplayDatasetResolveOptions) (*ReplayReport, error) {
	if experiment == nil {
		return nil, fmt.Errorf("evaluation: replay experiment is required")
	}
	dataset, _, err := ResolveReplayDataset(ctx, experiment, resolveOptions)
	if err != nil {
		return nil, err
	}
	interval := time.Duration(experiment.Spec.EvaluationIntervalSeconds) * time.Second
	dataStep := time.Duration(dataset.Window.StepSeconds) * time.Second
	if interval < dataStep || interval%dataStep != 0 {
		return nil, fmt.Errorf("evaluation: replay interval %s must be a multiple of dataset step %s", interval, dataStep)
	}
	schedule, err := NewIntervalSchedule(dataset.Window.Start, dataset.Window.End, interval)
	if err != nil {
		return nil, err
	}

	tierMaxAge, ok := dataset.MaxAgeByMetric[tiDBGOMAXPROCSMetric]
	if !ok || tierMaxAge <= 0 {
		return nil, fmt.Errorf("evaluation: replay metric %q with max age is required", tiDBGOMAXPROCSMetric)
	}
	observedTier, err := ResolveStableTiDBTier(dataset.Metrics, schedule.Times, tierMaxAge)
	if err != nil {
		return nil, fmt.Errorf("evaluation: resolve replay TiDB tier: %w", err)
	}
	candidate, mappings, err := BuildReplayPolicy(experiment, observedTier)
	if err != nil {
		return nil, err
	}
	report, err := RunReplay(ctx, dataset, schedule, candidate, ReplayOptions{
		ExperimentID:             experiment.Spec.ID,
		ExperimentDigest:         experiment.Digest,
		PolicyRevision:           policyRevision,
		ObservedReplicasMetric:   experiment.Spec.ObservedReplicasMetric,
		HPADesiredReplicasMetric: experiment.Spec.HPADesiredReplicasMetric,
		PolicyMetrics:            mappings,
	})
	if err != nil {
		return nil, err
	}
	report.PolicyType = string(experiment.Spec.Policy.Type)
	return report, nil
}

func validateReplayExperimentSpec(spec *ReplayExperimentSpec) error {
	if spec == nil {
		return fmt.Errorf("evaluation: replay experiment is required")
	}
	if spec.Version != ReplayVersionV1Alpha1 || spec.ID == "" {
		return fmt.Errorf("evaluation: replay experiment version and id are required")
	}
	if spec.CasesDir == "" || spec.Component == "" || spec.MetricsProfile == "" || spec.DataStepSeconds < 1 || spec.EvaluationIntervalSeconds < 1 || spec.ObservedReplicasMetric == "" {
		return fmt.Errorf("evaluation: replay casesDir, component, metricsProfile, data step, interval, and observed replicas metric are required")
	}
	if !isSafeQueryName(spec.Component) {
		return fmt.Errorf("evaluation: replay component %q is invalid", spec.Component)
	}
	if !isMetricsProfileReference(spec.MetricsProfile) || !strings.HasPrefix(spec.MetricsProfile, spec.Component+"/") {
		return fmt.Errorf("evaluation: replay metricsProfile %q does not match component %q", spec.MetricsProfile, spec.Component)
	}
	if spec.Target.ClusterID == "" {
		return fmt.Errorf("evaluation: replay target.clusterId is required")
	}
	if spec.Window.Start.IsZero() || spec.Window.End.IsZero() || !spec.Window.Start.Before(spec.Window.End) {
		return fmt.Errorf("evaluation: replay window start must be before end")
	}
	if spec.Policy.Type != v1alpha1.AutoscalingPolicyCostOptimized {
		return fmt.Errorf("evaluation: replay policy type %q is unsupported", spec.Policy.Type)
	}
	if spec.Policy.TierTemplates == "" {
		return fmt.Errorf("evaluation: replay policy tierTemplates is required")
	}
	if len(spec.Policy.Metrics) == 0 {
		return fmt.Errorf("evaluation: replay policy metrics are required")
	}

	seenSources := make(map[string]struct{}, len(spec.Policy.Metrics))
	seenResources := make(map[string]struct{}, len(spec.Policy.Metrics))
	for i := range spec.Policy.Metrics {
		metric := &spec.Policy.Metrics[i]
		name := string(metric.Resource.Name)
		if metric.SourceMetric == "" || name == "" {
			return fmt.Errorf("evaluation: replay policy metrics[%d] sourceMetric and resource.name are required", i)
		}
		if _, duplicate := seenSources[metric.SourceMetric]; duplicate {
			return fmt.Errorf("evaluation: replay policy source metric %q is duplicated", metric.SourceMetric)
		}
		if _, duplicate := seenResources[name]; duplicate {
			return fmt.Errorf("evaluation: replay policy resource %q is duplicated", name)
		}
		seenSources[metric.SourceMetric] = struct{}{}
		seenResources[name] = struct{}{}

		target := &metric.Resource.Target
		if target.Type != v1alpha1.MetricTargetTypeUtilization || target.AverageUtilization == nil || *target.AverageUtilization < 1 || *target.AverageUtilization > 100 {
			return fmt.Errorf("evaluation: replay policy metrics[%d] requires utilization between 1 and 100", i)
		}
	}
	return nil
}

func selectReplayDataset(experiment *ReplayExperiment, dataset *ReplayDataset) (*ReplayDataset, error) {
	if dataset.Target.ClusterID != experiment.Spec.Target.ClusterID {
		return nil, fmt.Errorf("evaluation: replay target clusterId %q does not match dataset clusterId %q", experiment.Spec.Target.ClusterID, dataset.Target.ClusterID)
	}
	window := experiment.Spec.Window
	if window.Start.Before(dataset.Window.Start) || window.End.After(dataset.Window.End) {
		return nil, fmt.Errorf("evaluation: replay window %s to %s is outside dataset window %s to %s", window.Start, window.End, dataset.Window.Start, dataset.Window.End)
	}

	selected := *dataset
	selected.Window.Start = window.Start
	selected.Window.End = window.End
	return &selected, nil
}

func resolveExperimentPath(baseDir, path string) string {
	if filepath.IsAbs(path) {
		return filepath.Clean(path)
	}
	return filepath.Clean(filepath.Join(baseDir, path))
}
