package orchestrator

import (
	"errors"
	"fmt"
)

// FalsifierCriteria are the bar a self-proposed org diff must clear before
// it can be applied. See plan §17 Phase J + §18 #15: criteria must be
// written down *before* an org-builder is built.
//
// Default criteria (operational; revisited as benchmark variance is known):
//   - Improvement on a primary benchmark by ≥ MinImprovementFrac
//   - No degradation on any secondary benchmark by more than MaxDegradationFrac
//   - At least MinSampleRuns runs of each benchmark under both orgs
//   - Evaluator-user agreement ≥ MinEvaluatorAgreement on the held-out set
type FalsifierCriteria struct {
	MinImprovementFrac      float64 // e.g. 0.10 → +10% on primary
	MaxDegradationFrac      float64 // e.g. 0.05 → ≤5% drop on any secondary
	MinSampleRuns           int     // e.g. 3
	MinEvaluatorAgreement   float64 // e.g. 0.80 on calibration set
	PrimaryBenchmark        string  // benchmark id
	SecondaryBenchmarks     []string
}

// DefaultFalsifier returns the criteria committed in the plan §17 Phase J.
func DefaultFalsifier() FalsifierCriteria {
	return FalsifierCriteria{
		MinImprovementFrac:    0.10,
		MaxDegradationFrac:    0.05,
		MinSampleRuns:         3,
		MinEvaluatorAgreement: 0.80,
	}
}

// FalsifierResult is the structured outcome of EvaluateDiff.
type FalsifierResult struct {
	Pass          bool
	PrimaryDelta  float64
	WorstSecondary float64
	SampleOk      bool
	AgreementOk   bool
	Rationale     string
}

// EvaluateDiff applies the falsifier to two orgs' measured runs on the same
// benchmark suite. Inputs are *per-benchmark* mean overall-scores under each
// org. EvaluatorAgreement is the calibration metric across the held-out set.
//
// This is the gate Phase J's org-builder must pass before any auto-apply.
// It deliberately doesn't talk to the database — it works on already-loaded
// metrics so callers can run it offline and on aggregated reports.
func EvaluateDiff(crit FalsifierCriteria,
	baselinePrimary, candidatePrimary float64,
	baselineSecondary, candidateSecondary map[string]float64,
	sampleRunsPerBenchmark int,
	evaluatorAgreement float64,
) (FalsifierResult, error) {
	if crit.PrimaryBenchmark == "" {
		return FalsifierResult{}, errors.New("falsifier: primary benchmark must be set")
	}
	r := FalsifierResult{}
	if baselinePrimary == 0 {
		baselinePrimary = 0.0001
	}
	r.PrimaryDelta = (candidatePrimary - baselinePrimary) / baselinePrimary
	improvedEnough := r.PrimaryDelta >= crit.MinImprovementFrac

	worst := 0.0
	for b, bs := range baselineSecondary {
		cs := candidateSecondary[b]
		if bs == 0 {
			bs = 0.0001
		}
		delta := (cs - bs) / bs
		if delta < worst {
			worst = delta
		}
	}
	r.WorstSecondary = worst
	noBadDegrade := worst >= -crit.MaxDegradationFrac

	r.SampleOk = sampleRunsPerBenchmark >= crit.MinSampleRuns
	r.AgreementOk = evaluatorAgreement >= crit.MinEvaluatorAgreement

	r.Pass = improvedEnough && noBadDegrade && r.SampleOk && r.AgreementOk
	r.Rationale = fmt.Sprintf(
		"primary_delta=%.3f (need ≥ %.2f), worst_secondary=%.3f (need ≥ -%.2f), samples=%d (need ≥ %d), agreement=%.2f (need ≥ %.2f)",
		r.PrimaryDelta, crit.MinImprovementFrac,
		r.WorstSecondary, crit.MaxDegradationFrac,
		sampleRunsPerBenchmark, crit.MinSampleRuns,
		evaluatorAgreement, crit.MinEvaluatorAgreement,
	)
	return r, nil
}

// (The org-builder pipeline lives in orgbuilder.go; the falsifier criteria
// here are the gate it calls.)
