package store

import (
	"context"
	"encoding/json"
	"fmt"
	"time"
)

// RoutingTraceRetention is how long a raw trace lives (owner's call,
// 06-08-2026). A trace is read within a day or two of the turn that produced it,
// or never, so two weeks is diagnosis with room for a weekend. It is deliberately
// an age and not a row count: the useful question is "what did she do this week",
// and a busy Tuesday must not push last Friday out.
//
// A correction is not covered by this bound. The moment the owner corrects a
// turn, the pair is promoted out of the trace into a seed-shaped row and kept
// indefinitely, because a label is not a transcript. Keeping the transcript that
// carried it would defeat the point of the bound.
const RoutingTraceRetention = 14 * 24 * time.Hour

// RoutingTrace is one turn's arbitration, persisted. It is internal/decision's
// Record plus the four things the ring never had to carry: which reach the
// utterance arrived on, whether stage 0 answered before the classifier was
// consulted, which encoder body was live, and what the turn actually did.
type RoutingTrace struct {
	ID        int64     `json:"id"`
	Ts        time.Time `json:"ts"`
	Utterance string    `json:"utterance"`
	Source    string    `json:"source"`
	Winner    string    `json:"winner"`
	Intent    string    `json:"intent"`
	// ClaimedBeforeHead — stage 0 or a pre-route resolver answered, so the turn
	// teaches nothing about the classifier. It is a large share of real traffic,
	// and counting those turns as training signal would fit the head to the
	// grammars rather than to him.
	ClaimedBeforeHead bool `json:"claimed_before_head"`
	// EncoderID names the encoder body that was live. A fitted distance means
	// nothing under another body, and V-546 trains a copy of the weights.
	EncoderID string `json:"encoder_id"`
	// Outcome is what happened, not what was routed: a route that reached a gap
	// and a route that ran are different turns.
	Outcome string `json:"outcome"`
	// Correction is the owner's label, empty until he gives one (V-630).
	Correction string `json:"correction"`
	// Claims is internal/decision's per-claimant detail, stored as JSON because
	// nothing queries inside it: it is read whole, beside the turn it explains.
	Claims json.RawMessage `json:"claims"`
}

// WriteRoutingTrace appends one turn and drops the ones past retention.
func (s *Store) WriteRoutingTrace(ctx context.Context, tr RoutingTrace) (int64, error) {
	claims := "[]"
	if len(tr.Claims) > 0 {
		claims = string(tr.Claims)
	}
	res, err := s.db.ExecContext(ctx, `
		INSERT INTO routing_traces
		       (ts, utterance, source, winner, intent, claimed_before_head, encoder_id, outcome, correction, claims)
		VALUES (?,?,?,?,?,?,?,?,?,?)`,
		tr.Ts.UnixMilli(), tr.Utterance, tr.Source, tr.Winner, tr.Intent,
		tr.ClaimedBeforeHead, tr.EncoderID, tr.Outcome, tr.Correction, claims)
	if err != nil {
		return 0, fmt.Errorf("write routing trace: %w", err)
	}
	id, err := res.LastInsertId()
	if err != nil {
		return 0, fmt.Errorf("last insert id: %w", err)
	}
	// Prune rarely. Turns arrive at human rate, so the bound is a ceiling and
	// paying for a delete on every one of them buys nothing. 64 turns is hours.
	if id%64 == 0 {
		if err := s.PruneRoutingTraces(ctx, tr.Ts.Add(-RoutingTraceRetention)); err != nil {
			return id, err
		}
	}
	return id, nil
}

// PruneRoutingTraces deletes every trace older than before. A corrected turn is
// deleted with the rest: the label was promoted out when the owner wrote it, so
// what is left here is the transcript, and the transcript is what expires.
func (s *Store) PruneRoutingTraces(ctx context.Context, before time.Time) error {
	if _, err := s.db.ExecContext(ctx,
		`DELETE FROM routing_traces WHERE ts < ?`, before.UnixMilli()); err != nil {
		return fmt.Errorf("prune routing traces: %w", err)
	}
	return nil
}

// RecentRoutingTraces returns the newest n turns, newest first.
func (s *Store) RecentRoutingTraces(ctx context.Context, n int) ([]RoutingTrace, error) {
	rows, err := s.db.QueryContext(ctx, `
		SELECT id, ts, utterance, source, winner, intent, claimed_before_head,
		       encoder_id, outcome, correction, claims
		  FROM routing_traces
		 ORDER BY id DESC
		 LIMIT ?`, n)
	if err != nil {
		return nil, fmt.Errorf("recent routing traces: %w", err)
	}
	defer rows.Close()
	var out []RoutingTrace
	for rows.Next() {
		var tr RoutingTrace
		var tsMilli int64
		var claims string
		if err := rows.Scan(&tr.ID, &tsMilli, &tr.Utterance, &tr.Source, &tr.Winner,
			&tr.Intent, &tr.ClaimedBeforeHead, &tr.EncoderID, &tr.Outcome,
			&tr.Correction, &claims); err != nil {
			return nil, err
		}
		tr.Ts = time.UnixMilli(tsMilli).UTC()
		tr.Claims = json.RawMessage(claims)
		out = append(out, tr)
	}
	return out, rows.Err()
}
