// Package claim is the common unit for the many claimants that compete for one
// utterance (V-565, umbrella V-558, design in
// docs/plans/19-dialogue-arbitration.md).
//
// Maven's cascade has twenty-two stage-0 grammars, seven router intents,
// twenty-two query sources and seven stateful pre-emptors, and every one of them
// answers "is this mine?" alone. None can answer "is this more mine than
// yours?", because their scores are not comparable: stage 0 asserts 1.0 by
// fiat, the classifier reports a cosine, the LLM router derives one from
// structure. So list order is the whole arbitration.
//
// A Claim carries evidence rather than a verdict. Two things read that evidence
// and neither needs a float:
//
//   - specificity — a claim explaining more of the utterance is preferred, and
//     that is Consumed against Unexplained;
//   - negative constraint — a claimant may veto itself and say why, and that is
//     Veto.
//
// Where a number is unavoidable, it is an ordinal Band and not a probability.
// The band set is argued from measurement in the plan doc: the classifier's
// cosine is flat against correctness (62% at both ends of a spread 0.083 wide)
// and its top-two margin is p50 0.009, so no claimant Maven has today can
// produce a graded confidence.
//
// This package deliberately imports nothing from the rest of Maven.
// internal/dialogue must not import internal/router, so a shared unit that
// pulled in router.Intent would smuggle that edge back in. Intent is a plain
// string and the conversion happens at each edge.
package claim

import "strings"

// Band — the kind of evidence behind a claim, ordinal and comparable. Higher
// wins a tie. Four values, because four is what the claimants can report.
type Band int

const (
	// BandUnknown — the zero value. A claim that never set a band is a bug in
	// its builder, not a weak claim, so it must not silently rank as one.
	BandUnknown Band = iota

	// BandVetoed — the claimant will take the turn only if nobody else will,
	// and Veto says why it should not. The three arms of gateLLMDecision (a
	// fact with no key, an act with no allowlisted fn, a reminder with no
	// subject) land here. Still a claim: asking "о чём напомнить?" beats
	// silence.
	BandVetoed

	// BandNearest — the claim rests only on resemblance to something else,
	// with no anchor in the utterance and no structural check behind it. The
	// nearest-centroid classifier. One band and not a graded scale, because
	// the cosine measured flat against correctness.
	BandNearest

	// BandStructural — the claimant read the whole sentence and produced a
	// complete route, every slot its intent requires filled. The LLM router at
	// full confidence, and a stateful claimant holding a pending question.
	// Below BandAnchored on purpose: the stateful claimants pre-empt
	// unconditionally today, and that is the V-558 defect. There are seven of
	// them and preRouteLadder in cmd/mavend/decisiontrace.go is the roster.
	BandStructural

	// BandAnchored — a literal pattern anchored in the utterance matched, and
	// the matched span is what decides the intent. Stage 0 grammars and
	// query-source matchers. Certainty about the shape of the sentence, which
	// is not certainty about the answer.
	BandAnchored
)

// String — the band's name, for a trace line and for a test failure that has to
// say which band it got.
func (b Band) String() string {
	switch b {
	case BandVetoed:
		return "vetoed"
	case BandNearest:
		return "nearest"
	case BandStructural:
		return "structural"
	case BandAnchored:
		return "anchored"
	default:
		return "unknown"
	}
}

// Claim — one claimant's bid for one utterance.
type Claim struct {
	// Claimant — who wants the turn. A grammar name, a query source name, a
	// stage label. Read by the trace and by a test naming a loser.
	Claimant string

	// Intent — the route this claim would take. A plain string and not
	// router.Intent: see the package comment.
	Intent string

	// Filled — the slot names this claim would fill ("time", "fn", "key",
	// "text"). Names and not values, because arbitration compares shape.
	Filled []string

	// Consumed — the utterance tokens this claim explains, in the order they
	// appear. The numerator of specificity.
	Consumed []string

	// Unexplained — the tokens this claim does not explain, in order. Carried
	// rather than derived, so a claimant may decline a span it did match.
	Unexplained []string

	// Band — the kind of evidence. The tie-break, after coverage.
	Band Band

	// Veto — why this claim should NOT win, empty when there is none. A
	// non-empty Veto and a Band above BandVetoed is legal: a claim can be
	// well-evidenced and still name a reason to prefer somebody else.
	Veto string
}

// Coverage — the fraction of the utterance this claim explains, in [0,1]. A
// claim with no tokens either way covers nothing; it is not division by zero
// and it is not a full claim.
func (c Claim) Coverage() float64 {
	total := len(c.Consumed) + len(c.Unexplained)
	if total == 0 {
		return 0
	}
	return float64(len(c.Consumed)) / float64(total)
}

// Vetoed reports whether the claimant named a reason against itself.
func (c Claim) Vetoed() bool { return c.Veto != "" }

// MoreSpecificThan — the ordering V-560's arbiter will read. Coverage first,
// because that is what fixes the failure this program opened with: a pending
// reminder ate "какая сейчас погода в Риме?" as a time answer while explaining
// none of it. Band only breaks a coverage tie.
//
// Deliberately NOT wired into the cascade by V-565. It is here so the ordering
// is one function with tests on it, rather than a rule restated at each of the
// sites that will eventually call it.
func (c Claim) MoreSpecificThan(other Claim) bool {
	cc, oc := c.Coverage(), other.Coverage()
	if cc != oc {
		return cc > oc
	}
	return c.Band > other.Band
}

// Tokens — the utterance split for coverage accounting. Whitespace, then
// trailing and leading punctuation, then lowercased.
//
// This is tokenization over the raw string and not a Russian pattern: it
// contains no word list, and its output is a count rather than a fact or a
// route (CLAUDE.md § Russian patterns). Lowercasing is Unicode-aware, so
// Cyrillic folds the same way Latin does.
func Tokens(utterance string) []string {
	fields := strings.FieldsFunc(utterance, func(r rune) bool {
		return r == ' ' || r == '\t' || r == '\n' || r == '\r'
	})
	out := make([]string, 0, len(fields))
	for _, f := range fields {
		t := strings.Trim(strings.ToLower(f), ".,!?;:()\"'«»…-–—")
		if t == "" {
			continue
		}
		out = append(out, t)
	}
	return out
}

// Split partitions the utterance's tokens into the ones a claim explains and
// the rest, preserving order in both. A token is explained when it appears in
// one of the spans the claimant filled (a slot value, a matched substring).
//
// Duplicates are handled by membership and not by count: "напомни напомни
// позвонить" with span "напомни" explains both copies. The alternative is a
// multiset, and no claimant Maven has can say which copy it meant.
func Split(utterance string, spans ...string) (consumed, unexplained []string) {
	explained := map[string]bool{}
	for _, s := range spans {
		for _, t := range Tokens(s) {
			explained[t] = true
		}
	}
	for _, t := range Tokens(utterance) {
		if explained[t] {
			consumed = append(consumed, t)
		} else {
			unexplained = append(unexplained, t)
		}
	}
	return consumed, unexplained
}
