package event

import (
	"sync"
	"time"
)

// Bus — the in-memory intake journal: a bounded ring of recent Events plus
// zero or more subscribers.
//
// Two properties are load-bearing, both about not changing production
// behaviour when nobody is watching:
//
//   - A nil *Bus is a working no-op. Publish on nil returns immediately, so
//     an intake path can call b.Publish(...) unconditionally and a daemon that
//     never built a bus behaves exactly as it did before. This is what let
//     eight callers adopt the envelope without a config flag each.
//   - Publish never blocks on a subscriber and never propagates a panic from
//     one. Intake is on the request path of POST /api/ambient and of every
//     fact write; a slow or broken observer must not be able to stall or kill
//     a write that already succeeded.
//
// The ring is bounded because it is memory that nothing prunes otherwise. Its
// contents are a window, not a record: the durable consequence of an event is
// the fact, note or task the intake path wrote.
type Bus struct {
	mu   sync.Mutex
	ring []Event // len == cap once full; oldest at (next % cap)
	next int
	n    int
	subs []func(Event)
}

// DefaultCapacity — how many recent events a bus keeps. A busy day is a few
// hundred intake events (a feed poll is one per new item), so this is roughly
// "today and yesterday" at a few hundred KB.
const DefaultCapacity = 512

// NewBus returns a bus keeping the last capacity events. capacity <= 0 uses
// DefaultCapacity.
func NewBus(capacity int) *Bus {
	if capacity <= 0 {
		capacity = DefaultCapacity
	}
	return &Bus{ring: make([]Event, capacity)}
}

// Publish normalizes e, drops it if it is not Valid, appends it to the ring and
// hands it to every subscriber. Safe on a nil receiver and safe from any
// goroutine.
//
// now is passed in rather than read from the clock: the whole point of #284's
// replay is that no time.Now() sits inside a path a scenario drives.
func (b *Bus) Publish(e Event, now time.Time) {
	if b == nil {
		return
	}
	e = e.Normalize(now)
	if !e.Valid() {
		return
	}

	b.mu.Lock()
	b.ring[b.next] = e
	b.next = (b.next + 1) % len(b.ring)
	if b.n < len(b.ring) {
		b.n++
	}
	subs := make([]func(Event), len(b.subs))
	copy(subs, b.subs)
	b.mu.Unlock()

	for _, fn := range subs {
		notify(fn, e)
	}
}

// notify calls one subscriber, swallowing a panic. A test double or a page
// renderer must not be able to take down a daemon from the intake path.
func notify(fn func(Event), e Event) {
	defer func() { _ = recover() }()
	fn(e)
}

// Subscribe registers fn to be called for every subsequent event, in publish
// order. There is no unsubscribe: subscribers are wired at startup and live as
// long as the daemon. Safe on a nil receiver (the subscription is dropped,
// which is the honest outcome when there is no bus to subscribe to).
func (b *Bus) Subscribe(fn func(Event)) {
	if b == nil || fn == nil {
		return
	}
	b.mu.Lock()
	defer b.mu.Unlock()
	b.subs = append(b.subs, fn)
}

// Recent returns up to limit events, most recently NOTICED first. limit <= 0
// returns everything held. Safe on a nil receiver (returns nil).
//
// The order is the ring's insertion order, which is NoticedAt order, and it is
// deliberately not OccurredAt order: a cold feed read publishes a week of items
// in feed order, so sorting by when things happened would put a six-day-old
// item above one that arrived before it. Readers must label the column
// accordingly.
func (b *Bus) Recent(limit int) []Event {
	if b == nil {
		return nil
	}
	b.mu.Lock()
	defer b.mu.Unlock()
	if b.n == 0 {
		return nil
	}
	if limit <= 0 || limit > b.n {
		limit = b.n
	}
	out := make([]Event, 0, limit)
	// next points one past the newest; walk backwards.
	for i := 0; i < limit; i++ {
		idx := (b.next - 1 - i + len(b.ring)*2) % len(b.ring)
		out = append(out, b.ring[idx])
	}
	return out
}

// Len reports how many events the ring currently holds. Safe on nil.
func (b *Bus) Len() int {
	if b == nil {
		return 0
	}
	b.mu.Lock()
	defer b.mu.Unlock()
	return b.n
}
