package main

import (
	"log/slog"
	"sync"
)

// maxQueuedPerSession caps a session's queue. A held turn plus unbounded
// enqueues would grow memory without limit; an operator nudging a long
// autonomous turn realistically queues only a handful, so a generous cap is
// pure insurance. Overflow drops the newest enqueue and logs (the message is
// already persisted in the DB by handleChat before enqueue, so it isn't lost
// from the transcript — it just won't auto-run).
const maxQueuedPerSession = 20

// messageQueue holds operator messages that arrived while a turn was already
// running for a session. Plan 2026-08-03 (F2): instead of rejecting the
// operator's message with "Nomos is still finishing a previous step… send it
// again", the message is queued and auto-run when the in-flight turn releases
// the session's turn-gate permit.
//
// The queue only schedules WHEN a turn runs, not WHETHER the message is stored
// — handleChat persists the user message before acquiring the gate, so a queued
// message is already in the transcript; this just makes sure a turn eventually
// acts on it.
//
// Draining is strictly one-at-a-time under the turn gate (see drainQueued in
// main.go), so this cannot stack concurrent turns — the exact hazard the gate
// itself exists to prevent. Background resumeSession callers never touch this
// queue; they keep their non-blocking skip.
type messageQueue struct {
	mu    sync.Mutex
	queue map[string][]string
}

func newMessageQueue() *messageQueue {
	return &messageQueue{queue: map[string][]string{}}
}

// enqueue appends a message to the back of the session's FIFO. Returns false
// (and logs) if the session is already at maxQueuedPerSession — the caller's
// message is already persisted in the DB, so this only skips auto-running it.
func (q *messageQueue) enqueue(sessionID, msg string) bool {
	q.mu.Lock()
	defer q.mu.Unlock()
	if len(q.queue[sessionID]) >= maxQueuedPerSession {
		slog.Warn("nomos: message queue full; dropping auto-run for operator message", "session", sessionID, "cap", maxQueuedPerSession)
		return false
	}
	q.queue[sessionID] = append(q.queue[sessionID], msg)
	return true
}

// dequeue pops the next message from the front of the session's FIFO. Returns
// ok=false when empty.
func (q *messageQueue) dequeue(sessionID string) (string, bool) {
	q.mu.Lock()
	defer q.mu.Unlock()
	xs := q.queue[sessionID]
	if len(xs) == 0 {
		return "", false
	}
	m := xs[0]
	q.queue[sessionID] = xs[1:]
	return m, true
}

// requeueFront pushes a message back to the front — used when a drainer popped
// a message but lost the race for the gate to a live turn; that turn's own
// release will drain it again.
func (q *messageQueue) requeueFront(sessionID, msg string) {
	q.mu.Lock()
	defer q.mu.Unlock()
	q.queue[sessionID] = append([]string{msg}, q.queue[sessionID]...)
}

// peek reports the queued depth for a session (test/diagnostic helper).
func (q *messageQueue) peek(sessionID string) int {
	q.mu.Lock()
	defer q.mu.Unlock()
	return len(q.queue[sessionID])
}
