package web

import (
	"sync"
	"sync/atomic"
)

// The SSE hub: how a move one player makes reaches the phones of everyone else
// at the felt.
//
// It is in-memory and it is intentionally dumb. It holds no game state and makes
// no decisions — it is a fan-out of opaque byte frames, keyed by table id. The
// authority is always the database; a frame is a nudge that says "the table at
// this version changed, come and look", and a subscriber that misses one (a
// dropped send, a reconnect) refetches the table, which is authoritative anyway.
// So a lost frame is a cosmetic hiccup, never a wrong balance.
//
// Two rules hold it together, and both are load-bearing:
//
//  1. **Sends are non-blocking.** A subscriber's channel is buffered, and a send
//     that would block is dropped, not waited on. The publish happens under the
//     table lock (which is what orders frames correctly for free), so a blocking
//     send would hold that lock while one phone on a train stalls — and the turn
//     clock behind that lock stalls with it, for the whole casino. A dropped frame
//     costs that one subscriber a refetch; a held lock costs everyone the room.
//
//  2. **The publisher never touches the database.** The hub is reached only after
//     the DB work is done and the connection released. Holding a *sql.Rows or a tx
//     open for the life of a stream would hold the one connection in the pool
//     forever, and a single subscriber would brick the whole application.

// hubFrame is what goes down the wire: an opaque payload the browser knows how to
// read (a JSON table view), tagged with the version it represents so a subscriber
// can tell a frame it already has from one it missed.
type hubFrame struct {
	Version int64
	Data    []byte
}

// tableSub is one open EventSource: a buffered channel and the id that lets the
// subscriber unregister itself when the stream closes.
type tableSub struct {
	id int64
	ch chan hubFrame
}

// gamesHub fans table frames out to whoever is watching each table.
type gamesHub struct {
	mu     sync.Mutex
	tables map[string]map[int64]*tableSub
	nextID atomic.Int64
}

func newGamesHub() *gamesHub {
	return &gamesHub{tables: make(map[string]map[int64]*tableSub)}
}

// subChanBuffer is how many frames a slow subscriber can fall behind before the
// hub starts dropping theirs. A few is plenty: a subscriber that far behind is
// going to refetch the authoritative table anyway, so buffering more just delays
// that with staler frames.
const subChanBuffer = 8

// subscribe registers a new watcher of a table and returns its channel plus the
// unsubscribe to defer. The channel is buffered so a publish never blocks on a
// reader that is mid-write to its socket.
func (h *gamesHub) subscribe(tableID string) (<-chan hubFrame, func()) {
	sub := &tableSub{id: h.nextID.Add(1), ch: make(chan hubFrame, subChanBuffer)}

	h.mu.Lock()
	subs := h.tables[tableID]
	if subs == nil {
		subs = make(map[int64]*tableSub)
		h.tables[tableID] = subs
	}
	subs[sub.id] = sub
	h.mu.Unlock()

	return sub.ch, func() { h.unsubscribe(tableID, sub.id) }
}

func (h *gamesHub) unsubscribe(tableID string, id int64) {
	h.mu.Lock()
	defer h.mu.Unlock()
	subs := h.tables[tableID]
	if subs == nil {
		return
	}
	delete(subs, id)
	if len(subs) == 0 {
		delete(h.tables, tableID)
	}
}

// publish pushes a frame to everyone watching a table, dropping it for any
// subscriber whose buffer is full rather than waiting on them. See rule 1: this
// is called under the table lock, so it must never block.
func (h *gamesHub) publish(tableID string, f hubFrame) {
	h.mu.Lock()
	defer h.mu.Unlock()
	for _, sub := range h.tables[tableID] {
		select {
		case sub.ch <- f:
		default:
			// Full buffer: this subscriber is behind. Dropping is correct — they will
			// refetch the authoritative table when they next read a version gap.
		}
	}
}

// watchers reports how many streams are open on a table. Used by the caller that
// decides whether a frame is worth rendering at all.
func (h *gamesHub) watchers(tableID string) int {
	h.mu.Lock()
	defer h.mu.Unlock()
	return len(h.tables[tableID])
}
