package account

import (
	"context"
	"database/sql"
	"fmt"
	"strings"
	"time"

	"github.com/google/uuid"
)

// UserListItem is the admin user-list projection: a small subset of the account plus
// whether it is a robot (derived from its identities), so the console can label the kind
// without a per-row identity query.
type UserListItem struct {
	ID                uuid.UUID
	DisplayName       string
	PreferredLanguage string
	IsGuest           bool
	IsRobot           bool
	// IsDeleted marks a tombstoned account (deleted_at set), shown as a badge — a search
	// spans both lists, so a result can be either live or deleted.
	IsDeleted bool
	// FlaggedHighRateAt is the soft high-rate marker (zero when unflagged), shown
	// as a badge in the console list.
	FlaggedHighRateAt time.Time
	CreatedAt         time.Time
}

// UserFilter narrows the admin user list: Robots selects robot accounts (otherwise the
// non-robot "people"); Deleted selects tombstoned accounts (every other scope hides them);
// NameMask and ExternalIDMask are glob masks ('*' = any run, '?' = one char) matched
// case-insensitively against the display name / any identity's external id; EmailExact is a
// strict (exact) match against an account's email identity. An empty value means no filter
// on that field.
type UserFilter struct {
	Robots         bool
	Deleted        bool
	NameMask       string
	ExternalIDMask string
	EmailExact     string
}

// robotExists is the correlated subquery testing whether account a is a robot.
const robotExists = `EXISTS (SELECT 1 FROM backend.identities i WHERE i.account_id = a.account_id AND i.kind = 'robot')`

// IsRobot reports whether the account is a robot pool member (it carries a robot
// identity). The admin console uses it to label a game's robot seats.
func (s *Store) IsRobot(ctx context.Context, accountID uuid.UUID) (bool, error) {
	var ok bool
	err := s.db.QueryRowContext(ctx,
		`SELECT EXISTS (SELECT 1 FROM backend.identities WHERE account_id = $1 AND kind = 'robot')`,
		accountID).Scan(&ok)
	if err != nil {
		return false, fmt.Errorf("account: is-robot %s: %w", accountID, err)
	}
	return ok, nil
}

// userListWhere builds the shared WHERE clause and its positional args (from $1). On the
// Robots tab it lists/searches robots only. Otherwise a search (any of the name /
// external-id / email filters) spans live and deleted people alike — never robots — so the
// operator finds a match from one query regardless of the People / Deleted tab; the search
// also looks in the retention journal, so a deleted account is still found by the email /
// external id it held (those rows moved from identities to retained_identities on deletion)
// and by its retained real name. With no search, the People / Deleted tab scope applies.
func userListWhere(f UserFilter) (string, []any) {
	name := LikePattern(f.NameMask)
	ext := LikePattern(f.ExternalIDMask)
	email := strings.ToLower(strings.TrimSpace(f.EmailExact))
	searching := name != "" || ext != "" || email != ""

	var args []any
	var where string
	switch {
	case f.Robots:
		where = robotExists + ` = true`
	case searching:
		where = robotExists + ` = false`
	case f.Deleted:
		where = robotExists + ` = false AND a.deleted_at IS NOT NULL`
	default:
		where = robotExists + ` = false AND a.deleted_at IS NULL`
	}
	if name != "" {
		args = append(args, name)
		where += fmt.Sprintf(` AND (a.display_name ILIKE $%d ESCAPE '\' OR a.deleted_display_name ILIKE $%d ESCAPE '\')`, len(args), len(args))
	}
	if ext != "" {
		args = append(args, ext)
		where += fmt.Sprintf(` AND (EXISTS (SELECT 1 FROM backend.identities i WHERE i.account_id = a.account_id AND i.external_id ILIKE $%d ESCAPE '\') OR EXISTS (SELECT 1 FROM backend.retained_identities r WHERE r.account_id = a.account_id AND r.external_id ILIKE $%d ESCAPE '\'))`, len(args), len(args))
	}
	if email != "" {
		args = append(args, email)
		where += fmt.Sprintf(` AND (EXISTS (SELECT 1 FROM backend.identities i WHERE i.account_id = a.account_id AND i.kind = 'email' AND i.external_id = $%d) OR EXISTS (SELECT 1 FROM backend.retained_identities r WHERE r.account_id = a.account_id AND r.kind = 'email' AND r.external_id = $%d))`, len(args), len(args))
	}
	return where, args
}

// ListUsers returns the filtered admin user list, newest first, paginated.
func (s *Store) ListUsers(ctx context.Context, f UserFilter, limit, offset int) ([]UserListItem, error) {
	where, args := userListWhere(f)
	q := `SELECT a.account_id, a.display_name, a.preferred_language, a.is_guest, a.flagged_high_rate_at, a.created_at, ` + robotExists + ` AS is_robot, (a.deleted_at IS NOT NULL) AS is_deleted
FROM backend.accounts a WHERE ` + where +
		fmt.Sprintf(` ORDER BY a.created_at DESC LIMIT $%d OFFSET $%d`, len(args)+1, len(args)+2)
	args = append(args, limit, offset)
	rows, err := s.db.QueryContext(ctx, q, args...)
	if err != nil {
		return nil, fmt.Errorf("account: list users: %w", err)
	}
	defer rows.Close()
	var out []UserListItem
	for rows.Next() {
		var it UserListItem
		var flagged sql.NullTime
		if err := rows.Scan(&it.ID, &it.DisplayName, &it.PreferredLanguage, &it.IsGuest, &flagged, &it.CreatedAt, &it.IsRobot, &it.IsDeleted); err != nil {
			return nil, fmt.Errorf("account: scan user: %w", err)
		}
		if flagged.Valid {
			it.FlaggedHighRateAt = flagged.Time
		}
		out = append(out, it)
	}
	return out, rows.Err()
}

// FlaggedAccount is one row of the console's high-rate review queue.
type FlaggedAccount struct {
	ID                uuid.UUID
	DisplayName       string
	FlaggedHighRateAt time.Time
}

// flaggedListCap bounds the console's flagged-account list; the operator clears
// flags as they are reviewed, so the queue stays short in practice.
const flaggedListCap = 200

// ListFlaggedHighRate returns the accounts carrying the high-rate flag, most
// recently flagged first.
func (s *Store) ListFlaggedHighRate(ctx context.Context) ([]FlaggedAccount, error) {
	rows, err := s.db.QueryContext(ctx,
		`SELECT account_id, display_name, flagged_high_rate_at
FROM backend.accounts WHERE flagged_high_rate_at IS NOT NULL
ORDER BY flagged_high_rate_at DESC LIMIT $1`, flaggedListCap)
	if err != nil {
		return nil, fmt.Errorf("account: list flagged: %w", err)
	}
	defer rows.Close()
	var out []FlaggedAccount
	for rows.Next() {
		var fa FlaggedAccount
		if err := rows.Scan(&fa.ID, &fa.DisplayName, &fa.FlaggedHighRateAt); err != nil {
			return nil, fmt.Errorf("account: scan flagged: %w", err)
		}
		out = append(out, fa)
	}
	return out, rows.Err()
}

// CountUsers counts the filtered admin user list, for pagination.
func (s *Store) CountUsers(ctx context.Context, f UserFilter) (int, error) {
	where, args := userListWhere(f)
	var n int
	if err := s.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM backend.accounts a WHERE `+where, args...).Scan(&n); err != nil {
		return 0, fmt.Errorf("account: count users: %w", err)
	}
	return n, nil
}

// LikePattern converts a glob mask ('*' any run, '?' one char) to an ILIKE pattern,
// escaping the SQL wildcards already in the input first. An empty/blank mask returns "".
func LikePattern(mask string) string {
	mask = strings.TrimSpace(mask)
	if mask == "" {
		return ""
	}
	escaped := strings.NewReplacer(`\`, `\\`, `%`, `\%`, `_`, `\_`).Replace(mask)
	escaped = strings.ReplaceAll(escaped, "*", "%")
	return strings.ReplaceAll(escaped, "?", "_")
}
