package policy

import (
	"fmt"
	"math"

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

// CostOptimized chooses the lowest total-cost capacity that satisfies every
// metric and the configured replica bounds.
type CostOptimized struct {
	tiers       []tier.Tier
	calculators []replicas.Calculator
	bounds      *v1alpha1.ReplicasSpec
}

var _ Policy = (*CostOptimized)(nil)

// NewCostOptimized creates a cost-optimized policy. Callers own the inputs and
// must not mutate them while the policy is in use.
func NewCostOptimized(tiers []tier.Tier, calculators []replicas.Calculator, bounds *v1alpha1.ReplicasSpec) (*CostOptimized, error) {
	if len(tiers) == 0 {
		return nil, fmt.Errorf("policy: tiers are required")
	}
	if len(calculators) == 0 {
		return nil, fmt.Errorf("policy: calculators are required")
	}
	if bounds != nil {
		if bounds.Min < 0 {
			return nil, fmt.Errorf("policy: replicas.min must not be negative")
		}
		if bounds.Max < 1 {
			return nil, fmt.Errorf("policy: replicas.max must be greater than zero")
		}
		if bounds.Max < bounds.Min {
			return nil, fmt.Errorf("policy: replicas.max must be greater than or equal to replicas.min")
		}
	}
	return &CostOptimized{tiers: tiers, calculators: calculators, bounds: bounds}, nil
}

// Choose evaluates every tier against every metric. For one candidate tier the
// desired replica count is the maximum recommendation from all metrics.
func (p *CostOptimized) Choose(ms []metrics.Metrics) (tier.Tier, int32, error) {
	if len(ms) == 0 {
		return nil, 0, fmt.Errorf("policy: metrics are required")
	}

	var bestTier tier.Tier
	var bestReplicas int32
	bestCost := int64(math.MaxInt64)

	for i := range p.tiers {
		candidate := p.tiers[i]
		desiredReplicas, err := p.replicasForTier(ms, candidate)
		if err != nil {
			return nil, 0, err
		}
		desiredReplicas = p.applyMin(desiredReplicas)
		if p.exceedsMax(desiredReplicas) {
			continue
		}

		cost, err := totalCost(candidate, desiredReplicas)
		if err != nil {
			return nil, 0, fmt.Errorf("policy: tier %q: %w", candidate.Name(), err)
		}
		if bestTier == nil || cost < bestCost {
			bestTier = candidate
			bestReplicas = desiredReplicas
			bestCost = cost
		}
	}

	if bestTier == nil {
		return nil, 0, fmt.Errorf("policy: no tier satisfies replica bounds")
	}
	return bestTier, bestReplicas, nil
}

func (p *CostOptimized) replicasForTier(ms []metrics.Metrics, candidate tier.Tier) (int32, error) {
	if len(ms) != len(p.calculators) {
		return 0, fmt.Errorf("policy: got %d metric results, want %d", len(ms), len(p.calculators))
	}
	var desiredReplicas int32
	for i := range ms {
		metric := &ms[i]
		replicas, err := p.calculators[i].Calculate(metric, candidate)
		if err != nil {
			return 0, fmt.Errorf("policy: tier %q metric[%d]: %w", candidate.Name(), i, err)
		}
		if replicas > desiredReplicas {
			desiredReplicas = replicas
		}
	}
	return desiredReplicas, nil
}

func (p *CostOptimized) applyMin(replicas int32) int32 {
	if p.bounds != nil && replicas < p.bounds.Min {
		return p.bounds.Min
	}
	return replicas
}

func (p *CostOptimized) exceedsMax(replicas int32) bool {
	return p.bounds != nil && replicas > p.bounds.Max
}

func totalCost(candidate tier.Tier, replicas int32) (int64, error) {
	tierCost := candidate.Cost()
	if tierCost < 0 {
		return 0, fmt.Errorf("tier cost must not be negative")
	}
	if replicas < 0 {
		return 0, fmt.Errorf("replicas must not be negative")
	}
	if tierCost != 0 && int64(replicas) > math.MaxInt64/tierCost {
		return 0, fmt.Errorf("total cost overflows int64")
	}
	return tierCost * int64(replicas), nil
}
