// Package verify adds an active corroboration layer at the boundary where one
// agent's judgment is passed to a stronger downstream agent — without eroding
// the cost win that makes a heterogeneous swarm worthwhile.
//
// The passive gate (internal/swarm/quality) discounts a cheap model's judgment
// so it must survive decay to be seen. Active corroboration goes further: at
// the handoff it decides whether to Promote a signal (corroborated — raise its
// weight so it reaches the reasoner), Reject it (verified false — drop it), or
// Pass (leave it to the passive gate).
//
// The whole point of a heterogeneous swarm is spending less. Verifying every
// cheap-model judgment with a strong model would pay frontier prices on the
// bulk work and destroy that. So corroboration is cost-aware by construction,
// in this strict order:
//
//  1. Trust frontier-sourced judgments as-is — no re-check needed.
//  2. Free corroboration first: if a deterministic tool already grounds the
//     claim (nuclei/sqlmap hit) or two independent signals agree, promote it at
//     ZERO LLM cost. This handles the common case.
//  3. Low-stakes signals are never worth paying to verify — let the passive gate
//     and decay handle them.
//  4. Only a HIGH-STAKES, UNCORROBORATED judgment from a CHEAP model — the exact
//     slice where a wrong pass makes the exploit agent burn budget on a phantom
//     — triggers one paid strong-model verification, and only while a hard
//     per-campaign cap remains. Out of budget → fall back to the passive gate.
//
// Net: verification is the rare exception, it is bounded, and it pays for itself
// by preventing wasted downstream exploit spend — so it protects the "less
// money" property instead of eroding it.
package verify

import (
	"context"
	"sync"

	"github.com/Armur-Ai/Pentest-Swarm-AI/internal/pipeline"
	"github.com/Armur-Ai/Pentest-Swarm-AI/internal/swarm/blackboard"
	"github.com/Armur-Ai/Pentest-Swarm-AI/internal/swarm/quality"
)

// Decision is the outcome of corroborating a candidate at a context boundary.
type Decision int

const (
	// Pass leaves the candidate's weight unchanged — the passive pheromone gate
	// and decay decide its fate. The default, zero-cost outcome.
	Pass Decision = iota
	// Promote raises the candidate's weight so it reliably reaches the
	// downstream reasoner (it was corroborated).
	Promote
	// Reject drops the candidate — a paid verification found it isn't real.
	Reject
)

func (d Decision) String() string {
	switch d {
	case Promote:
		return "promote"
	case Reject:
		return "reject"
	default:
		return "pass"
	}
}

// Candidate is a judgment being handed from one agent to a stronger one.
type Candidate struct {
	Target    string
	Type      blackboard.FindingType
	Severity  pipeline.Severity
	Detail    string  // the finding summary, so a verifier has something to judge
	Model     string  // the model that produced the judgment (→ reliability tier)
	Pheromone float64 // its current weight

	// Spec is an optional recipe for re-proving the finding against the live
	// target (the parameterised attack/control context a single request can't
	// carry). When present, the live oracles use it; when nil, the finding is
	// not oracle-probed and falls through to the model verifier.
	Spec *ProbeSpec
}

// Verifier re-derives a judgment with a stronger model. It is kept behind an
// interface so the (expensive) call is made only when the policy allows, and so
// the policy is testable without a live model.
type Verifier interface {
	// Verify reports whether the candidate is a real, exploitable finding, with
	// a confidence in [0,1]. Implementations wrap a strong provider.
	Verify(ctx context.Context, c Candidate) (real bool, confidence float64, err error)
}

// Evidence is the read side the corroborator needs for free corroboration:
// independent support already sitting on the board for the same target.
type Evidence interface {
	// SupportingSignals returns how many independent signals reference the
	// target, and whether any of them is tool-grounded (a deterministic tool
	// result, not a model judgment).
	SupportingSignals(ctx context.Context, target string) (count int, toolGrounded bool)
}

// Policy configures the cost-aware corroboration gate.
type Policy struct {
	// TrustTierAtOrAbove: a candidate whose source-model tier factor is at or
	// above this is trusted without any re-check (a frontier judgment). Default
	// 1.0 (only frontier is auto-trusted).
	TrustTierAtOrAbove float64
	// HighStakes reports whether a candidate is worth a paid verification when
	// uncorroborated. If nil, DefaultHighStakes is used. Only high-stakes
	// signals can ever trigger a strong-model call.
	HighStakes func(Candidate) bool
	// MaxVerifications caps paid verification calls per campaign so corroboration
	// can never erode the cost savings. 0 disables paid escalation entirely
	// (free corroboration + passive gate only).
	MaxVerifications int
	// MaxOracleProbes caps active oracle probes per campaign. Oracles probe over
	// the network (a listen window, a baseline+attack request pair), so they are
	// bounded like paid verifications are. 0 disables oracle probing entirely.
	MaxOracleProbes int
}

// DefaultHighStakes treats critical/high-severity judgments as worth verifying
// when uncorroborated — these are the ones that send the exploit agent spending.
func DefaultHighStakes(c Candidate) bool {
	return c.Severity == pipeline.SeverityCritical || c.Severity == pipeline.SeverityHigh
}

// Corroborator applies a Policy, counting paid verifications against the cap.
// Construct one per campaign so the budget persists across handoffs; guard it
// if shared across goroutines. Evidence is passed per call (the campaign's live
// blackboard) while the budget counter lives here.
type Corroborator struct {
	policy   Policy
	verifier Verifier // may be nil (no paid escalation)
	oracles  []Oracle // external ground-truth probes, tried before the verifier
	mu        sync.Mutex
	used      int // paid model verifications spent
	probed    int // active oracle probes spent
	proven    int // candidates an oracle proved
	disproven int // candidates an oracle refuted
}

// New builds a Corroborator. verifier may be nil to run free-corroboration-only.
func New(policy Policy, verifier Verifier) *Corroborator {
	if policy.TrustTierAtOrAbove <= 0 {
		policy.TrustTierAtOrAbove = 1.0
	}
	if policy.HighStakes == nil {
		policy.HighStakes = DefaultHighStakes
	}
	return &Corroborator{policy: policy, verifier: verifier}
}

// WithOracles registers external ground-truth oracles, consulted for a
// high-stakes uncorroborated candidate BEFORE the paid model verifier — a
// decisive oracle verdict is trusted over any model claim. Chainable off New;
// returns the same Corroborator. Safe to call once at construction.
func (c *Corroborator) WithOracles(oracles ...Oracle) *Corroborator {
	c.oracles = append(c.oracles, oracles...)
	return c
}

// Verifications returns how many paid verification calls have been spent.
func (c *Corroborator) Verifications() int {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.used
}

// OracleProbes returns how many active oracle probes have been spent.
func (c *Corroborator) OracleProbes() int {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.probed
}

// OracleOutcomes returns how many candidates oracles proved and refuted over
// the campaign — the external-ground-truth tally, independent of model claims.
func (c *Corroborator) OracleOutcomes() (proven, disproven int) {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.proven, c.disproven
}

// record tallies a decisive oracle verdict.
func (c *Corroborator) record(v OracleVerdict) {
	c.mu.Lock()
	defer c.mu.Unlock()
	switch v {
	case Proven:
		c.proven++
	case Disproven:
		c.disproven++
	}
}

// spend reserves one verification against the cap, reporting whether budget
// remained. Safe for concurrent callers (the classifier runs agents in parallel).
func (c *Corroborator) spend() bool {
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.used >= c.policy.MaxVerifications {
		return false
	}
	c.used++
	return true
}

// spendProbe reserves one oracle probe against its cap. Separate budget from
// paid verifications so external ground truth and model re-judgment are capped
// independently. Safe for concurrent callers.
func (c *Corroborator) spendProbe() bool {
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.probed >= c.policy.MaxOracleProbes {
		return false
	}
	c.probed++
	return true
}

// probeOracles consults each applicable oracle, in order, for a hard external
// verdict — and stops at the first decisive one (Proven or Disproven). Each
// probe costs one unit against MaxOracleProbes; when the cap is exhausted it
// stops trying. A probe error is treated as Inconclusive (an infrastructure
// failure must not confirm or condemn a finding). ok is false when no oracle
// was decisive, so the caller falls through to the model verifier.
func (c *Corroborator) probeOracles(ctx context.Context, cand Candidate) (v OracleVerdict, conf float64, receipt, name string, ok bool) {
	for _, o := range c.oracles {
		if !o.Applies(cand) {
			continue
		}
		if !c.spendProbe() {
			break // out of probe budget — fall through to the verifier
		}
		verdict, confidence, r, err := o.Probe(ctx, cand)
		if err != nil || !verdict.Decisive() {
			continue
		}
		return verdict, confidence, r, o.Name(), true
	}
	return Inconclusive, 0, "", "", false
}

// Proof records that an external oracle spoke decisively about a candidate — a
// receipt the downstream report and attack graph can trust over any model
// claim. The zero value (Verdict == Inconclusive) means no oracle proved or
// refuted the candidate.
type Proof struct {
	Oracle     string        // the oracle that spoke (e.g. "oob-callback")
	Verdict    OracleVerdict // Proven or Disproven
	Confidence float64       // the oracle's confidence in [0,1]
	Receipt    string        // short human-readable evidence
}

// Proven reports whether an oracle confirmed the candidate (vs. refuted it or
// stayed silent).
func (p Proof) Proven() bool { return p.Verdict == Proven }

// Decide corroborates one candidate against the campaign's evidence and returns
// the decision plus the weight to use if it is written (unchanged for Pass,
// raised for Promote, 0 for Reject). ev may be nil (skips free corroboration).
// It is DecideWithProof with the proof discarded.
func (c *Corroborator) Decide(ctx context.Context, cand Candidate, ev Evidence) (Decision, float64) {
	d, w, _ := c.DecideWithProof(ctx, cand, ev)
	return d, w
}

// DecideWithProof is Decide, additionally returning the oracle Proof when an
// external oracle was decisive (Proven → Promote, Disproven → Reject). For
// every non-oracle outcome the Proof is the zero value. Callers that surface a
// "VERIFIED" badge or set attack-graph edge exploitability use this form so the
// external verdict is trusted over the model's own judgment.
func (c *Corroborator) DecideWithProof(ctx context.Context, cand Candidate, ev Evidence) (Decision, float64, Proof) {
	// 1. Frontier-sourced judgment: trust as-is, no spend.
	if quality.TierFor(cand.Model).Factor >= c.policy.TrustTierAtOrAbove {
		return Pass, cand.Pheromone, Proof{}
	}

	// 2. Free corroboration: a tool result grounds it, or independent signals
	//    agree. Zero LLM cost — handles the common case.
	if ev != nil {
		if n, toolGrounded := ev.SupportingSignals(ctx, cand.Target); toolGrounded || n >= 2 {
			return Promote, promote(cand.Pheromone, 0.85), Proof{}
		}
	}

	// 3. Low-stakes: never worth paying to verify — leave it to the passive gate.
	if !c.policy.HighStakes(cand) {
		return Pass, cand.Pheromone, Proof{}
	}

	// 4. High-stakes + uncorroborated: prefer EXTERNAL ground truth over a paid
	//    model re-judgment. An oracle (OOB callback, differential, timing)
	//    derives a verdict from a signal outside any model, so a decisive oracle
	//    is trusted over the LLM verifier — the line between guessing and proof.
	if v, conf, receipt, name, ok := c.probeOracles(ctx, cand); ok {
		proof := Proof{Oracle: name, Verdict: v, Confidence: conf, Receipt: receipt}
		c.record(v)
		switch v {
		case Proven:
			return Promote, promote(cand.Pheromone, max(conf, provenFloor)), proof
		case Disproven:
			return Reject, 0, proof
		}
	}

	// 5. No decisive oracle: one paid model verification, if the per-campaign
	//    cap allows. Otherwise fall back to the passive gate.
	if c.verifier == nil || !c.spend() {
		return Pass, cand.Pheromone, Proof{}
	}
	real, conf, err := c.verifier.Verify(ctx, cand)
	if err != nil {
		// Verification failed (timeout, provider error) — don't punish the
		// candidate for our failure; let the passive gate decide.
		return Pass, cand.Pheromone, Proof{}
	}
	if !real {
		return Reject, 0, Proof{}
	}
	return Promote, promote(cand.Pheromone, conf), Proof{}
}

// promote raises a weight toward a corroborated floor without ever lowering it
// or exceeding 1.0.
func promote(cur, floor float64) float64 {
	if floor > cur {
		cur = floor
	}
	if cur > 1 {
		cur = 1
	}
	if cur < 0 {
		cur = 0
	}
	return cur
}
