package account

import (
	"context"
	"fmt"
	"time"

	"github.com/go-jet/jet/v2/postgres"
	"go.uber.org/zap"

	"scrabble/backend/internal/postgres/jet/backend/table"
)

// ReapAbandonedGuests deletes guest accounts created before olderThan that are
// not seated in any game. It returns the number deleted.
//
// Scope is deliberately "no game seat at all", not merely "no active game": a
// finished game belongs to the other players' history, and game_players carries no
// ON DELETE CASCADE to accounts (docs/ARCHITECTURE.md §4), so a guest with any seat
// is retained (and a delete would be blocked by that foreign key regardless). The
// dependent rows of a reaped guest — sessions, identities, account_stats — fall
// away through their own ON DELETE CASCADE foreign keys. Account age is the
// abandonment signal because sessions are revoke-only with no maintained
// last_seen_at, so a lingering session never expires on its own.
func (s *Store) ReapAbandonedGuests(ctx context.Context, olderThan time.Time) (int64, error) {
	stmt := table.Accounts.DELETE().WHERE(
		table.Accounts.IsGuest.EQ(postgres.Bool(true)).
			AND(table.Accounts.CreatedAt.LT(postgres.TimestampzT(olderThan))).
			AND(postgres.NOT(postgres.EXISTS(
				postgres.SELECT(table.GamePlayers.AccountID).
					FROM(table.GamePlayers).
					WHERE(table.GamePlayers.AccountID.EQ(table.Accounts.AccountID)),
			))),
	)
	res, err := stmt.ExecContext(ctx, s.db)
	if err != nil {
		return 0, fmt.Errorf("account: reap guests: %w", err)
	}
	n, err := res.RowsAffected()
	if err != nil {
		return 0, fmt.Errorf("account: reap guests rows affected: %w", err)
	}
	return n, nil
}

// GuestReaper periodically deletes abandoned guest accounts via
// Store.ReapAbandonedGuests. It mirrors the game turn-timeout sweeper and the
// matchmaker reaper: one background goroutine, started once from main.
type GuestReaper struct {
	store     *Store
	retention time.Duration
	clock     func() time.Time
	log       *zap.Logger
}

// NewGuestReaper constructs a reaper deleting guests whose account age exceeds
// retention. log may be nil.
func NewGuestReaper(store *Store, retention time.Duration, log *zap.Logger) *GuestReaper {
	if log == nil {
		log = zap.NewNop()
	}
	return &GuestReaper{
		store:     store,
		retention: retention,
		clock:     func() time.Time { return time.Now().UTC() },
		log:       log,
	}
}

// Run reaps abandoned guests on each tick until ctx is cancelled.
func (r *GuestReaper) Run(ctx context.Context, interval time.Duration) {
	ticker := time.NewTicker(interval)
	defer ticker.Stop()
	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			n, err := r.store.ReapAbandonedGuests(ctx, r.clock().Add(-r.retention))
			if err != nil {
				r.log.Warn("guest reap failed", zap.Error(err))
			} else if n > 0 {
				r.log.Info("reaped abandoned guests", zap.Int64("count", n))
			}
		}
	}
}
