package account

import (
	"context"
	"time"

	"github.com/google/uuid"
	"go.uber.org/zap"
)

// suspensionSweepInterval is how often the sweeper re-checks for temporary blocks
// that lapsed. A minute is well under the coarsest block grain (operators pick day
// presets) while keeping the query trivial.
const suspensionSweepInterval = time.Minute

// suspensionExpiryQuerier is the slice of the account store the sweeper depends on:
// the accounts whose temporary block lapsed in a window. *Store satisfies it; a fake
// drives the sweeper's unit tests.
type suspensionExpiryQuerier interface {
	SuspensionsExpiredBetween(ctx context.Context, since, until time.Time) ([]uuid.UUID, error)
}

// SuspensionSweeper re-evaluates chat write access when a temporary block self-
// expires. No operator action fires on expiry — the suspension gate just re-reads
// the wall clock — so without this a temporarily blocked player would stay muted in
// the moderated discussion chat after their block lapsed. Each tick it finds blocks
// that expired since the previous tick and calls onExpire for the affected accounts;
// onExpire is wired to publish the chat-access-changed event, after which the gateway
// re-resolves the true eligibility. A liberal call (an account that still has another
// active block) is therefore harmless. The window is in-memory, so a block that
// expires while the process is down is not re-granted until the next operator action
// or the player rejoins — an accepted best-effort gap.
type SuspensionSweeper struct {
	store    suspensionExpiryQuerier
	onExpire func(accountID uuid.UUID)
	log      *zap.Logger
	// since is the upper bound of the previous swept window; the next sweep covers
	// (since, now]. It advances only on a successful query, so a failed tick retries
	// the same window rather than dropping expiries.
	since time.Time
}

// NewSuspensionSweeper builds the sweeper over the account store, the per-account
// expiry callback (publishing the chat-access-changed event) and a logger. The first
// window opens at construction time, so blocks that lapsed earlier are not re-emitted.
func NewSuspensionSweeper(store *Store, onExpire func(accountID uuid.UUID), log *zap.Logger) *SuspensionSweeper {
	if log == nil {
		log = zap.NewNop()
	}
	return &SuspensionSweeper{store: store, onExpire: onExpire, log: log, since: time.Now().UTC()}
}

// Interval reports the sweep cadence, for the startup log line.
func (w *SuspensionSweeper) Interval() time.Duration { return suspensionSweepInterval }

// Run sweeps every Interval until ctx is cancelled.
func (w *SuspensionSweeper) Run(ctx context.Context) {
	ticker := time.NewTicker(suspensionSweepInterval)
	defer ticker.Stop()
	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			w.sweep(ctx)
		}
	}
}

// sweep emits a chat-access-changed signal for every account whose temporary block
// lapsed in (since, now], then advances the window. On a query error it keeps the
// window so the next tick retries it.
func (w *SuspensionSweeper) sweep(ctx context.Context) {
	now := time.Now().UTC()
	ids, err := w.store.SuspensionsExpiredBetween(ctx, w.since, now)
	if err != nil {
		w.log.Warn("suspension expiry sweep failed", zap.Error(err))
		return
	}
	w.since = now
	for _, id := range ids {
		w.onExpire(id)
	}
}
