// This file defines and validates the normalized case input consumed by evaluation.
package evaluation

import (
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"time"
)

const CaseBundleVersionV1Alpha1 = "v1alpha1"

type DataSource string

const (
	DataSourceManual DataSource = "manual"
	DataSourceO11Y   DataSource = "o11y"
)

type OracleClass string

const (
	OracleClassUnnecessaryScaleOut OracleClass = "UnnecessaryScaleOut"
	OracleClassUsefulScaleOut      OracleClass = "UsefulScaleOut"
)

// Capacity is the policy target. Historical cases may omit Tier when their
// evidence establishes only the replica target.
type Capacity struct {
	Tier     string `json:"tier,omitempty"`
	Replicas int32  `json:"replicas"`
}

// MetricSample is one normalized observation in a metric series.
type MetricSample struct {
	Timestamp time.Time `json:"timestamp"`
	Value     float64   `json:"value"`
}

// MetricSeries preserves the labels needed to evaluate skew and hotspot cases.
type MetricSeries struct {
	Name    string            `json:"name"`
	Labels  map[string]string `json:"labels,omitempty"`
	Samples []MetricSample    `json:"samples"`
}

// CaseInput records the pre-decision state. Only Metrics are passed to the
// production policy; the remaining fields belong to evaluation and reporting.
type CaseInput struct {
	DecisionAt time.Time      `json:"decisionAt"`
	Component  string         `json:"component"`
	ClusterID  string         `json:"clusterId,omitempty"`
	Current    Capacity       `json:"current"`
	Metrics    []MetricSeries `json:"metrics"`
}

type Oracle struct {
	Class    OracleClass `json:"class"`
	Expected Capacity    `json:"expected"`
}

type DataQuality struct {
	IsPartial        bool     `json:"isPartial"`
	MissingSeries    []string `json:"missingSeries,omitempty"`
	GappedSeries     []string `json:"gappedSeries,omitempty"`
	IncompleteSeries []string `json:"incompleteSeries,omitempty"`
}

// CaseBundle is the normalized input shared by manual and O11Y cases.
type CaseBundle struct {
	Version  string      `json:"version"`
	ID       string      `json:"id"`
	Source   DataSource  `json:"source"`
	Input    CaseInput   `json:"input"`
	Baseline Capacity    `json:"baseline"`
	Oracle   Oracle      `json:"oracle"`
	Quality  DataQuality `json:"quality"`
}

// DecodeCase decodes one strict JSON CaseBundle and validates its policy input.
func DecodeCase(r io.Reader) (*CaseBundle, error) {
	decoder := json.NewDecoder(r)
	decoder.DisallowUnknownFields()

	var bundle CaseBundle
	if err := decoder.Decode(&bundle); err != nil {
		return nil, fmt.Errorf("evaluation: decode case: %w", err)
	}

	var extra any
	if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) {
		if err == nil {
			return nil, fmt.Errorf("evaluation: decode case: multiple JSON values")
		}
		return nil, fmt.Errorf("evaluation: decode case trailing data: %w", err)
	}

	if err := validateCase(&bundle); err != nil {
		return nil, err
	}
	return &bundle, nil
}

func validateCase(bundle *CaseBundle) error {
	if bundle == nil {
		return fmt.Errorf("evaluation: case is required")
	}
	if bundle.Version != CaseBundleVersionV1Alpha1 {
		return fmt.Errorf("evaluation: case version %q is unsupported", bundle.Version)
	}
	if bundle.ID == "" {
		return fmt.Errorf("evaluation: case id is required")
	}
	switch bundle.Source {
	case DataSourceManual, DataSourceO11Y:
	default:
		return fmt.Errorf("evaluation: case source %q is unsupported", bundle.Source)
	}
	if bundle.Input.DecisionAt.IsZero() {
		return fmt.Errorf("evaluation: decisionAt is required")
	}
	if bundle.Input.Component == "" {
		return fmt.Errorf("evaluation: component is required")
	}
	if err := validateCaseCapacity(&bundle.Input.Current, "current"); err != nil {
		return err
	}
	if len(bundle.Input.Metrics) == 0 {
		return fmt.Errorf("evaluation: metrics are required")
	}
	if bundle.Quality.IsPartial {
		return fmt.Errorf("evaluation: partial metrics cannot be evaluated")
	}
	if len(bundle.Quality.MissingSeries) > 0 {
		return fmt.Errorf("evaluation: missing metric series: %v", bundle.Quality.MissingSeries)
	}
	if len(bundle.Quality.GappedSeries) > 0 {
		return fmt.Errorf("evaluation: gapped metric series: %v", bundle.Quality.GappedSeries)
	}
	if len(bundle.Quality.IncompleteSeries) > 0 {
		return fmt.Errorf("evaluation: incomplete metric series: %v", bundle.Quality.IncompleteSeries)
	}

	for i := range bundle.Input.Metrics {
		series := &bundle.Input.Metrics[i]
		if series.Name == "" {
			return fmt.Errorf("evaluation: metrics[%d].name is required", i)
		}
		if len(series.Samples) == 0 {
			return fmt.Errorf("evaluation: metrics[%d].samples are required", i)
		}

		var previous time.Time
		for j := range series.Samples {
			sample := &series.Samples[j]
			if sample.Timestamp.IsZero() {
				return fmt.Errorf("evaluation: metrics[%d].samples[%d].timestamp is required", i, j)
			}
			if sample.Timestamp.After(bundle.Input.DecisionAt) {
				return fmt.Errorf("evaluation: metrics[%d].samples[%d] is after decisionAt", i, j)
			}
			if !previous.IsZero() && sample.Timestamp.Before(previous) {
				return fmt.Errorf("evaluation: metrics[%d].samples must be ordered by timestamp", i)
			}
			previous = sample.Timestamp
		}
	}

	if err := validateCaseCapacity(&bundle.Baseline, "baseline"); err != nil {
		return err
	}
	switch bundle.Oracle.Class {
	case OracleClassUnnecessaryScaleOut, OracleClassUsefulScaleOut:
	default:
		return fmt.Errorf("evaluation: oracle class %q is unsupported", bundle.Oracle.Class)
	}
	return validateCaseCapacity(&bundle.Oracle.Expected, "oracle.expected")
}

func validateCaseCapacity(capacity *Capacity, field string) error {
	if capacity.Replicas < 1 {
		return fmt.Errorf("evaluation: %s.replicas must be greater than zero", field)
	}
	return nil
}

func validatePolicyCapacity(capacity *Capacity) error {
	if capacity == nil {
		return fmt.Errorf("evaluation: policy returned no capacity")
	}
	if capacity.Tier == "" {
		return fmt.Errorf("evaluation: actual.tier is required")
	}
	return validateCaseCapacity(capacity, "actual")
}
