// router/decisiontrace.go — what the cascade tells the per-turn decision record.
//
// The cascade's arbitration is order (V-558): the first grammar whose Build
// agrees wins, and the model and the classifier are only reached because nobody
// upstream did. None of that is visible afterwards, so V-564 has each stage say
// its piece into the record riding the context. Nothing here reads the record
// back and nothing here can change a route — a nil recorder is the normal case
// in the fixture runner and every router test.
package router

import (
	"context"

	"github.com/kami/maven/internal/decision"
)

// The three routing engines, named as claimants. The model and the classifier
// are one stage and not two, because only one of them ever runs: the classifier
// is reached when the model is absent or errored, never alongside it. The heads
// run before both and decline on low confidence, so they can appear beside
// either one in a record.
const (
	claimantLLM        = "llm-router"
	claimantClassifier = "classifier"
	claimantHeads      = "routing-heads"
)

// thinReason names which arm of gateLLMDecision cut the confidence. The gate
// has three structural holes and they are three different defects, so "thinned"
// alone is not enough to act on.
func thinReason(d *Decision) string {
	switch {
	case d.Intent == IntentFact && !d.Slots.HasKey:
		return "a fact with no key even after the parser tried"
	case d.Intent == IntentAct && !d.Slots.HasFn:
		return "an act that never resolved to an allowlisted fn"
	case d.Intent == IntentReminder && !reminderHasSubject(d.Slots.Text):
		return "a reminder with no subject to say at the hour"
	default:
		return "below the clarify threshold"
	}
}

// Reasons a stage-0 grammar did not take a turn. Kept apart because they are
// different defects: a pattern that never matched is a rule that does not know
// the shape, a Build that declined is a rule that knew the shape and refused
// the content (narrative-query and the wakeword acts do this by design), and a
// grammar after the winner was never consulted at all.
const (
	reasonNoMatch      = "pattern did not match"
	reasonBuildDeclmn  = "matched the shape, Build declined the content"
	reasonEarlierClaim = "an earlier grammar claimed the turn"
)

// noteGrammarOutcomes records the stage-0 pass. examined is how many grammars
// were reached; declined holds the names whose Build said no; won is the winner
// or empty. Everything past the winner is named as never asked, because that
// silence is the thing the hardcoded order hides.
func (r *Router) noteGrammarOutcomes(ctx context.Context, examined int, declined map[int]bool, won string, intent Intent) {
	rec := decision.From(ctx)
	if rec == nil {
		return
	}
	for i, g := range r.grammars {
		switch {
		case i >= examined:
			rec.Note(decision.Claim{
				Stage: decision.StageZero, Claimant: g.Name,
				Outcome: decision.NeverAsked, Reason: reasonEarlierClaim,
			})
		case g.Name == won:
			rec.Note(decision.Scored(decision.StageZero, g.Name, string(intent), 1.0,
				decision.Won, ""))
		case declined[i]:
			rec.Note(decision.Claim{
				Stage: decision.StageZero, Claimant: g.Name,
				Outcome: decision.Declined, Reason: reasonBuildDeclmn,
			})
		default:
			rec.Note(decision.Claim{
				Stage: decision.StageZero, Claimant: g.Name,
				Outcome: decision.Declined, Reason: reasonNoMatch,
			})
		}
	}
}
