package decision

import "sync"

// ringSize is how many turns are kept. Turns arrive at human rate, not machine
// rate, so the whole store is memory: no migration, no insert on the answer
// path, and nothing of his words survives a restart. That is what makes this
// cheap enough to leave on always (V-564). Ecosystem traces went to SQLite
// because one act writes several hops and they must outlive the turn; an
// arbitration record is read minutes later or never.
const ringSize = 25

// Ring holds the newest records, newest first on read.
type Ring struct {
	mu   sync.Mutex
	recs []*Record
}

func NewRing() *Ring { return &Ring{} }

// Push adds one finished record and drops the oldest past the bound.
//
// The dropped pointers are cleared before the reslice. Resliceing alone moves
// the window forward and leaves the evicted records addressable from the
// backing array, so up to ringSize turns he had already aged out stayed in
// memory until the next append reallocated. That is a leak anywhere and it is
// the wrong one here, because the reason this store is memory-only is that his
// words should not outlive the diagnosis.
func (r *Ring) Push(rec *Record) {
	if r == nil || rec == nil {
		return
	}
	r.mu.Lock()
	defer r.mu.Unlock()
	r.recs = append(r.recs, rec)
	if drop := len(r.recs) - ringSize; drop > 0 {
		for i := 0; i < drop; i++ {
			r.recs[i] = nil
		}
		r.recs = r.recs[drop:]
	}
}

// Recent returns up to n records, newest first.
func (r *Ring) Recent(n int) []*Record {
	if r == nil || n <= 0 {
		return nil
	}
	r.mu.Lock()
	defer r.mu.Unlock()
	if n > len(r.recs) {
		n = len(r.recs)
	}
	out := make([]*Record, 0, n)
	for i := 0; i < n; i++ {
		out = append(out, r.recs[len(r.recs)-1-i])
	}
	return out
}
