package main

import (
	"sync"
	"time"
)

// turnGate enforces at most one in-flight agent turn per session.
//
// Why this exists (plan 2026-08-03, F1): handleChat runs a turn in the HTTP
// request goroutine, and every "resume" path (the empty-message reconnect,
// the auto-continuation worker, the idle sweep, answer-question, the /resume
// endpoint) launches ANOTHER goroutine running a full turn. Nothing prevented
// two turns for the SAME session at once, so a network blip that triggered a
// reconnect would spawn a duplicate resumeSession while the original turn was
// still alive — their tool calls interleaved on the wire and in the persisted
// transcript, which is the root cause behind the "parallel/nesting/sequence
// is off" and "task didn't end / flaky" reports.
//
// Model: one permit (buffered-1 channel seeded with a single token) per
// session id. Acquiring consumes the token; releasing puts it back.
//   - Background/best-effort callers (resumeSession and everything it backs)
//     use a non-blocking acquire and SKIP when busy — a duplicate nudge while a
//     turn is already running adds nothing, and the continuation/idle tickers
//     will retry on their own.
//   - The live chat path (an operator message) waits briefly for a finishing
//     background turn, then bails with an actionable error if still busy — see
//     handleChat.
//
// The permits map grows one entry per session id seen. For this single-agent
// homelab process that set is small and bounded by real sessions; cleanup is
// intentionally omitted (a sweep would race with acquire/release and the
// memory is negligible).
type turnGate struct {
	mu      sync.Mutex
	permits map[string]chan struct{}
}

func newTurnGate() *turnGate {
	return &turnGate{permits: make(map[string]chan struct{})}
}

// permit returns the single token-channel for sessionID, creating and seeding
// it on first use. Creation is guarded so two concurrent first-callers for the
// same id share one channel.
func (g *turnGate) permit(sessionID string) chan struct{} {
	g.mu.Lock()
	defer g.mu.Unlock()
	ch, ok := g.permits[sessionID]
	if !ok {
		ch = make(chan struct{}, 1)
		ch <- struct{}{}
		g.permits[sessionID] = ch
	}
	return ch
}

// acquire takes the session's permit. With wait <= 0 it is non-blocking
// (returns false immediately if a turn is active). With wait > 0 it blocks up
// to wait for the permit, returning false on timeout. Every true return MUST
// be paired with exactly one release.
func (g *turnGate) acquire(sessionID string, wait time.Duration) bool {
	ch := g.permit(sessionID)
	if wait <= 0 {
		select {
		case <-ch:
			return true
		default:
			return false
		}
	}
	t := time.NewTimer(wait)
	defer t.Stop()
	select {
	case <-ch:
		return true
	case <-t.C:
		return false
	}
}

// release returns the session's permit. Idempotent: a release with no matching
// acquire (or a double release) is a no-op rather than a blocking send.
func (g *turnGate) release(sessionID string) {
	ch := g.permit(sessionID)
	select {
	case ch <- struct{}{}:
	default:
	}
}
