// This file calls the production Policy contract and scores decisions against case oracles.
package evaluation

import (
	"context"
	"fmt"
	"time"

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

type Verdict string

const (
	VerdictPass Verdict = "PASS"
	VerdictFail Verdict = "FAIL"
)

type HorizontalChange string

const (
	HorizontalNoScale  HorizontalChange = "NoScale"
	HorizontalScaleOut HorizontalChange = "ScaleOut"
	HorizontalScaleIn  HorizontalChange = "ScaleIn"
)

type Result struct {
	CaseID           string           `json:"caseId"`
	DecisionAt       time.Time        `json:"decisionAt"`
	Component        string           `json:"component"`
	Class            OracleClass      `json:"class"`
	Verdict          Verdict          `json:"verdict"`
	Baseline         Capacity         `json:"baseline"`
	Expected         Capacity         `json:"expected"`
	Actual           Capacity         `json:"actual"`
	HorizontalChange HorizontalChange `json:"horizontalChange"`
	AvoidedReplicas  int32            `json:"avoidedReplicas"`
	Mismatches       []string         `json:"mismatches,omitempty"`
}

// SuiteMetrics reports only quantities that independent decision cases
// catalog can support. Action rate and direction reversals would require a
// continuous replay window and are deliberately absent.
type SuiteMetrics struct {
	TotalCases                       int     `json:"totalCases"`
	PassedCases                      int     `json:"passedCases"`
	FailedCases                      int     `json:"failedCases"`
	UsefulScaleOutCases              int     `json:"usefulScaleOutCases"`
	UsefulScaleOutAccepted           int     `json:"usefulScaleOutAccepted"`
	UsefulScaleOutRecall             float64 `json:"usefulScaleOutRecall"`
	UnnecessaryScaleOutCases         int     `json:"unnecessaryScaleOutCases"`
	UnnecessaryScaleOutRejected      int     `json:"unnecessaryScaleOutRejected"`
	UnnecessaryScaleOutRejectionRate float64 `json:"unnecessaryScaleOutRejectionRate"`
	AvoidedReplicas                  int32   `json:"avoidedReplicas"`
}

type SuiteResult struct {
	Results []Result     `json:"results"`
	Metrics SuiteMetrics `json:"metrics"`
}

// Evaluate calls the policy once and compares its desired capacity with the
// case oracle. It performs no Kubernetes or external API operations.
func Evaluate(ctx context.Context, bundle *CaseBundle, candidate productionpolicy.Policy) (*Result, error) {
	return evaluate(ctx, bundle, candidate, nil)
}

// EvaluateWithProjection selects and renames historical series before calling
// the production policy. The projection contains acquisition-to-runtime metric
// names only; it does not implement policy behavior.
func EvaluateWithProjection(ctx context.Context, bundle *CaseBundle, candidate productionpolicy.Policy, mappings []PolicyMetricMapping) (*Result, error) {
	if len(mappings) == 0 {
		return nil, fmt.Errorf("evaluation: policy metric projection is required")
	}
	return evaluate(ctx, bundle, candidate, mappings)
}

func evaluate(ctx context.Context, bundle *CaseBundle, candidate productionpolicy.Policy, mappings []PolicyMetricMapping) (*Result, error) {
	if err := validateCase(bundle); err != nil {
		return nil, err
	}
	if candidate == nil {
		return nil, fmt.Errorf("evaluation: policy is required")
	}
	if err := ctx.Err(); err != nil {
		return nil, fmt.Errorf("evaluation: context: %w", err)
	}

	policyMetrics := toPolicyMetrics(bundle.Input.Metrics)
	if len(mappings) > 0 {
		var err error
		policyMetrics, err = toProjectedPolicyMetrics(bundle.Input.Metrics, mappings)
		if err != nil {
			return nil, fmt.Errorf("evaluation: case %q: project policy metrics: %w", bundle.ID, err)
		}
	}
	desiredTier, desiredReplicas, err := candidate.Choose(policyMetrics)
	if err != nil {
		return nil, fmt.Errorf("evaluation: case %q: choose capacity: %w", bundle.ID, err)
	}
	actual := &Capacity{Replicas: desiredReplicas}
	if desiredTier != nil {
		actual.Tier = desiredTier.Name()
	}
	if err := validatePolicyCapacity(actual); err != nil {
		return nil, fmt.Errorf("evaluation: case %q: %w", bundle.ID, err)
	}

	mismatches := compareCapacities(&bundle.Oracle.Expected, actual)
	result := &Result{
		CaseID:           bundle.ID,
		DecisionAt:       bundle.Input.DecisionAt,
		Component:        bundle.Input.Component,
		Class:            bundle.Oracle.Class,
		Verdict:          VerdictPass,
		Baseline:         bundle.Baseline,
		Expected:         bundle.Oracle.Expected,
		Actual:           *actual,
		HorizontalChange: horizontalChange(bundle.Input.Current.Replicas, actual.Replicas),
		Mismatches:       mismatches,
	}
	if bundle.Oracle.Expected.Replicas == bundle.Input.Current.Replicas &&
		actual.Replicas == bundle.Input.Current.Replicas &&
		bundle.Baseline.Replicas > bundle.Input.Current.Replicas {
		result.AvoidedReplicas = bundle.Baseline.Replicas - bundle.Input.Current.Replicas
	}
	if len(mismatches) > 0 {
		result.Verdict = VerdictFail
	}
	return result, nil
}

// EvaluateSuite evaluates every case with the same production policy.
func EvaluateSuite(ctx context.Context, bundles []*CaseBundle, candidate productionpolicy.Policy) (*SuiteResult, error) {
	return evaluateSuite(ctx, bundles, candidate, nil)
}

func EvaluateSuiteWithProjection(ctx context.Context, bundles []*CaseBundle, candidate productionpolicy.Policy, mappings []PolicyMetricMapping) (*SuiteResult, error) {
	if len(mappings) == 0 {
		return nil, fmt.Errorf("evaluation: policy metric projection is required")
	}
	return evaluateSuite(ctx, bundles, candidate, mappings)
}

func evaluateSuite(ctx context.Context, bundles []*CaseBundle, candidate productionpolicy.Policy, mappings []PolicyMetricMapping) (*SuiteResult, error) {
	if len(bundles) == 0 {
		return nil, fmt.Errorf("evaluation: suite cases are required")
	}

	suite := &SuiteResult{Results: make([]Result, 0, len(bundles))}
	for i := range bundles {
		result, err := evaluate(ctx, bundles[i], candidate, mappings)
		if err != nil {
			return nil, fmt.Errorf("evaluation: suite case %d: %w", i, err)
		}
		suite.Results = append(suite.Results, *result)
		addSuiteResult(&suite.Metrics, result)
	}
	finishSuiteMetrics(&suite.Metrics)
	return suite, nil
}

func finishSuiteMetrics(metrics *SuiteMetrics) {
	if metrics.UsefulScaleOutCases > 0 {
		metrics.UsefulScaleOutRecall = float64(metrics.UsefulScaleOutAccepted) / float64(metrics.UsefulScaleOutCases)
	}
	if metrics.UnnecessaryScaleOutCases > 0 {
		metrics.UnnecessaryScaleOutRejectionRate = float64(metrics.UnnecessaryScaleOutRejected) / float64(metrics.UnnecessaryScaleOutCases)
	}
}

func addSuiteResult(metrics *SuiteMetrics, result *Result) {
	metrics.TotalCases++
	if result.Verdict == VerdictPass {
		metrics.PassedCases++
	} else {
		metrics.FailedCases++
	}

	switch result.Class {
	case OracleClassUsefulScaleOut:
		metrics.UsefulScaleOutCases++
		if result.HorizontalChange == HorizontalScaleOut {
			metrics.UsefulScaleOutAccepted++
		}
	case OracleClassUnnecessaryScaleOut:
		metrics.UnnecessaryScaleOutCases++
		if result.HorizontalChange == HorizontalNoScale {
			metrics.UnnecessaryScaleOutRejected++
		}
	}
	metrics.AvoidedReplicas += result.AvoidedReplicas
}

func compareCapacities(expected, actual *Capacity) []string {
	mismatches := make([]string, 0, 2)
	if expected.Tier != "" && actual.Tier != expected.Tier {
		mismatches = append(mismatches, fmt.Sprintf("tier=%q, want %q", actual.Tier, expected.Tier))
	}
	if actual.Replicas != expected.Replicas {
		mismatches = append(mismatches, fmt.Sprintf("replicas=%d, want %d", actual.Replicas, expected.Replicas))
	}
	return mismatches
}

func horizontalChange(current, desired int32) HorizontalChange {
	switch {
	case desired > current:
		return HorizontalScaleOut
	case desired < current:
		return HorizontalScaleIn
	default:
		return HorizontalNoScale
	}
}
