// Package decision records who claimed one turn and who lost it (V-564).
//
// Arbitration between the claimants on the utterance stream is order, hardcoded
// in the resolver ladder, in buildRouter and in querySources (V-558). Order is
// invisible in a log: the daemon says which intent won and which query source
// answered, never who else wanted the turn, with what score, or why it did not
// get it. The Rome misroute took a probe, a log read and a code read to explain,
// which is one diagnosis too many for a defect family already three deep.
//
// The record rides the context, the same seam querysource.go uses and for the
// same reason: a turn answers through one string that the mic, telegram and the
// web all share, so a second return value is not threadable. A context with no
// recorder notes nothing, so every Note here is free in a test or a tool that
// did not ask for one.
//
// The most important thing it holds is not a loss but a silence. A claimant
// that was NEVER ASKED — because something earlier in the ladder returned
// first — looks identical to one that examined the turn and declined, and it is
// that confusion the hardcoded ordering hides. So a stage declares its roster
// up front and Finish names everyone who never reported.
package decision

import (
	"context"
	"sync"
	"time"
)

// Stages, in the order a turn passes through them.
const (
	StagePreRoute = "pre-route" // clarify, confirm, repair and their siblings
	StageZero     = "stage0"    // grammar rules
	StageRoute    = "route"     // LLM router, classifier
	StageMerge    = "merge"     // the follow-up merge, which edits rather than claims
	StageQuery    = "query"     // the query source chain
	StageAction   = "action"    // whoever actually produced the reply
)

// Outcomes. Coarse on purpose: the record answers "who wanted this turn and
// what happened to their claim", not "re-derive the branch".
const (
	Won         = "won"           // this claimant produced the turn
	Declined    = "declined"      // it looked at the turn and said not mine
	LostOnOrder = "lost_on_order" // it wanted the turn, something earlier had it
	LostOnScore = "lost_on_score" // it was scored against a rival and scored lower
	Thinned     = "thinned"       // it claimed, and a gate cut its confidence
	Merged      = "merged"        // it changed the winning claim without owning it
	NeverAsked  = "never_asked"   // it never got to look at all
)

// Claim is one claimant's say on one turn.
type Claim struct {
	Stage    string  `json:"stage"`
	Claimant string  `json:"claimant"`
	Intent   string  `json:"intent,omitempty"` // what it would have made the turn
	Score    float64 `json:"score,omitempty"`  // only meaningful with HasScore
	HasScore bool    `json:"has_score,omitempty"`
	Outcome  string  `json:"outcome"`
	Reason   string  `json:"reason,omitempty"` // why it lost, in its own terms
}

// Record is one turn's arbitration. Utterance is held because a record with no
// utterance is unreadable, and this store is diagnostics with a short life —
// unlike the facts table, which is the audit trail.
type Record struct {
	Ts        time.Time `json:"ts"`
	Utterance string    `json:"utterance"`
	Winner    string    `json:"winner"`
	Claims    []Claim   `json:"claims"`

	mu      sync.Mutex
	rosters []roster
}

type roster struct {
	stage string
	names []string
}

// Expect declares the claimants a stage could have asked, so Finish can tell a
// decline from a silence. The slice is held, not copied: every caller passes a
// package-level table.
func (r *Record) Expect(stage string, names []string) {
	if r == nil {
		return
	}
	r.mu.Lock()
	defer r.mu.Unlock()
	r.rosters = append(r.rosters, roster{stage: stage, names: names})
}

// Note appends one claim. The winner is whoever noted Won last, which is the
// claimant that actually returned the reply.
func (r *Record) Note(c Claim) {
	if r == nil {
		return
	}
	r.mu.Lock()
	defer r.mu.Unlock()
	r.Claims = append(r.Claims, c)
	if c.Outcome == Won {
		r.Winner = c.Stage + ":" + c.Claimant
	}
}

// NoteIfUnclaimed records a win only when nobody has claimed the turn yet. It
// is what closes a record whose route was decided but whose reply came from
// somewhere with no scoreboard — a clarify question, or an action handler with
// no chain in front of it. Without it a thinned route leaves the record with no
// winner at all, which reads as a lost turn rather than an asked question.
func (r *Record) NoteIfUnclaimed(c Claim) {
	if r == nil {
		return
	}
	r.mu.Lock()
	claimed := r.Winner != ""
	r.mu.Unlock()
	if claimed {
		return
	}
	c.Outcome = Won
	r.Note(c)
}

// Finish fills in the never-asked claimants and returns the record. Called once
// by whoever installed the recorder, after the turn has answered.
func (r *Record) Finish(now time.Time) *Record {
	if r == nil {
		return nil
	}
	r.mu.Lock()
	defer r.mu.Unlock()
	r.Ts = now
	reported := map[string]bool{}
	for _, c := range r.Claims {
		reported[c.Stage+":"+c.Claimant] = true
	}
	for _, ros := range r.rosters {
		for _, name := range ros.names {
			if !reported[ros.stage+":"+name] {
				r.Claims = append(r.Claims, Claim{
					Stage: ros.stage, Claimant: name, Outcome: NeverAsked,
				})
			}
		}
	}
	return r
}

// --- the context seam ---

type recorderKey struct{}

// With returns a context carrying a fresh record, and the record to read after
// the turn has answered.
func With(ctx context.Context, utterance string) (context.Context, *Record) {
	rec := &Record{Utterance: utterance}
	return context.WithValue(ctx, recorderKey{}, rec), rec
}

// From returns the record on the context, or nil. Every method on *Record is
// nil-safe, so a caller does not have to check.
func From(ctx context.Context) *Record {
	rec, _ := ctx.Value(recorderKey{}).(*Record)
	return rec
}

// Note is the shorthand every claim site uses: a no-op when nobody is recording.
func Note(ctx context.Context, c Claim) {
	From(ctx).Note(c)
}

// Expect is the roster shorthand, likewise a no-op with no recorder.
func Expect(ctx context.Context, stage string, names []string) {
	From(ctx).Expect(stage, names)
}

// Scored is a claim carrying a confidence, kept as a constructor so a caller
// cannot forget HasScore and have a real 0.0 read as "no score".
func Scored(stage, claimant, intent string, score float64, outcome, reason string) Claim {
	return Claim{
		Stage: stage, Claimant: claimant, Intent: intent,
		Score: score, HasScore: true, Outcome: outcome, Reason: reason,
	}
}
