// Package modes holds the routing mode inventory: the roughly thirty distinct
// downstream behaviours mavend has, each mapped back to one of the seven public
// intents (V-631, umbrella V-628).
//
// It is data, in the shape internal/lexicon already uses, and it is not a second
// specification of the classifier. Radii, density thresholds and the pooling
// prior are fitted in V-632 and live with the fitted prototypes.
//
// Two rules decide whether something is a mode. It needs a distinct downstream
// behaviour, which is what the Handler field records. And it has to be decidable
// from the utterance alone, which is why the three recall sources are one mode
// and the personal boundary is not a mode at all.
package modes

import (
	"embed"
	"encoding/json"
	"fmt"
)

//go:embed modes_v1.json
var files embed.FS

// Mode is one routing class.
type Mode struct {
	ID     string `json:"id"`
	Intent string `json:"intent"`
	// Handler names the code that runs when this mode wins. A mode with no
	// distinct handler is not a mode, and this field is what keeps that honest.
	Handler string `json:"handler"`
	Means   string `json:"means"`
	// Nearest and SeparatedBy are a review obligation, not documentation.
	// Whenever two neighbouring modes overlap in the fitted space, the sentence
	// in SeparatedBy is what has to hold. If nothing separates them, they were
	// one mode and this file is wrong.
	Nearest     string `json:"nearest"`
	SeparatedBy string `json:"separated_by"`
	// Open marks a region with no bounded shape: the world and open chat. Those
	// carry RejectPolicy, and nothing else may.
	Open            bool     `json:"open"`
	RejectPolicy    string   `json:"reject_policy,omitempty"`
	PrototypeCount  int      `json:"prototype_count"`
	MinSeedExamples int      `json:"min_seed_examples"`
	Note            string   `json:"note,omitempty"`
	Examples        []string `json:"examples"`
}

// Inventory is the whole file. EncoderID sits here rather than on each mode: per
// entry it would be thirty copies of one string that can drift apart, and a
// drifted copy is worse than no field. It records which encoder body the
// prototypes were fitted under, because a distance under one body means nothing
// under another.
type Inventory struct {
	Version   int    `json:"version"`
	EncoderID string `json:"encoder_id"`
	Note      string `json:"note"`
	Modes     []Mode `json:"modes"`
}

// Intents — the seven public labels. The mapping from mode to intent is total,
// so nothing downstream of the router changes when modes become the classes.
var Intents = []string{"fact", "reminder", "note", "query", "act", "chat", "system"}

// Load reads the embedded inventory.
func Load() (*Inventory, error) {
	b, err := files.ReadFile("modes_v1.json")
	if err != nil {
		return nil, fmt.Errorf("modes: read: %w", err)
	}
	var inv Inventory
	if err := json.Unmarshal(b, &inv); err != nil {
		return nil, fmt.Errorf("modes: parse: %w", err)
	}
	return &inv, nil
}

// ByID indexes the inventory.
func (inv *Inventory) ByID() map[string]Mode {
	out := make(map[string]Mode, len(inv.Modes))
	for _, m := range inv.Modes {
		out[m.ID] = m
	}
	return out
}

// Fittable reports whether the mode has enough real seed examples to fit
// prototypes from. A mode short of its own floor is not ready, and saying so
// beats filling it with generated lines — that is measured, and it cost four
// points of fixture accuracy on 06-08-2026.
func (m Mode) Fittable() bool { return len(m.Examples) >= m.MinSeedExamples }
