package uisession

import (
	"errors"
	"strings"
	"sync"
	"time"
)

// SubscriptionState is the Store-internal strong-typed enum describing the
// authoritative post-mutation state of a session. The HTTP handler maps it
// explicitly to the wire `state` field (empty|selected|cleared); unknown
// values are fail-closed. The Store type is NOT coupled to the wire string —
// no String()/MarshalJSON on this type; the mapping lives only in the HTTP
// handler.
type SubscriptionState uint8

const (
	SubscriptionStateEmpty SubscriptionState = iota
	SubscriptionStateSelected
	SubscriptionStateCleared
)

// ChangeKind is the Store-internal strong-typed enum identifying which kind
// of mutation produced a Change. The handler explicitly translates
// Selected|Cleared|Advanced to wire event names; no String()/MarshalJSON on
// the Store type, no numeric direct-output, no implicit coupling.
type ChangeKind uint8

const (
	ChangeKindSelected ChangeKind = iota
	ChangeKindCleared
	ChangeKindAdvanced
)

// Change is one Store-emitted mutation descriptor. State is the post-change
// authoritative SubscriptionState; ActiveServerID is the post-change server
// (empty unless State == SubscriptionStateSelected). State MUST NOT be
// treated as a server identifier.
type Change struct {
	Kind           ChangeKind
	Revision       uint64
	State          SubscriptionState
	ActiveServerID string
	Generation     uint64
}

// SubscriptionSnapshot is the linearized view of a session at subscribe time.
type SubscriptionSnapshot struct {
	State          SubscriptionState
	ActiveServerID string
	Generation     uint64
	Revision       uint64
}

// Subscription is the subscribe return value. Release is idempotent.
type Subscription struct {
	Snapshot SubscriptionSnapshot
	Changes  <-chan Change
	Release  func()
}

// ErrSessionClosed is the typed sentinel returned by Select/Subscribe when
// the session has been revoked (tombstoned).
var ErrSessionClosed = errors.New("uisession: session is closed (revoked)")

// subscriberRegistry holds the in-flight subscribers for one session.
// subscriberRegistry type is declared here so same-package _test.go seam
// helpers can read its `entries` length without exporting the registry.
type subscriberRegistry struct {
	nextID    int
	expiresAt time.Time
	entries   map[int]*subscriber
}

type subscriber struct {
	changes chan Change
}

func (s *Store) Subscribe(sessionID string, expiresAt time.Time) (Subscription, error) {
	sessionID = strings.TrimSpace(sessionID)
	if sessionID == "" {
		return Subscription{}, ErrSessionRequired
	}
	now := s.now()
	if expired(expiresAt, now) {
		return Subscription{}, ErrSessionExpired
	}

	s.mu.Lock()
	defer s.mu.Unlock()
	s.pruneLocked(now)
	if _, closed := s.tombstones[sessionID]; closed {
		return Subscription{}, ErrSessionClosed
	}

	current, exists := s.entries[sessionID]
	snapshot := SubscriptionSnapshot{
		State:      SubscriptionStateEmpty,
		Generation: current.Generation,
		Revision:   current.revision,
	}
	if exists {
		snapshot.ActiveServerID = current.ActiveServerID
		if current.ActiveServerID == "" {
			snapshot.State = SubscriptionStateCleared
		} else {
			snapshot.State = SubscriptionStateSelected
		}
	}

	registry := s.subscribers[sessionID]
	if registry == nil {
		registry = &subscriberRegistry{
			expiresAt: expiresAt,
			entries:   make(map[int]*subscriber),
		}
		s.subscribers[sessionID] = registry
	}
	id := registry.nextID
	registry.nextID++
	sub := &subscriber{changes: make(chan Change, s.subCapacity)}
	registry.entries[id] = sub

	var once sync.Once
	release := func() {
		once.Do(func() {
			s.mu.Lock()
			defer s.mu.Unlock()
			registry := s.subscribers[sessionID]
			if registry == nil || registry.entries[id] != sub {
				return
			}
			delete(registry.entries, id)
			close(sub.changes)
			if len(registry.entries) == 0 {
				delete(s.subscribers, sessionID)
			}
		})
	}

	return Subscription{
		Snapshot: snapshot,
		Changes:  sub.changes,
		Release:  release,
	}, nil
}

func (s *Store) Revoke(sessionID string, expiresAt time.Time) {
	sessionID = strings.TrimSpace(sessionID)
	if sessionID == "" {
		return
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	if _, exists := s.tombstones[sessionID]; exists {
		return
	}
	s.notifyGenerationChangedLocked(sessionID)
	s.closeSubscribersLocked(sessionID)
	delete(s.entries, sessionID)
	s.tombstones[sessionID] = expiresAt
}

// Rotate moves the state owned by one UI-session credential to its renewed
// credential. The old credential is closed before the new identity becomes
// observable, while the active selection and its generation remain unchanged.
func (s *Store) Rotate(previousSessionID, nextSessionID string, expiresAt time.Time) {
	previousSessionID = strings.TrimSpace(previousSessionID)
	nextSessionID = strings.TrimSpace(nextSessionID)
	if previousSessionID == "" || nextSessionID == "" || previousSessionID == nextSessionID {
		return
	}

	now := s.now()
	s.mu.Lock()
	defer s.mu.Unlock()

	// Token renewal has already accepted previousSessionID. Move its selection
	// before pruning so a lease-boundary renewal cannot strand the new token
	// without an active server.
	s.notifyGenerationChangedLocked(previousSessionID)
	s.closeSubscribersLocked(previousSessionID)
	if current, ok := s.entries[previousSessionID]; ok {
		delete(s.entries, previousSessionID)
		current.ExpiresAt = expiresAt
		current.updatedAt = now
		s.entries[nextSessionID] = current
	}
	s.tombstones[previousSessionID] = expiresAt
	s.pruneLocked(now)
	s.enforceLimitLocked()
}

func (s *Store) publishLocked(sessionID string, change Change) {
	registry := s.subscribers[sessionID]
	if registry == nil {
		return
	}
	for id, sub := range registry.entries {
		if change.Kind == ChangeKindAdvanced {
			if s.publishAdvancedLocked(sub, change) {
				continue
			}
		} else {
			select {
			case sub.changes <- change:
				continue
			default:
			}
		}
		delete(registry.entries, id)
		close(sub.changes)
	}
	if len(registry.entries) == 0 {
		delete(s.subscribers, sessionID)
	}
}

func (s *Store) publishAdvancedLocked(sub *subscriber, change Change) bool {
	select {
	case sub.changes <- change:
		return true
	default:
	}

	buffered := len(sub.changes)
	if buffered == 0 {
		return false
	}
	pending := make([]Change, 0, buffered)
	for i := 0; i < buffered; i++ {
		pending = append(pending, <-sub.changes)
	}
	last := len(pending) - 1
	if pending[last].Kind != ChangeKindAdvanced ||
		pending[last].ActiveServerID != change.ActiveServerID {
		for _, item := range pending {
			sub.changes <- item
		}
		return false
	}
	pending[last] = change
	for _, item := range pending {
		sub.changes <- item
	}
	return true
}

func (s *Store) closeSubscribersLocked(sessionID string) {
	registry := s.subscribers[sessionID]
	if registry == nil {
		return
	}
	delete(s.subscribers, sessionID)
	for _, sub := range registry.entries {
		close(sub.changes)
	}
}
