package router

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

// Decision row written to the in-memory store. Mirrors the PIIEvent
// shape so the admin page can render the two side-by-side. Note:
// Prompt is NEVER stored — admins audit by Hash if they need to
// dedupe recurring routing patterns.
type DecisionRecord struct {
	ID            string        `json:"id"`
	CorrelationID string        `json:"correlation_id"`
	UserID        string        `json:"user_id"`
	RouterModel   string        `json:"router_model"`   // The smart-router model name the client asked for.
	RequestedModel string       `json:"requested_model"`// Same as RouterModel for now; reserved for chained routers.
	ServedModel   string        `json:"served_model"`   // The candidate the classifier picked.
	Classifier    string        `json:"classifier"`     // Classifier.Name(), e.g. "score".
	Label         string        `json:"label"`
	Score         float64       `json:"score"`
	LatencyMs     int64         `json:"latency_ms"`
	Cached        bool          `json:"cached"`         // True when the decision came from the L2 embedding cache.
	CacheSimilarity float64     `json:"cache_similarity,omitempty"` // Cosine similarity of the cache hit, 0 when not cached.
	// LabelScores carries the full per-label score distribution so the
	// admin UI can show how close inactive labels got to the activation
	// threshold. Empty on cache hits (only the final label set is cached).
	LabelScores         []LabelScore `json:"label_scores,omitempty"`
	ActivationThreshold float64      `json:"activation_threshold,omitempty"`
	// Source groups decisions by the entry point that produced them so
	// the admin page can split realtime / chat / anthropic streams. Empty
	// string is treated as "chat" for backward compatibility with rows
	// written before the field existed.
	Source        string        `json:"source,omitempty"`
	CreatedAt     time.Time     `json:"created_at"`
}

// Source values for DecisionRecord.Source. Kept as constants so callers
// don't drift on capitalisation.
const (
	SourceChat      = "chat"
	SourceAnthropic = "anthropic"
	SourceRealtime  = "realtime"
)

// DecisionStore persists routing decisions for the admin page and
// future drift checks. In-process by default so a no-auth box still
// gets a decision log; a future GORM impl can reuse the auth DB.
type DecisionStore interface {
	Record(ctx context.Context, r DecisionRecord) error
	List(ctx context.Context, q DecisionListQuery) ([]DecisionRecord, error)
	Count(ctx context.Context) (int, error)
	Close() error
}

// DecisionListQuery filters the decision log. Empty fields match all.
// Limit ≤ 0 picks a default cap.
type DecisionListQuery struct {
	CorrelationID string
	UserID        string
	RouterModel   string
	Source        string
	Limit         int
}

// NewMemoryDecisionStore returns a ring-buffer DecisionStore. capacity
// ≤ 0 picks 5_000 — same order of magnitude as PIIEvents but smaller
// because routing decisions correlate one-to-one with usage records;
// the existing UsageRecord log carries the bulk.
func NewMemoryDecisionStore(capacity int) DecisionStore {
	if capacity <= 0 {
		capacity = 5_000
	}
	return &memoryDecisionStore{
		ring: make([]DecisionRecord, capacity),
		cap:  capacity,
	}
}

type memoryDecisionStore struct {
	mu     sync.RWMutex
	ring   []DecisionRecord
	cap    int
	cursor int
	full   bool
}

func (s *memoryDecisionStore) Record(_ context.Context, r DecisionRecord) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.ring[s.cursor] = r
	s.cursor++
	if s.cursor == s.cap {
		s.cursor = 0
		s.full = true
	}
	return nil
}

func (s *memoryDecisionStore) List(_ context.Context, q DecisionListQuery) ([]DecisionRecord, error) {
	limit := q.Limit
	if limit <= 0 {
		limit = 1000
	}
	s.mu.RLock()
	defer s.mu.RUnlock()
	out := make([]DecisionRecord, 0, limit)
	scan := func(r DecisionRecord) bool {
		if r.ID == "" {
			return false
		}
		if q.CorrelationID != "" && r.CorrelationID != q.CorrelationID {
			return false
		}
		if q.UserID != "" && r.UserID != q.UserID {
			return false
		}
		if q.RouterModel != "" && r.RouterModel != q.RouterModel {
			return false
		}
		if q.Source != "" {
			// Empty source on the row is treated as SourceChat for back-
			// compat with rows written before the field existed.
			rowSource := r.Source
			if rowSource == "" {
				rowSource = SourceChat
			}
			if rowSource != q.Source {
				return false
			}
		}
		out = append(out, r)
		return len(out) >= limit
	}
	if s.full {
		for i := s.cursor - 1; i >= 0; i-- {
			if scan(s.ring[i]) {
				return out, nil
			}
		}
		for i := s.cap - 1; i >= s.cursor; i-- {
			if scan(s.ring[i]) {
				return out, nil
			}
		}
	} else {
		for i := s.cursor - 1; i >= 0; i-- {
			if scan(s.ring[i]) {
				return out, nil
			}
		}
	}
	return out, nil
}

func (s *memoryDecisionStore) Count(_ context.Context) (int, error) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	if s.full {
		return s.cap, nil
	}
	return s.cursor, nil
}

func (s *memoryDecisionStore) Close() error { return nil }
