package events

// Replay ring: every published event gets a monotonic id and lands in a
// fixed-capacity ring buffer so an SSE client that reconnects with
// Last-Event-ID can be caught up without a full poll. The ring is
// best-effort — when a client's resume point has already been overwritten,
// Replay reports the gap and the caller falls back to the state snapshot
// it sends on every connect anyway.

import (
	"sync"

	panelv1 "github.com/dbledeez/panel/proto/panel/v1"
)

// Stamped pairs an event with its bus-assigned monotonic id. IDs start at 1
// and never repeat for the lifetime of the process.
type Stamped struct {
	ID    uint64
	Event *panelv1.Event
}

// DefaultRingCap is how many recent events the bus retains for replay.
// State/app/player events are small and logs dominate volume; at a burst of
// ~200 events/s this still covers ~20s of reconnect window, and the SSE
// snapshot covers anything older.
const DefaultRingCap = 4096

type replayRing struct {
	mu   sync.Mutex
	buf  []Stamped
	next uint64 // id to assign to the NEXT appended event (starts at 1)
	head int    // index of oldest valid entry
	n    int    // number of valid entries (<= cap)
}

func newReplayRing(capacity int) *replayRing {
	if capacity <= 0 {
		capacity = DefaultRingCap
	}
	return &replayRing{buf: make([]Stamped, capacity), next: 1}
}

// append stamps e with the next monotonic id, stores it, and returns the id.
func (r *replayRing) append(e *panelv1.Event) uint64 {
	r.mu.Lock()
	defer r.mu.Unlock()
	id := r.next
	r.next++
	idx := (r.head + r.n) % len(r.buf)
	if r.n < len(r.buf) {
		r.buf[idx] = Stamped{ID: id, Event: e}
		r.n++
	} else {
		r.buf[r.head] = Stamped{ID: id, Event: e}
		r.head = (r.head + 1) % len(r.buf)
	}
	return id
}

// lastID returns the most recently assigned id (0 when nothing published).
func (r *replayRing) lastID() uint64 {
	r.mu.Lock()
	defer r.mu.Unlock()
	return r.next - 1
}

// since returns every retained event with ID > afterID, oldest first.
// ok=false means the resume point has been overwritten (afterID is older
// than the oldest retained event), so the returned slice — while still the
// full retained suffix — has a gap before it; callers should resync from a
// snapshot. afterID >= lastID returns (nil, true).
func (r *replayRing) since(afterID uint64) (out []Stamped, ok bool) {
	r.mu.Lock()
	defer r.mu.Unlock()
	if r.n == 0 {
		// Nothing retained: only gapless if the caller is already current.
		return nil, afterID >= r.next-1
	}
	oldest := r.buf[r.head].ID
	ok = afterID >= oldest-1
	for i := 0; i < r.n; i++ {
		s := r.buf[(r.head+i)%len(r.buf)]
		if s.ID > afterID {
			out = append(out, s)
		}
	}
	return out, ok
}
