package router

import "github.com/kami/maven/internal/claim"

// ClaimOf — build a claim.Claim from a Decision (V-565, design in
// docs/plans/19-dialogue-arbitration.md).
//
// Additive and beside the existing path. Decision.Confidence keeps its float
// and keeps working: r.threshold and gateLLMDecision read it, and the
// classifier cascade is the failure floor. Nothing in Route calls this yet.
// The arbiter that reads claims is V-560.
//
// claimant names who produced the decision. The cascade does not record which
// stage-0 grammar matched, so the caller passes what it knows and the builder
// does not guess.
func ClaimOf(claimant string, d Decision) claim.Claim {
	consumed, unexplained := claim.Split(d.Utterance, claimSpans(d)...)
	return claim.Claim{
		Claimant:    claimant,
		Intent:      string(d.Intent),
		Filled:      filledSlots(d.Slots),
		Consumed:    consumed,
		Unexplained: unexplained,
		Band:        bandOf(d),
		Veto:        vetoOf(d),
	}
}

// claimSpans — the parts of the utterance the decision says it read. Slot
// values, not the utterance, because coverage is the question of how much of
// the sentence the claim actually explains.
//
// A stage-0 grammar reports whatever its Build put in the slots, which for the
// reminder rule is the text after "напомни" and not the verb itself. That
// under-reports coverage rather than over-reporting it, which is the safe
// direction: a claim that overstates what it explains wins arbitrations it
// should lose.
func claimSpans(d Decision) []string {
	spans := []string{d.Slots.Text, d.Slots.Key, d.Slots.Value, d.Slots.Fn}
	return append(spans, d.Slots.Args...)
}

// filledSlots — the slot names this decision would fill. Text counts only when
// it differs from the whole utterance: fillSlots backfills the raw utterance
// into Text for a note, a query and a chat turn, so a set Text is not by itself
// evidence that anything was extracted.
func filledSlots(s Slots) []string {
	var out []string
	if s.HasTime {
		out = append(out, "time")
	}
	if s.HasFn {
		out = append(out, "fn")
	}
	if s.HasKey {
		out = append(out, "key")
	}
	if s.Text != "" {
		out = append(out, "text")
	}
	return out
}

// bandOf — which kind of evidence this decision rests on.
//
// Stage 0 is anchored: a literal pattern matched and its span decided the
// intent. The LLM path (stage 1) is structural: the model read the whole
// sentence, and gateLLMDecision already checked the route for structural
// holes. The classifier (stages 2 and 3) is nearest, and the measurement is why
// it is one band rather than a scale — on the 91-case RU fixture its cosine
// spans 0.859 to 0.942 and scores 62% at both ends.
//
// A decision carrying a veto lands in BandVetoed regardless of who produced it.
// That is the point of the band: a self-vetoed claim should lose to any claim
// that is not, whatever machinery built it.
func bandOf(d Decision) claim.Band {
	if vetoOf(d) != "" {
		return claim.BandVetoed
	}
	switch d.Stage {
	case 0:
		return claim.BandAnchored
	case 1:
		return claim.BandStructural
	default:
		return claim.BandNearest
	}
}

// vetoOf — why this decision should not win, recovered as a reason rather than
// a number.
//
// gateLLMDecision flattens three named structural holes into
// llmThinConfidence, and the reason is lost at that point: 0.3 tells a reader
// that something was wrong and never which thing. The same three conditions are
// checked here so the claim carries the sentence a trace can print and the
// owner can be told.
//
// Clarify is checked last and is the general case. A decision below threshold
// has already asked to be doubted, whichever path set it.
func vetoOf(d Decision) string {
	switch {
	case d.Intent == IntentFact && !d.Slots.HasKey:
		return "fact with no key: nothing to write, or a confident write under the wrong key"
	case d.Intent == IntentAct && !d.Slots.HasFn:
		return "act with no allowlisted fn: running an unlisted command or silently doing nothing"
	case d.Intent == IntentReminder && !reminderHasSubject(d.Slots.Text):
		return "reminder with no subject: it would fire empty at the hour"
	case d.Clarify:
		return "below the confidence gate"
	}
	return ""
}
