// Package router is maven's reactive path — the cascade that turns a free-form
// utterance into a deterministic Decision.
//
// Spec contract (from docs/design.md § Reactive path — routing):
//
//   - the TARGET design is LLM-as-router: the resident model (Qwen3-1.7B)
//     emits GBNF-constrained structured JSON for the route, and the same
//     model phrases replies; the embedder is a RAG hint, not a routing gate.
//     the classifier/embedder cascade below is the committed default today,
//     but it is an interim stopgap (docs/design.md § Superseded, "classifier-owns-
//     the-route") and the known cause of weak RU query handling — not a
//     design to extend.
//   - a CASCADE, not one decider — layers:
//     stage 0 — exact match (regex/grammar). wake-word + known command
//     grammar. "maven, restart nginx" hits the allowlist directly,
//     skips the classifier. lowest latency — the client-side wake-word/
//     command-grammar path (cmd/mavwaked).
//     stage 1 — route decision. the resident model (target), else the
//     nearest-centroid classifier over embedded labeled intents (today's
//     stopgap: one forward pass, ~30ms cpu, similarity score). any LLM
//     error falls through to the classifier so a turn never breaks.
//     stage 2 — slot extraction, per intent. classification gives *what kind*,
//     not *the args*. reminders need a datetime, acts need fn+params.
//     stage 3 — confidence gate. below threshold → clarify, don't guess.
//     same pattern as since(key)==null → don't fire: a misrouted
//     fact is a confident wrong write — worse than a gap.
//   - save-where is the routing axis: act | reminder | fact | note | query.
//   - misroute correction = new centroid example — append-only, grows the
//     classifier as used. same shape as nudges.outcome tuning cooldowns.
//
// The Embedder is the one impure seam (the ONNX int8 model is a later module).
// Given a deterministic embedder, the classifier + cascade are pure and unit-
// testable with zero infra — same instinct as the loop's gather/pure split.
package router

import "time"

// Intent — the seven save-where labels from docs/design.md's routing table. The
// discriminator is "does the loop evaluate a predicate against it?":
//
//   - act:      command now, not stored (function call into the allowlist)
//   - reminder: has a fire-time → reminders table (sqlite). bypasses the gate
//   - fact:     structured state the loop reasons over → facts (sqlite)
//   - note:     recall/preference, no predicate touches it → semantic store
//   - query:    answer, don't store → the resident model reads sqlite or the
//     semantic store (RAG)
//   - chat:     conversational, no store side-effect — LLM replies from
//     dialogue history + general knowledge
//
// fact-vs-note is the whole line: predicate will read it → structured facts
// row; just "recall when relevant" → semantic store. reminder splits off by
// future timestamp; act splits off by being imperative-now.
type Intent string

const (
	IntentAct      Intent = "act"
	IntentReminder Intent = "reminder"
	IntentFact     Intent = "fact"
	IntentNote     Intent = "note"
	IntentQuery    Intent = "query"
	IntentChat     Intent = "chat"
	IntentSystem   Intent = "system"
)

// Slots — per-intent extracted arguments (stage 2). Not every field is set for
// every intent; the Intent decides which matter. A slot that doesn't parse
// leaves its Has* flag false — the daemon's SLM last-resort lane picks it up
// for free-form notes the parsers choke on. Never an error to be missing.
type Slots struct {
	// Reminder: absolute fire time. The router resolves relative→absolute AT
	// CAPTURE ("in 4h" → now+4h, never the string) per spec — the reminders
	// table stores only absolute fire_ts. HasTime=false ⇒ no datetime parsed.
	Time    time.Time
	HasTime bool

	// Act: the function name from the allowlist + positional args. The router
	// fuzzy-matches the verb against the allowlist; not on the list → refuse,
	// don't improvise. Fn empty ⇒ no allowlist match. Destructive acts still
	// gate behind confirm at the daemon layer, not here.
	Fn    string
	Args  []string
	HasFn bool

	// Fact: structured (key,value) the loop will evaluate predicates against.
	// "drank water" → key=water; "slept 6h" → key=sleep, value=6h. The value
	// is the raw string the daemon json-encodes before WriteFact.
	Key    string
	Value  string
	HasKey bool

	// Note/Query: free-form payload (chroma-bound for note, RAG input for query).
	// Always set to the utterance for those intents.
	Text string
}

// Decision — the router's output. The cascade is deterministic: stage 0 wins
// outright; otherwise classify (1) → extract (2) → gate (3). Clarify=true ⇒
// the daemon asks instead of guessing — "shuts up when uncertain" for routing,
// same shape as since(key)==null → don't fire for the loop.
type Decision struct {
	Utterance  string
	Stage      int // 0 exact-match, 1 classified, 2 slots-extracted, 3 clarify-gated
	Intent     Intent
	Confidence float64 // 1.0 for stage-0; classifier cosine similarity for 1+
	Slots      Slots
	Clarify    bool // stage 3: below threshold — ask, don't guess

	// Source — where the answer lives, for a query. The second half of the
	// route, and empty on every other intent. SourceUnknown means no decider
	// named one and the daemon walks its whole chain, which is what shipped
	// before this field existed. See source.go for why it is twelve values.
	Source Source

	// SourceAnchored — a stage 0 grammar named that destination, matching a
	// literal pattern to do it. Only the router sets this, and only there.
	//
	// It exists because one thing downstream is not reversible by evidence
	// (V-666). Naming a destination normally takes guessing sources off a turn,
	// and one of those is the personal boundary, which is what stops a question
	// about him from reaching the world. A grammar that read "что такое X" may
	// take it off. A model or a softmax may not, because a wrong destination
	// there widens what leaves the box rather than costing an answer.
	//
	// Read Stage instead and the two decisions get coupled: stage 0 also means
	// confidence 1.0 and an anchored claim band, and a later cascade change
	// could make one true where the other is not.
	SourceAnchored bool

	// Continued — this decision was rebuilt from the previous turn rather
	// than routed, because the utterance was an ellipsis ("а завтра?").
	// Handlers use it to know that Slots.Text is the PREVIOUS turn's topic
	// and not something the current utterance said. Nothing in the router
	// sets it; the daemon's continuation path does.
	Continued bool
}
