package router

import (
	"context"
	"errors"
	"math"
	"sort"
)

// ErrNoIntents — the classifier has no seeded examples; cannot classify.
// The daemon seeds ~10 examples/intent at bootstrap (per spec). Until then
// every free-form utterance routes to clarify-or-ask, never to a guess.
var ErrNoIntents = errors.New("router: no intents seeded")

// Example — one labeled utterance + its embedding. The classifier is
// append-only: misroute correction = AddExample for the corrected intent
// (per spec — "append-only, grows the classifier as used. more reliable over
// time, no retrain"). The note stays as provenance; the router never silently
// rewires a label.
type Example struct {
	Text string
	Vec  []float32
}

// Result — one scored intent from Classify. Score is cosine similarity in
// [-1,1]; higher = closer to that intent's centroid.
type Result struct {
	Intent Intent
	Score  float64
}

// Classifier — nearest-centroid over labeled intents. One forward pass (the
// embed) yields a vector; cosine similarity to each intent's centroid (mean
// of its examples) gives a score; max wins. ~10 examples/intent is the spec's
// bootstrap target.
//
// Pure given the Embedder: the only I/O is the embed call itself. Centroid
// math is deterministic and unit-testable with a fake embedder. The cascade
// (router.go) applies the confidence threshold; the classifier just scores.
type Classifier struct {
	embedder  Embedder
	examples  map[Intent][]Example
	centroids map[Intent][]float32
	dim       int
}

func NewClassifier(e Embedder) *Classifier {
	return &Classifier{
		embedder:  e,
		examples:  make(map[Intent][]Example),
		centroids: make(map[Intent][]float32),
		dim:       e.Dim(),
	}
}

// AddExample — appends a labeled example and recomputes that intent's centroid.
// Misroute correction calls this with the corrected intent. Production path.
func (c *Classifier) AddExample(ctx context.Context, intent Intent, text string) error {
	vec, err := c.embedder.Embed(ctx, text)
	if err != nil {
		return err
	}
	c.addVec(intent, text, vec)
	return nil
}

// AddExampleVec — for tests that want to skip the embedder (inject vectors
// directly). Keeps the classifier pure under a fake embedder without re-running
// the hash. Not used by the production cascade.
func (c *Classifier) AddExampleVec(intent Intent, text string, vec []float32) {
	c.addVec(intent, text, vec)
}

func (c *Classifier) addVec(intent Intent, text string, vec []float32) {
	c.examples[intent] = append(c.examples[intent], Example{Text: text, Vec: vec})
	c.centroids[intent] = meanVec(c.examples[intent])
}

// Examples — read-only view of seeded examples per intent. Introspectable: the
// daemon surfaces "what maven has been taught" through an authed surface.
func (c *Classifier) Examples(intent Intent) []Example {
	return c.examples[intent]
}

// Intents — the set of intents with at least one seeded example.
func (c *Classifier) Intents() []Intent {
	out := make([]Intent, 0, len(c.centroids))
	for k := range c.centroids {
		out = append(out, k)
	}
	sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
	return out
}

// Classify — embeds the utterance and returns all intents scored by cosine
// similarity to their centroid, sorted best-first (ties broken by intent asc
// for determinism). The caller applies the confidence threshold (stage 3).
// Returns ErrNoIntents if no examples have been seeded.
func (c *Classifier) Classify(ctx context.Context, utterance string) ([]Result, error) {
	if len(c.centroids) == 0 {
		return nil, ErrNoIntents
	}
	vec, err := c.embedder.Embed(ctx, utterance)
	if err != nil {
		return nil, err
	}
	results := make([]Result, 0, len(c.centroids))
	for intent, centroid := range c.centroids {
		results = append(results, Result{Intent: intent, Score: cosine(vec, centroid)})
	}
	sort.Slice(results, func(i, j int) bool {
		if results[i].Score != results[j].Score {
			return results[i].Score > results[j].Score
		}
		return results[i].Intent < results[j].Intent
	})
	return results, nil
}

// meanVec — L2-normalized mean of a set of example vectors. Normalizing the
// centroid keeps cosine = dot product against normalized query vectors, and
// stops high-example-count intents from dominating purely by magnitude.
func meanVec(exs []Example) []float32 {
	if len(exs) == 0 {
		return nil
	}
	m := make([]float32, len(exs[0].Vec))
	for _, e := range exs {
		for i, x := range e.Vec {
			m[i] += x
		}
	}
	var sum float64
	for i := range m {
		m[i] /= float32(len(exs))
		sum += float64(m[i]) * float64(m[i])
	}
	if sum == 0 {
		return m
	}
	inv := float32(1.0 / math.Sqrt(sum))
	for i := range m {
		m[i] *= inv
	}
	return m
}

// cosine — both inputs are L2-normalized ⇒ dot product == cosine similarity.
// Mismatched/empty dimensions return 0 (no signal), which the threshold gate
// turns into clarify — never into a confident wrong write.
func cosine(a, b []float32) float64 {
	if len(a) != len(b) || len(a) == 0 {
		return 0
	}
	var dot float64
	for i := range a {
		dot += float64(a[i]) * float64(b[i])
	}
	return dot
}
