package store

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"strings"
	"time"
)

// ErrNoSuchTrace — the trace the correction names is gone or never existed.
// Held apart from a write failure because it is the expected outcome of
// correcting a turn older than the 14-day bound, and the surface should say that
// rather than report a broken database.
var ErrNoSuchTrace = errors.New("no such routing trace")

// RoutingLabel is one correction: what he said, what she made of it, and what it
// should have been. It is the only supervised signal in the box, so it outlives
// the trace it came from (V-630, docs/plans/22-correcting-a-turn.md).
type RoutingLabel struct {
	ID        int64     `json:"id"`
	Ts        time.Time `json:"ts"`
	Utterance string    `json:"utterance"`
	// Was is the intent the cascade chose. Kept beside the target because the
	// pair is what names the confusion, and a label with no "was" cannot say
	// which boundary moved.
	Was string `json:"was"`
	// ShouldBe is the owner's target, and may be empty. "That was wrong, I am
	// not going to tell you what it was" is a usable negative, and requiring the
	// target would cost the cheap half of the gesture.
	ShouldBe  string `json:"should_be"`
	Source    string `json:"source"`
	EncoderID string `json:"encoder_id"`
}

// CorrectTurn records the owner's correction of one persisted turn. It promotes
// the pair into routing_labels and stamps the trace, both in one transaction:
// a stamped trace with no label would lose the signal when the trace expires,
// and a label with no stamp would let the same turn be corrected twice.
//
// shouldBe empty is allowed and means "wrong, target unstated".
func (s *Store) CorrectTurn(ctx context.Context, traceID int64, shouldBe string, now time.Time) error {
	tx, err := s.db.BeginTx(ctx, nil)
	if err != nil {
		return fmt.Errorf("correct turn: begin: %w", err)
	}
	defer func() { _ = tx.Rollback() }()

	var utterance, was, source, encoderID string
	err = tx.QueryRowContext(ctx, `
		SELECT utterance, intent, source, encoder_id FROM routing_traces WHERE id = ?`,
		traceID).Scan(&utterance, &was, &source, &encoderID)
	if errors.Is(err, sql.ErrNoRows) {
		return ErrNoSuchTrace
	}
	if err != nil {
		return fmt.Errorf("correct turn: read trace: %w", err)
	}

	shouldBe = strings.TrimSpace(shouldBe)
	if _, err := tx.ExecContext(ctx, `
		INSERT INTO routing_labels (ts, utterance, was, should_be, source, encoder_id)
		VALUES (?,?,?,?,?,?)
		ON CONFLICT(utterance) DO UPDATE SET
			ts = excluded.ts, was = excluded.was, should_be = excluded.should_be,
			source = excluded.source, encoder_id = excluded.encoder_id`,
		now.UnixMilli(), utterance, was, shouldBe, source, encoderID); err != nil {
		return fmt.Errorf("correct turn: write label: %w", err)
	}
	// The stamp is what the trace itself carries: "corrected", or the target he
	// gave. It expires with the trace, and that is fine — the label above is the
	// durable half.
	stamp := shouldBe
	if stamp == "" {
		stamp = "wrong"
	}
	if _, err := tx.ExecContext(ctx,
		`UPDATE routing_traces SET correction = ? WHERE id = ?`, stamp, traceID); err != nil {
		return fmt.Errorf("correct turn: stamp trace: %w", err)
	}
	if err := tx.Commit(); err != nil {
		return fmt.Errorf("correct turn: commit: %w", err)
	}
	return nil
}

// RoutingLabels returns the newest n corrections, newest first. Nothing prunes
// them: 31 modes and 9 of them with no example at all is the problem this table
// exists to solve, and a label is a few dozen bytes.
func (s *Store) RoutingLabels(ctx context.Context, n int) ([]RoutingLabel, error) {
	rows, err := s.db.QueryContext(ctx, `
		SELECT id, ts, utterance, was, should_be, source, encoder_id
		  FROM routing_labels ORDER BY id DESC LIMIT ?`, n)
	if err != nil {
		return nil, fmt.Errorf("routing labels: %w", err)
	}
	defer rows.Close()
	var out []RoutingLabel
	for rows.Next() {
		var l RoutingLabel
		var tsMilli int64
		if err := rows.Scan(&l.ID, &tsMilli, &l.Utterance, &l.Was, &l.ShouldBe,
			&l.Source, &l.EncoderID); err != nil {
			return nil, err
		}
		l.Ts = time.UnixMilli(tsMilli).UTC()
		out = append(out, l)
	}
	return out, rows.Err()
}
