// This file schedules continuous replay, builds point-in-time inputs, and summarizes decisions.
package evaluation

import (
	"context"
	"fmt"
	"math"
	"sort"
	"time"

	decisionmetrics "github.com/tidbcloud/cluster-service-ng/internal/capacity-planner/pkg/metrics"
	productionpolicy "github.com/tidbcloud/cluster-service-ng/internal/capacity-planner/pkg/policy"
)

const ReplayVersionV1Alpha1 = "v1alpha1"

type ReplayDataset struct {
	Version        string
	ID             string
	Component      string
	Target         O11YTargetSpec
	Window         O11YQueryWindow
	ArchiveDigest  string
	Metrics        []MetricSeries
	MaxAgeByMetric map[string]time.Duration
}

type ReplaySchedule struct {
	Times    []time.Time
	Interval time.Duration
}

type PolicyMetricMapping struct {
	SourceMetric string `json:"sourceMetric"`
	PolicyMetric string `json:"policyMetric"`
}

type ReplayOptions struct {
	ExperimentID             string
	ExperimentDigest         string
	PolicyRevision           string
	ObservedReplicasMetric   string
	HPADesiredReplicasMetric string
	PolicyMetrics            []PolicyMetricMapping
}

type ReplayDecisionStatus string

const (
	ReplayDecisionEvaluated   ReplayDecisionStatus = "EVALUATED"
	ReplayDecisionSkipped     ReplayDecisionStatus = "SKIPPED"
	ReplayDecisionPolicyError ReplayDecisionStatus = "POLICY_ERROR"
)

type ReplayDecision struct {
	EvaluatedAt        time.Time            `json:"evaluatedAt"`
	Status             ReplayDecisionStatus `json:"status"`
	ObservedReplicas   int32                `json:"observedReplicas,omitempty"`
	HPADesiredReplicas *int32               `json:"hpaDesiredReplicas,omitempty"`
	Desired            Capacity             `json:"desired"`
	HorizontalChange   HorizontalChange     `json:"horizontalChange,omitempty"`
	Message            string               `json:"message,omitempty"`
}

type RecommendationEpisode struct {
	StartAt       time.Time                 `json:"startAt"`
	EndAt         time.Time                 `json:"endAt"`
	DecisionCount int                       `json:"decisionCount"`
	StartReason   RecommendationStartReason `json:"startReason"`
	Desired       Capacity                  `json:"desired"`
}

type RecommendationStartReason string

const (
	RecommendationStartInitial          RecommendationStartReason = "INITIAL"
	RecommendationStartTargetChanged    RecommendationStartReason = "TARGET_CHANGED"
	RecommendationStartAfterSkipped     RecommendationStartReason = "AFTER_SKIPPED_INPUT"
	RecommendationStartAfterPolicyError RecommendationStartReason = "AFTER_POLICY_ERROR"
)

type ReplaySummary struct {
	ScheduledPoints                int     `json:"scheduledPoints"`
	EvaluatedPoints                int     `json:"evaluatedPoints"`
	SkippedPoints                  int     `json:"skippedPoints"`
	PolicyErrorPoints              int     `json:"policyErrorPoints"`
	RecommendationEpisodes         int     `json:"recommendationEpisodes"`
	TargetChanges                  int     `json:"targetChanges"`
	DirectionReversals             int     `json:"directionReversals"`
	ObservedReplicaHours           float64 `json:"observedReplicaHours"`
	RecommendedReplicaHours        float64 `json:"recommendedReplicaHours"`
	ScheduledReplicaHourIntervals  int     `json:"scheduledReplicaHourIntervals"`
	ComparableReplicaHourIntervals int     `json:"comparableReplicaHourIntervals"`
	HPAReferencePoints             int     `json:"hpaReferencePoints"`
	HPATargetChanges               int     `json:"hpaTargetChanges"`
	HPADirectionReversals          int     `json:"hpaDirectionReversals"`
	HPAReplicaHours                float64 `json:"hpaReplicaHours"`
}

type ReplayDatasetIdentity struct {
	ID            string          `json:"id"`
	Component     string          `json:"component"`
	Target        O11YTargetSpec  `json:"target"`
	Window        O11YQueryWindow `json:"window"`
	ArchiveDigest string          `json:"archiveDigest"`
}

type ReplayReport struct {
	Version                   string                  `json:"version"`
	ExperimentID              string                  `json:"experimentId"`
	ExperimentDigest          string                  `json:"experimentDigest,omitempty"`
	PolicyRevision            string                  `json:"policyRevision,omitempty"`
	PolicyType                string                  `json:"policyType,omitempty"`
	EvaluationIntervalSeconds int64                   `json:"evaluationIntervalSeconds,omitempty"`
	Dataset                   ReplayDatasetIdentity   `json:"dataset"`
	PolicyMetrics             []PolicyMetricMapping   `json:"policyMetrics"`
	Summary                   ReplaySummary           `json:"summary"`
	Decisions                 []ReplayDecision        `json:"decisions"`
	Episodes                  []RecommendationEpisode `json:"episodes"`
}

func NewIntervalSchedule(start, end time.Time, interval time.Duration) (*ReplaySchedule, error) {
	if start.IsZero() || end.IsZero() {
		return nil, fmt.Errorf("evaluation: replay schedule start and end are required")
	}
	if start.After(end) {
		return nil, fmt.Errorf("evaluation: replay schedule start must not be after end")
	}
	if interval <= 0 {
		return nil, fmt.Errorf("evaluation: replay interval must be greater than zero")
	}

	times := make([]time.Time, 0, int(end.Sub(start)/interval)+1)
	for at := start; !at.After(end); at = at.Add(interval) {
		times = append(times, at)
	}
	return &ReplaySchedule{Times: times, Interval: interval}, nil
}

func NewSparseSchedule(times []time.Time) (*ReplaySchedule, error) {
	if len(times) == 0 {
		return nil, fmt.Errorf("evaluation: replay times are required")
	}

	sorted := append([]time.Time(nil), times...)
	for i := range sorted {
		if sorted[i].IsZero() {
			return nil, fmt.Errorf("evaluation: replay times[%d] is required", i)
		}
	}
	sort.Slice(sorted, func(i, j int) bool { return sorted[i].Before(sorted[j]) })
	unique := sorted[:1]
	for i := 1; i < len(sorted); i++ {
		if !sorted[i].Equal(unique[len(unique)-1]) {
			unique = append(unique, sorted[i])
		}
	}
	return &ReplaySchedule{Times: unique}, nil
}

func RunReplay(ctx context.Context, dataset *ReplayDataset, schedule *ReplaySchedule, candidate productionpolicy.Policy, options ReplayOptions) (*ReplayReport, error) {
	if err := validateReplayInputs(dataset, schedule, candidate, &options); err != nil {
		return nil, err
	}

	seriesByMetric := indexMetricSeries(dataset.Metrics)
	report := &ReplayReport{
		Version:                   ReplayVersionV1Alpha1,
		ExperimentID:              options.ExperimentID,
		ExperimentDigest:          options.ExperimentDigest,
		PolicyRevision:            options.PolicyRevision,
		EvaluationIntervalSeconds: int64(schedule.Interval / time.Second),
		Dataset: ReplayDatasetIdentity{
			ID:            dataset.ID,
			Component:     dataset.Component,
			Target:        dataset.Target,
			Window:        dataset.Window,
			ArchiveDigest: dataset.ArchiveDigest,
		},
		PolicyMetrics: append([]PolicyMetricMapping(nil), options.PolicyMetrics...),
		Decisions:     make([]ReplayDecision, 0, len(schedule.Times)),
	}

	for i := range schedule.Times {
		if err := ctx.Err(); err != nil {
			return nil, fmt.Errorf("evaluation: replay context: %w", err)
		}
		at := schedule.Times[i]
		hpaDesiredReplicas, err := pointInTimeReferenceReplicas(dataset, seriesByMetric, at, options.HPADesiredReplicasMetric)
		if err != nil {
			return nil, err
		}
		observedReplicas, policyMetrics, err := pointInTimePolicyMetrics(dataset, seriesByMetric, at, &options)
		if err != nil {
			report.Decisions = append(report.Decisions, ReplayDecision{
				EvaluatedAt:        at,
				Status:             ReplayDecisionSkipped,
				HPADesiredReplicas: hpaDesiredReplicas,
				Message:            err.Error(),
			})
			continue
		}

		targetTier, targetReplicas, err := candidate.Choose(policyMetrics)
		if err != nil {
			report.Decisions = append(report.Decisions, ReplayDecision{
				EvaluatedAt:        at,
				Status:             ReplayDecisionPolicyError,
				ObservedReplicas:   observedReplicas,
				HPADesiredReplicas: hpaDesiredReplicas,
				Message:            err.Error(),
			})
			continue
		}
		desired := &Capacity{Replicas: targetReplicas}
		if targetTier != nil {
			desired.Tier = targetTier.Name()
		}
		if err := validatePolicyCapacity(desired); err != nil {
			report.Decisions = append(report.Decisions, ReplayDecision{
				EvaluatedAt:        at,
				Status:             ReplayDecisionPolicyError,
				ObservedReplicas:   observedReplicas,
				HPADesiredReplicas: hpaDesiredReplicas,
				Message:            err.Error(),
			})
			continue
		}

		report.Decisions = append(report.Decisions, ReplayDecision{
			EvaluatedAt:        at,
			Status:             ReplayDecisionEvaluated,
			ObservedReplicas:   observedReplicas,
			HPADesiredReplicas: hpaDesiredReplicas,
			Desired:            *desired,
			HorizontalChange:   horizontalChange(observedReplicas, desired.Replicas),
		})
	}

	summarizeReplay(report, schedule)
	return report, nil
}

func validateReplayInputs(dataset *ReplayDataset, schedule *ReplaySchedule, candidate productionpolicy.Policy, options *ReplayOptions) error {
	if dataset == nil {
		return fmt.Errorf("evaluation: replay dataset is required")
	}
	if dataset.Version != ReplayVersionV1Alpha1 || dataset.ID == "" || dataset.Component == "" {
		return fmt.Errorf("evaluation: replay dataset identity is invalid")
	}
	if dataset.Window.Start.IsZero() || dataset.Window.End.IsZero() || dataset.Window.Start.After(dataset.Window.End) {
		return fmt.Errorf("evaluation: replay dataset window is invalid")
	}
	if len(dataset.Metrics) == 0 || len(dataset.MaxAgeByMetric) == 0 {
		return fmt.Errorf("evaluation: replay dataset metrics are required")
	}
	if schedule == nil || len(schedule.Times) == 0 {
		return fmt.Errorf("evaluation: replay schedule is required")
	}
	for i := range schedule.Times {
		if schedule.Times[i].Before(dataset.Window.Start) || schedule.Times[i].After(dataset.Window.End) {
			return fmt.Errorf("evaluation: replay time %s is outside dataset window", schedule.Times[i])
		}
	}
	if candidate == nil {
		return fmt.Errorf("evaluation: policy is required")
	}
	if options.ExperimentID == "" || options.ObservedReplicasMetric == "" || len(options.PolicyMetrics) == 0 {
		return fmt.Errorf("evaluation: replay experiment id, observed replicas metric, and policy metrics are required")
	}

	seenSources := make(map[string]struct{}, len(options.PolicyMetrics))
	seenTargets := make(map[string]struct{}, len(options.PolicyMetrics))
	for i := range options.PolicyMetrics {
		mapping := &options.PolicyMetrics[i]
		if mapping.SourceMetric == "" || mapping.PolicyMetric == "" {
			return fmt.Errorf("evaluation: policy metrics[%d] source and target are required", i)
		}
		if _, duplicate := seenSources[mapping.SourceMetric]; duplicate {
			return fmt.Errorf("evaluation: source metric %q is duplicated", mapping.SourceMetric)
		}
		if _, duplicate := seenTargets[mapping.PolicyMetric]; duplicate {
			return fmt.Errorf("evaluation: policy metric %q is duplicated", mapping.PolicyMetric)
		}
		seenSources[mapping.SourceMetric] = struct{}{}
		seenTargets[mapping.PolicyMetric] = struct{}{}
	}
	return nil
}

func indexMetricSeries(series []MetricSeries) map[string][]MetricSeries {
	indexed := make(map[string][]MetricSeries)
	for i := range series {
		indexed[series[i].Name] = append(indexed[series[i].Name], series[i])
	}
	return indexed
}

func pointInTimePolicyMetrics(dataset *ReplayDataset, seriesByMetric map[string][]MetricSeries, at time.Time, options *ReplayOptions) (int32, []decisionmetrics.Metrics, error) {
	observedSeries := seriesByMetric[options.ObservedReplicasMetric]
	maxAge := dataset.MaxAgeByMetric[options.ObservedReplicasMetric]
	if len(observedSeries) != 1 || maxAge <= 0 {
		return 0, nil, fmt.Errorf("observed replicas metric %q must have one series and a positive max age", options.ObservedReplicasMetric)
	}
	observedSample, ok := latestFreshSample(observedSeries[0].Samples, at, maxAge)
	if !ok {
		return 0, nil, fmt.Errorf("observed replicas metric %q is stale or missing", options.ObservedReplicasMetric)
	}
	observedReplicas := int32(math.Round(observedSample.Value))
	if observedReplicas < 1 {
		return 0, nil, fmt.Errorf("observed replicas metric %q returned %.3f", options.ObservedReplicasMetric, observedSample.Value)
	}

	policyMetrics := make([]decisionmetrics.Metrics, 0, len(options.PolicyMetrics))
	var expectedInstances []string
	for i := range options.PolicyMetrics {
		mapping := &options.PolicyMetrics[i]
		maxAge := dataset.MaxAgeByMetric[mapping.SourceMetric]
		if maxAge <= 0 {
			return 0, nil, fmt.Errorf("policy source metric %q has no positive max age", mapping.SourceMetric)
		}

		seriesByInstance := make(map[string]decisionmetrics.TimeSeries)
		for j := range seriesByMetric[mapping.SourceMetric] {
			series := &seriesByMetric[mapping.SourceMetric][j]
			instance := series.Labels[metricInstanceLabel]
			if instance == "" {
				return 0, nil, fmt.Errorf("policy source metric %q has a series without instance", mapping.SourceMetric)
			}
			sample, ok := latestFreshSample(series.Samples, at, maxAge)
			if !ok {
				continue
			}
			seriesByInstance[instance] = decisionmetrics.TimeSeries{
				Ref:     &decisionmetrics.Reference{Name: instance},
				Samples: []decisionmetrics.Sample{{Timestamp: sample.Timestamp, Value: sample.Value}},
			}
		}
		if len(seriesByInstance) != int(observedReplicas) {
			return 0, nil, fmt.Errorf("policy source metric %q has %d fresh instances, want %d", mapping.SourceMetric, len(seriesByInstance), observedReplicas)
		}

		instances := make([]string, 0, len(seriesByInstance))
		for instance := range seriesByInstance {
			instances = append(instances, instance)
		}
		sort.Strings(instances)
		if i > 0 && !equalStrings(expectedInstances, instances) {
			return 0, nil, fmt.Errorf("policy source metric %q has a different fresh instance set", mapping.SourceMetric)
		}
		expectedInstances = instances

		metric := decisionmetrics.Metrics{Series: make([]decisionmetrics.TimeSeries, 0, len(instances))}
		for _, instance := range instances {
			metric.Series = append(metric.Series, seriesByInstance[instance])
		}
		policyMetrics = append(policyMetrics, metric)
	}
	return observedReplicas, policyMetrics, nil
}

func latestFreshSample(samples []MetricSample, at time.Time, maxAge time.Duration) (MetricSample, bool) {
	index := sort.Search(len(samples), func(i int) bool {
		return samples[i].Timestamp.After(at)
	})
	if index == 0 {
		return MetricSample{}, false
	}
	sample := samples[index-1]
	if at.Sub(sample.Timestamp) > maxAge {
		return MetricSample{}, false
	}
	return sample, true
}

func pointInTimeReferenceReplicas(dataset *ReplayDataset, seriesByMetric map[string][]MetricSeries, at time.Time, metric string) (*int32, error) {
	if metric == "" {
		return nil, nil
	}
	series := seriesByMetric[metric]
	maxAge := dataset.MaxAgeByMetric[metric]
	if len(series) != 1 || maxAge <= 0 {
		return nil, fmt.Errorf("HPA desired replicas metric %q must have one series and a positive max age", metric)
	}
	sample, ok := latestFreshSample(series[0].Samples, at, maxAge)
	if !ok {
		return nil, nil
	}
	replicas := int32(math.Round(sample.Value))
	if replicas < 1 {
		return nil, fmt.Errorf("HPA desired replicas metric %q returned %.3f", metric, sample.Value)
	}
	return &replicas, nil
}

func equalStrings(left, right []string) bool {
	if len(left) != len(right) {
		return false
	}
	for i := range left {
		if left[i] != right[i] {
			return false
		}
	}
	return true
}

func summarizeReplay(report *ReplayReport, schedule *ReplaySchedule) {
	report.Summary.ScheduledPoints = len(report.Decisions)
	if schedule.Interval > 0 && len(report.Decisions) > 1 {
		report.Summary.ScheduledReplicaHourIntervals = len(report.Decisions) - 1
	}
	var currentEpisode *RecommendationEpisode
	pendingStartReason := RecommendationStartReason("")
	lastEvaluatedIndex := -1
	lastDesired := Capacity{}
	lastDirection := 0
	var lastHPADesired int32
	lastHPAIndex := -1
	lastHPADirection := 0

	for i := range report.Decisions {
		decision := &report.Decisions[i]
		switch decision.Status {
		case ReplayDecisionSkipped:
			report.Summary.SkippedPoints++
			currentEpisode = nil
			pendingStartReason = RecommendationStartAfterSkipped
			lastEvaluatedIndex = -1
			lastDirection = 0
			lastHPAIndex = -1
			lastHPADirection = 0
			continue
		case ReplayDecisionPolicyError:
			report.Summary.PolicyErrorPoints++
			currentEpisode = nil
			pendingStartReason = RecommendationStartAfterPolicyError
			lastEvaluatedIndex = -1
			lastDirection = 0
			lastHPAIndex = -1
			lastHPADirection = 0
			continue
		case ReplayDecisionEvaluated:
			report.Summary.EvaluatedPoints++
		}

		if decision.HPADesiredReplicas != nil {
			report.Summary.HPAReferencePoints++
			if lastHPAIndex >= 0 && lastHPAIndex == i-1 {
				direction := changeDirection(*decision.HPADesiredReplicas - lastHPADesired)
				if direction != 0 {
					report.Summary.HPATargetChanges++
					if lastHPADirection != 0 && direction != lastHPADirection {
						report.Summary.HPADirectionReversals++
					}
					lastHPADirection = direction
				}
			}
			lastHPAIndex = i
			lastHPADesired = *decision.HPADesiredReplicas
		} else {
			lastHPAIndex = -1
			lastHPADirection = 0
		}

		if currentEpisode == nil || currentEpisode.Desired != decision.Desired {
			startReason := RecommendationStartTargetChanged
			if len(report.Episodes) == 0 {
				startReason = RecommendationStartInitial
			} else if pendingStartReason != "" {
				startReason = pendingStartReason
			}
			report.Episodes = append(report.Episodes, RecommendationEpisode{
				StartAt:       decision.EvaluatedAt,
				EndAt:         decision.EvaluatedAt,
				DecisionCount: 1,
				StartReason:   startReason,
				Desired:       decision.Desired,
			})
			currentEpisode = &report.Episodes[len(report.Episodes)-1]
		} else {
			currentEpisode.EndAt = decision.EvaluatedAt
			currentEpisode.DecisionCount++
		}
		pendingStartReason = ""

		if lastEvaluatedIndex >= 0 && lastEvaluatedIndex == i-1 {
			direction := changeDirection(decision.Desired.Replicas - lastDesired.Replicas)
			if direction != 0 {
				report.Summary.TargetChanges++
				if lastDirection != 0 && direction != lastDirection {
					report.Summary.DirectionReversals++
				}
				lastDirection = direction
			}
		}
		lastEvaluatedIndex = i
		lastDesired = decision.Desired

		if schedule.Interval > 0 && i+1 < len(report.Decisions) && decision.HPADesiredReplicas != nil {
			hours := report.Decisions[i+1].EvaluatedAt.Sub(decision.EvaluatedAt).Hours()
			report.Summary.ComparableReplicaHourIntervals++
			report.Summary.ObservedReplicaHours += float64(decision.ObservedReplicas) * hours
			report.Summary.RecommendedReplicaHours += float64(decision.Desired.Replicas) * hours
			report.Summary.HPAReplicaHours += float64(*decision.HPADesiredReplicas) * hours
		}
	}
	report.Summary.RecommendationEpisodes = len(report.Episodes)
}

func changeDirection(delta int32) int {
	if delta > 0 {
		return 1
	}
	if delta < 0 {
		return -1
	}
	return 0
}
