package services

// Local diagnostics for the authenticated simulator. Metadata is the default;
// content capture requires an explicit server-side opt-in for each new run.
import (
	"bufio"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"sync"
	"time"

	"github.com/google/uuid"
)

const simulationMaxBytes = 512 * 1024
const simulationRetention = 7 * 24 * time.Hour

var ErrSimulationID = errors.New("invalid simulation ID")
var ErrSimulationFeedback = errors.New("invalid simulation feedback")

type SimulationEvent struct {
	Seq       int            `json:"seq"`
	At        time.Time      `json:"at"`
	ElapsedMs int64          `json:"elapsed_ms"`
	Stage     string         `json:"stage"`
	Name      string         `json:"name,omitempty"`
	Status    string         `json:"status"`
	Data      map[string]any `json:"data,omitempty"`
}

type SimulationLog struct {
	TurnID      string            `json:"turn_id"`
	SessionID   string            `json:"session_id"`
	AgentID     uint              `json:"agent_id"`
	Status      string            `json:"status"`
	ContentMode string            `json:"content_mode"`
	Events      []SimulationEvent `json:"events"`
}

type SimulationLogStore struct {
	mu          sync.Mutex // Serializes complete line writes and reads, never AI work.
	dir         string
	subscribers map[string]map[chan struct{}]struct{}
}

type SimulationRecorder struct {
	store          *SimulationLogStore
	agentID        uint
	turnID         string
	start          time.Time
	seq            int
	bytes          int
	failed         bool
	finished       bool
	includeContent bool
}

type simulationRecorderKey struct{}

func NewSimulationID() string { return uuid.NewString() }

func ValidSimulationID(id string) bool {
	parsed, err := uuid.Parse(id)
	return err == nil && parsed.String() == id && parsed != uuid.Nil
}

func NewSimulationLogStore(dir string) *SimulationLogStore { return &SimulationLogStore{dir: dir} }

func (s *SimulationLogStore) path(agentID uint, turnID string) (string, error) {
	if agentID == 0 || !ValidSimulationID(turnID) {
		return "", ErrSimulationID
	}
	return filepath.Join(s.dir, fmt.Sprintf("agent-%d", agentID), turnID+".jsonl"), nil
}

func (s *SimulationLogStore) Start(agentID uint, turnID, sessionID string, data map[string]any) (*SimulationRecorder, error) {
	path, err := s.path(agentID, turnID)
	if err != nil || !ValidSimulationID(sessionID) {
		return nil, ErrSimulationID
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
		return nil, err
	}
	s.pruneLocked(filepath.Dir(path))
	f, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
	if err != nil {
		return nil, err
	}
	if err := f.Close(); err != nil {
		return nil, err
	}
	r := &SimulationRecorder{store: s, agentID: agentID, turnID: turnID, start: time.Now(), includeContent: SimulationLogContentEnabled()}
	if data == nil {
		data = map[string]any{}
	}
	data["turn_id"], data["session_id"] = turnID, sessionID
	data["content_mode"] = "metadata"
	if r.includeContent {
		data["content_mode"] = "debug"
	}
	if !r.appendLocked("started", "", "running", data) {
		return nil, errors.New("simulation log unavailable")
	}
	return r, nil
}

func (s *SimulationLogStore) pruneLocked(dir string) {
	entries, err := os.ReadDir(dir)
	if err != nil {
		return
	}
	type item struct {
		name string
		at   time.Time
	}
	var kept []item
	for _, entry := range entries {
		if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".jsonl") || !ValidSimulationID(strings.TrimSuffix(entry.Name(), ".jsonl")) {
			continue
		}
		info, err := entry.Info()
		if err != nil {
			continue
		}
		if time.Since(info.ModTime()) > simulationRetention {
			_ = os.Remove(filepath.Join(dir, entry.Name()))
			continue
		}
		kept = append(kept, item{entry.Name(), info.ModTime()})
	}
	sort.Slice(kept, func(i, j int) bool { return kept[i].at.After(kept[j].at) })
	// At most 200 recent tests per assistant; never remove an in-flight test.
	for i := 199; i < len(kept); i++ {
		if time.Since(kept[i].at) > 2*time.Minute {
			_ = os.Remove(filepath.Join(dir, kept[i].name))
		}
	}
}

// Restrict both each field and the total event. JSON-tool payloads are decoded
// first so sensitive named fields cannot bypass this filter as nested strings.
func simulationSafeValue(value any, depth int) any {
	if depth > 8 {
		return "[truncated: nesting limit]"
	}
	switch v := value.(type) {
	case map[string]any:
		out := map[string]any{}
		for key, value := range v {
			if diagnosticSensitiveKey(key) {
				out[key] = "[redacted]"
			} else {
				out[key] = simulationSafeValue(value, depth+1)
			}
		}
		return out
	case []any:
		out := make([]any, 0, min(len(v), 40)+1)
		for _, item := range v[:min(len(v), 40)] {
			out = append(out, simulationSafeValue(item, depth+1))
		}
		if len(v) > 40 {
			out = append(out, "[truncated: item limit]")
		}
		return out
	case string:
		v = diagnosticJSONText(v, depth)
		runes := []rune(v)
		if len(runes) > 16000 {
			return string(runes[:16000]) + " [truncated: field limit]"
		}
		return v
	default:
		return v
	}
}

func simulationSafeData(data map[string]any) map[string]any {
	encoded, err := json.Marshal(data)
	if err != nil {
		return map[string]any{"truncated": "unserializable data"}
	}
	var plain map[string]any
	if json.Unmarshal(encoded, &plain) != nil {
		return nil
	}
	return simulationSafeValue(plain, 0).(map[string]any)
}

func (r *SimulationRecorder) appendLocked(stage, name, status string, data map[string]any) bool {
	if r.failed {
		return false
	}
	if r.seq >= 120 && stage != "finished" && stage != "feedback" {
		return true
	}
	safeData := simulationEventData(data, r.includeContent, stage)
	e := SimulationEvent{Seq: r.seq + 1, At: time.Now().UTC(), ElapsedMs: time.Since(r.start).Milliseconds(), Stage: stage, Name: name, Status: status, Data: safeData}
	raw, err := json.Marshal(e)
	if err != nil {
		r.failed = true
		return false
	}
	limit := 48 * 1024
	if stage == "finished" {
		limit = 96 * 1024
	}
	if len(raw) > limit || r.bytes+len(raw) > simulationMaxBytes-110*1024 && stage != "finished" && stage != "feedback" {
		e.Data = map[string]any{"truncated": "event size limit; use the final reply and source IDs"}
		// Correlation must survive a long multilingual input/history snapshot.
		for _, key := range []string{"turn_id", "session_id", "content_mode", "settings", "response_mode", "response_chars", "handoff_reason"} {
			if value, ok := safeData[key]; ok {
				e.Data[key] = simulationSafeValue(value, 0)
			}
		}
		raw, _ = json.Marshal(e)
	}
	if r.bytes+len(raw)+1 > simulationMaxBytes {
		r.failed = true
		return false
	}
	path, _ := r.store.path(r.agentID, r.turnID)
	f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0600)
	if err != nil {
		r.failed = true
		return false
	}
	n, err := f.Write(append(raw, '\n'))
	closeErr := f.Close()
	if err != nil || closeErr != nil || n != len(raw)+1 {
		r.failed = true
		return false
	}
	r.seq++
	r.bytes += n
	for ch := range r.store.subscribers[path] {
		select {
		case ch <- struct{}{}:
		default:
		}
	}
	return true
}

// Subscribe wakes a reader after a persisted event. A buffered notification can
// coalesce; the reader replays the complete file so slow clients lose no events.
// Register before reading to avoid a read/subscribe gap. Never hold this lock
// while writing to the network or waiting for a model.
func (s *SimulationLogStore) Subscribe(agentID uint, turnID string) (<-chan struct{}, func(), error) {
	path, err := s.path(agentID, turnID)
	if err != nil {
		return nil, nil, err
	}
	ch := make(chan struct{}, 1)
	s.mu.Lock()
	if s.subscribers == nil {
		s.subscribers = make(map[string]map[chan struct{}]struct{})
	}
	if s.subscribers[path] == nil {
		s.subscribers[path] = make(map[chan struct{}]struct{})
	}
	s.subscribers[path][ch] = struct{}{}
	s.mu.Unlock()
	var once sync.Once
	return ch, func() {
		once.Do(func() {
			s.mu.Lock()
			defer s.mu.Unlock()
			delete(s.subscribers[path], ch)
			if len(s.subscribers[path]) == 0 {
				delete(s.subscribers, path)
			}
		})
	}, nil
}

func (r *SimulationRecorder) Context(ctx context.Context) context.Context {
	if r == nil {
		return ctx
	}
	return context.WithValue(ctx, simulationRecorderKey{}, r)
}

func (r *SimulationRecorder) Record(stage, name, status string, data map[string]any) {
	if r == nil {
		return
	}
	r.store.mu.Lock()
	defer r.store.mu.Unlock()
	if !r.finished {
		r.appendLocked(stage, name, status, data)
	}
}

func RecordSimulationEvent(ctx context.Context, stage, name, status string, data map[string]any) {
	if r, _ := ctx.Value(simulationRecorderKey{}).(*SimulationRecorder); r != nil {
		r.Record(stage, name, status, data)
	}
}

func (r *SimulationRecorder) Finish(status string, data map[string]any) bool {
	if r == nil {
		return false
	}
	r.store.mu.Lock()
	defer r.store.mu.Unlock()
	if r.finished {
		return !r.failed
	}
	r.finished = true
	return r.appendLocked("finished", "", status, data)
}

func (s *SimulationLogStore) Read(agentID uint, turnID string) (SimulationLog, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.readLocked(agentID, turnID)
}

func (s *SimulationLogStore) readLocked(agentID uint, turnID string) (SimulationLog, error) {
	out := SimulationLog{AgentID: agentID, TurnID: turnID, Status: "running", ContentMode: "metadata", Events: []SimulationEvent{}}
	path, err := s.path(agentID, turnID)
	if err != nil {
		return out, err
	}
	f, err := os.Open(path)
	if err != nil {
		return out, err
	}
	defer f.Close()
	info, err := f.Stat()
	if err != nil {
		return out, err
	}
	if time.Since(info.ModTime()) > simulationRetention {
		return out, os.ErrNotExist
	}
	if info.Size() > simulationMaxBytes {
		return out, errors.New("simulation log size limit")
	}
	scanner := bufio.NewScanner(io.LimitReader(f, simulationMaxBytes))
	scanner.Buffer(make([]byte, 4096), 128*1024)
	includeContent := SimulationLogContentEnabled()
	for scanner.Scan() {
		var e SimulationEvent
		if err := json.Unmarshal(scanner.Bytes(), &e); err != nil {
			return out, err
		}
		e.Data = simulationEventData(e.Data, includeContent, e.Stage)
		out.Events = append(out.Events, e)
		if e.Stage == "started" {
			out.SessionID, _ = e.Data["session_id"].(string)
			mode, _ := e.Data["content_mode"].(string)
			if includeContent && mode != "metadata" {
				out.ContentMode = "debug"
			}
			e.Data["content_mode"] = out.ContentMode
		}
		if e.Stage == "finished" {
			out.Status = e.Status
		}
	}
	if out.Status == "running" && time.Since(info.ModTime()) > 90*time.Second {
		out.Status = "interrupted"
	}
	return out, scanner.Err()
}

func (s *SimulationLogStore) Feedback(agentID uint, turnID, rating, note string, userID uint) error {
	if (rating != "correct" && rating != "needs_work") || len([]rune(note)) > 2000 {
		return ErrSimulationFeedback
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	log, err := s.readLocked(agentID, turnID)
	if err != nil {
		return err
	}
	if log.Status == "running" || len(log.Events) == 0 {
		return ErrSimulationFeedback
	}
	count := 0
	for _, e := range log.Events {
		if e.Stage == "feedback" {
			count++
		}
	}
	if count >= 10 {
		return ErrSimulationFeedback
	}
	path, _ := s.path(agentID, turnID)
	info, err := os.Stat(path)
	if err != nil {
		return err
	}
	r := &SimulationRecorder{store: s, agentID: agentID, turnID: turnID, start: log.Events[0].At, seq: len(log.Events), bytes: int(info.Size()), includeContent: log.ContentMode == "debug"}
	if !r.appendLocked("feedback", "", "saved", map[string]any{"rating": rating, "note": strings.TrimSpace(note), "user_id": userID}) {
		return errors.New("simulation feedback not saved")
	}
	return nil
}
