package events

import (
	"sync"

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

// LogBuffer is a bounded per-instance ring buffer of recent events. Its
// purpose is UX: when the operator opens the Console tab for an instance,
// we want them to see what's been going on — not just events that happen
// *after* they subscribed. The buffer tails the event bus continuously so
// a short history is always available.
//
// Events are stored verbatim (full *Event payload) so the caller can render
// them exactly like the live SSE stream would. Only log, player, and
// instance_state payloads are retained — stats and app_state churn too
// fast and aren't what "Console history" should show.
type LogBuffer struct {
	bus     *Bus
	cap     int
	mu      sync.Mutex
	byInst  map[string]*ringSlice
	stopped bool
	done    chan struct{}
}

// NewLogBuffer creates the buffer and starts a background goroutine that
// consumes the bus. Call Stop to shut down.
//
// capPerInstance is the maximum number of events kept per instance id. The
// UI typically asks for the last 400, so 500 is a comfortable default.
func NewLogBuffer(bus *Bus, capPerInstance int) *LogBuffer {
	if capPerInstance <= 0 {
		capPerInstance = 500
	}
	b := &LogBuffer{
		bus:    bus,
		cap:    capPerInstance,
		byInst: map[string]*ringSlice{},
		done:   make(chan struct{}),
	}
	go b.run()
	return b
}

func (b *LogBuffer) run() {
	defer close(b.done)
	sub := b.bus.Subscribe(Filter{})
	defer sub.Close()
	for ev := range sub.Events() {
		if !isRetained(ev) {
			continue
		}
		id := eventInstanceID(ev)
		if id == "" {
			continue
		}
		b.mu.Lock()
		if b.stopped {
			b.mu.Unlock()
			return
		}
		r := b.byInst[id]
		if r == nil {
			r = newRing(b.cap)
			b.byInst[id] = r
		}
		r.push(ev)
		b.mu.Unlock()
	}
}

// Recent returns up to `limit` most-recent retained events for an instance,
// oldest first. Safe to call concurrently with Run.
func (b *LogBuffer) Recent(instanceID string, limit int) []*panelv1.Event {
	b.mu.Lock()
	defer b.mu.Unlock()
	r := b.byInst[instanceID]
	if r == nil {
		return nil
	}
	return r.tail(limit)
}

// Forget drops the buffer for an instance. Called when an instance is
// deleted so memory doesn't leak for dead ids.
func (b *LogBuffer) Forget(instanceID string) {
	b.mu.Lock()
	delete(b.byInst, instanceID)
	b.mu.Unlock()
}

// Stop ends the background goroutine. Safe to call multiple times.
func (b *LogBuffer) Stop() {
	b.mu.Lock()
	if b.stopped {
		b.mu.Unlock()
		return
	}
	b.stopped = true
	b.mu.Unlock()
}

// isRetained decides whether an event belongs in the console history.
//
//   - log lines: yes, that's the whole point.
//   - player events: yes — joins/leaves/chat/deaths belong on the console.
//   - instance_state: yes — "server started"-style transitions are context.
//   - stats / app_state: no — churn too fast; would push real content out
//     of the ring.
func isRetained(e *panelv1.Event) bool {
	switch e.Payload.(type) {
	case *panelv1.Event_Log, *panelv1.Event_Player, *panelv1.Event_InstanceState:
		return true
	}
	return false
}

// ringSlice is a fixed-capacity FIFO. Implemented as a slice with head/len
// tracking to avoid allocating on steady-state appends.
type ringSlice struct {
	items []*panelv1.Event
	head  int // index of the oldest entry (when len == cap)
	len   int
	cap   int
}

func newRing(cap int) *ringSlice {
	return &ringSlice{items: make([]*panelv1.Event, cap), cap: cap}
}

func (r *ringSlice) push(e *panelv1.Event) {
	if r.len < r.cap {
		r.items[(r.head+r.len)%r.cap] = e
		r.len++
		return
	}
	// At capacity: overwrite the oldest.
	r.items[r.head] = e
	r.head = (r.head + 1) % r.cap
}

func (r *ringSlice) tail(limit int) []*panelv1.Event {
	if limit <= 0 || limit > r.len {
		limit = r.len
	}
	out := make([]*panelv1.Event, 0, limit)
	start := (r.head + r.len - limit + r.cap) % r.cap
	for i := 0; i < limit; i++ {
		out = append(out, r.items[(start+i)%r.cap])
	}
	return out
}
