// stream.go 实现通知 inbox 的实时推送（spec §11 步骤8）。
//
// StreamHub 是 per-user 的订阅扇出中心：每个浏览器标签页 Subscribe 一个有界缓冲
// channel；Dispatcher 落库成功后 Publish 到该用户的全部订阅者。慢订阅者（缓冲满）
// 直接丢弃该消息（drop-on-slow），绝不阻塞 Publish / Dispatch，避免 goroutine /
// 内存泄漏（镜像 chat StreamerHub 的丢弃纪律）。
package notify

import (
	"sync"

	"github.com/google/uuid"
)

// StreamHub 是 Dispatcher 推送实时通知所需的窄接口。
type StreamHub interface {
	Subscribe(userID uuid.UUID) (<-chan Message, func())
	Publish(userID uuid.UUID, m Message)
}

// streamSubBuffer 是单个订阅者的缓冲深度。满时新消息被丢弃（客户端可用 inbox
// 列表接口补偿，与 chat WS onopen 主动拉取同理）。
const streamSubBuffer = 16

// memHub 是 StreamHub 的进程内实现。
type memHub struct {
	mu   sync.Mutex
	subs map[uuid.UUID]map[int]chan Message
	next int
}

// NewStreamHub 构造进程内的 per-user 通知推送中心。
func NewStreamHub() StreamHub {
	return &memHub{subs: map[uuid.UUID]map[int]chan Message{}}
}

// Subscribe 为某用户注册一个订阅者，返回只读 channel 与取消函数。取消函数幂等：
// 关闭 channel 并从订阅表移除；调用方应在请求结束时 defer 调用。
func (h *memHub) Subscribe(userID uuid.UUID) (<-chan Message, func()) {
	ch := make(chan Message, streamSubBuffer)
	h.mu.Lock()
	if h.subs[userID] == nil {
		h.subs[userID] = map[int]chan Message{}
	}
	id := h.next
	h.next++
	h.subs[userID][id] = ch
	h.mu.Unlock()

	var once sync.Once
	cancel := func() {
		once.Do(func() {
			h.mu.Lock()
			if m, ok := h.subs[userID]; ok {
				if c, ok := m[id]; ok {
					delete(m, id)
					close(c)
				}
				if len(m) == 0 {
					delete(h.subs, userID)
				}
			}
			h.mu.Unlock()
		})
	}
	return ch, cancel
}

// Publish 向某用户的全部订阅者非阻塞投递一条消息。缓冲满的订阅者被跳过。
//
// 关键：整个投递循环持有 h.mu。因为发送是非阻塞的（select default），持锁开销极小，
// 但这让 Publish 与 cancel()（同样持 h.mu 后 close channel）互斥——否则快照 channel
// 释放锁后、cancel 并发 close，会 panic "send on closed channel"。
func (h *memHub) Publish(userID uuid.UUID, m Message) {
	h.mu.Lock()
	defer h.mu.Unlock()
	for _, c := range h.subs[userID] {
		select {
		case c <- m:
		default:
			// drop-on-slow：缓冲满，丢弃，避免阻塞 Dispatch。
		}
	}
}
