// Package eventbus is an in-process publish/subscribe bus for real-time
// events the server emits to subscribed clients (today: Flutter via SSE).
//
// Writers (likes handler, requests reconciler, scanner, etc.) call Publish
// to announce a state change. Each connected SSE client holds a
// subscription via Subscribe and forwards events to its HTTP response.
//
// The bus is intentionally simple:
//
//   - Per-subscriber bounded channel buffer. If a subscriber's buffer is
//     full when an event arrives, the event is dropped FOR THAT SUBSCRIBER
//     ONLY — writers never block, and other subscribers are unaffected.
//   - No persistence. A client that connects after an event was emitted
//     will not see that event. SSE clients are expected to fetch a fresh
//     snapshot via the normal /api/* endpoints on (re)connect.
//   - No cross-process broadcast. v1 runs as a single Go binary; a future
//     multi-instance deployment will need a real broker (NATS / Redis
//     pub-sub / etc.) but that's out of scope here.
package eventbus

import (
	"sync"
)

// Event is a single broadcast message. Kind names follow a "domain.action"
// convention (e.g. "track.liked", "request.status_changed"). UserID, when
// non-empty, scopes the event to a specific user — subscribers receive
// only events with UserID == their auth user OR with empty UserID (which
// means "broadcast to everyone subscribed").
type Event struct {
	Kind   string         `json:"kind"`
	UserID string         `json:"user_id,omitempty"`
	Data   map[string]any `json:"data"`
}

// Bus is a fan-out broadcaster. Safe for concurrent use; the zero value is
// not usable — call New().
type Bus struct {
	mu          sync.RWMutex
	subscribers map[chan Event]struct{}
}

// New returns a Bus with no subscribers.
func New() *Bus {
	return &Bus{subscribers: map[chan Event]struct{}{}}
}

// Publish fans an event out to every subscriber. Non-blocking per
// subscriber: if a subscriber's buffer is full, the event is dropped for
// that subscriber rather than stalling the writer.
func (b *Bus) Publish(e Event) {
	b.mu.RLock()
	defer b.mu.RUnlock()
	for ch := range b.subscribers {
		select {
		case ch <- e:
		default:
			// Subscriber buffer full — drop. The subscriber will resync
			// via a normal API fetch when the client decides to.
		}
	}
}

// Subscribe registers a new subscriber and returns its receive channel
// plus an unsubscribe func. The buffer argument controls how many events
// can be queued before drops occur; 16–64 is a reasonable range for an
// SSE client that processes events promptly.
//
// Callers MUST invoke the returned unsubscribe func when done (typically
// in a defer), or the subscription leaks.
func (b *Bus) Subscribe(buffer int) (<-chan Event, func()) {
	if buffer <= 0 {
		buffer = 16
	}
	ch := make(chan Event, buffer)
	b.mu.Lock()
	b.subscribers[ch] = struct{}{}
	b.mu.Unlock()
	unsub := func() {
		b.mu.Lock()
		if _, ok := b.subscribers[ch]; ok {
			delete(b.subscribers, ch)
			close(ch)
		}
		b.mu.Unlock()
	}
	return ch, unsub
}

// SubscriberCount returns the current number of active subscribers. For
// tests + observability; not load-bearing in hot paths.
func (b *Bus) SubscriberCount() int {
	b.mu.RLock()
	defer b.mu.RUnlock()
	return len(b.subscribers)
}
