// Package push is the gateway's live-event fan-out. The gateway holds one
// backend gRPC subscription that feeds Publish; each connected client opens a
// Subscribe stream and receives only the events addressed to its user id. A slow
// client never blocks the backend feed — its bounded queue drops on overflow.
package push

import "sync"

// Event is one live event addressed to a user. Payload is the FlatBuffers body
// the gateway forwards verbatim to the client.
type Event struct {
	UserID  string
	Kind    string
	Payload []byte
	EventID string
}

// defaultBuffer is the per-client queue depth used when NewHub is given a
// non-positive size.
const defaultBuffer = 64

// Hub fans backend events out to per-user client subscriptions.
type Hub struct {
	bufSize int

	mu     sync.Mutex
	nextID int
	subs   map[int]*subscription
}

type subscription struct {
	userID string
	ch     chan Event
}

// NewHub constructs a Hub whose per-client queue holds bufSize events.
func NewHub(bufSize int) *Hub {
	if bufSize <= 0 {
		bufSize = defaultBuffer
	}
	return &Hub{bufSize: bufSize, subs: make(map[int]*subscription)}
}

// Publish delivers e to every subscription for e.UserID, dropping it for any
// whose queue is full.
func (h *Hub) Publish(e Event) {
	h.mu.Lock()
	defer h.mu.Unlock()
	for _, s := range h.subs {
		if s.userID != e.UserID {
			continue
		}
		select {
		case s.ch <- e:
		default:
		}
	}
}

// Subscribe registers a client stream for userID and returns its event channel
// and an unsubscribe func that closes the channel.
func (h *Hub) Subscribe(userID string) (<-chan Event, func()) {
	h.mu.Lock()
	defer h.mu.Unlock()
	id := h.nextID
	h.nextID++
	s := &subscription{userID: userID, ch: make(chan Event, h.bufSize)}
	h.subs[id] = s
	return s.ch, func() { h.unsubscribe(id) }
}

// unsubscribe removes and closes a subscription. It holds the same lock as
// Publish, so it never closes a channel mid-send.
func (h *Hub) unsubscribe(id int) {
	h.mu.Lock()
	defer h.mu.Unlock()
	if s, ok := h.subs[id]; ok {
		delete(h.subs, id)
		close(s.ch)
	}
}

// SubscriberCount returns the number of active subscriptions (for tests/metrics).
func (h *Hub) SubscriberCount() int {
	h.mu.Lock()
	defer h.mu.Unlock()
	return len(h.subs)
}

// HasSubscribers reports whether any live client stream is registered for userID.
// It gates out-of-app push: an online user is already reached in-app, so the
// platform push (Telegram) is skipped for them — keeping the fallback channel free
// of duplicates.
func (h *Hub) HasSubscribers(userID string) bool {
	h.mu.Lock()
	defer h.mu.Unlock()
	for _, s := range h.subs {
		if s.userID == userID {
			return true
		}
	}
	return false
}
