package store

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"time"
)

// SetSetupToken inserts a row, replacing any existing token for
// this user (single-outstanding invariant). Caller passes a hash —
// raw tokens are never persisted.
func (s *Store) SetSetupToken(ctx context.Context, t SetupToken) error {
	_, err := s.db.ExecContext(ctx,
		`INSERT OR REPLACE INTO user_setup_tokens
		   (user_id, token_hash, expires_at, created_at, created_by)
		 VALUES (?, ?, ?, ?, ?)`,
		t.UserID, t.TokenHash,
		t.ExpiresAt.UTC().Format(time.RFC3339Nano),
		t.CreatedAt.UTC().Format(time.RFC3339Nano),
		nullable(t.CreatedBy))
	if err != nil {
		return fmt.Errorf("store: set setup token: %w", err)
	}
	return nil
}

// LookupSetupToken resolves a token hash to its row. Returns
// ErrNotFound for missing tokens. Expiry is NOT checked here —
// callers must compare ExpiresAt themselves so they can record
// 'expired' as a distinct outcome (audit-able) from 'never existed'.
func (s *Store) LookupSetupToken(ctx context.Context, tokenHash string) (*SetupToken, error) {
	row := s.db.QueryRowContext(ctx,
		`SELECT user_id, token_hash, expires_at, created_at, created_by
		 FROM user_setup_tokens WHERE token_hash = ?`, tokenHash)
	return scanSetupToken(row.Scan)
}

// GetSetupTokenByUserID returns the row for one user. Used by the
// edit page to know whether a 'Regenerate setup link' button should
// show as 'Generate' or 'Regenerate'. Returns ErrNotFound when no
// outstanding token exists.
func (s *Store) GetSetupTokenByUserID(ctx context.Context, userID string) (*SetupToken, error) {
	row := s.db.QueryRowContext(ctx,
		`SELECT user_id, token_hash, expires_at, created_at, created_by
		 FROM user_setup_tokens WHERE user_id = ?`, userID)
	return scanSetupToken(row.Scan)
}

// DeleteSetupToken removes the row for a user (single-use cleanup
// after /setup completes successfully).
func (s *Store) DeleteSetupToken(ctx context.Context, userID string) error {
	_, err := s.db.ExecContext(ctx,
		`DELETE FROM user_setup_tokens WHERE user_id = ?`, userID)
	if err != nil {
		return fmt.Errorf("store: delete setup token: %w", err)
	}
	return nil
}

// CleanupExpiredSetupTokens removes rows whose expires_at has passed.
// Returns the number of rows deleted. Called from the maintenance
// ticker every minute.
func (s *Store) CleanupExpiredSetupTokens(ctx context.Context, now time.Time) (int64, error) {
	res, err := s.db.ExecContext(ctx,
		`DELETE FROM user_setup_tokens WHERE expires_at < ?`,
		now.UTC().Format(time.RFC3339Nano))
	if err != nil {
		return 0, fmt.Errorf("store: cleanup setup tokens: %w", err)
	}
	n, _ := res.RowsAffected()
	return n, nil
}

func scanSetupToken(scan func(...any) error) (*SetupToken, error) {
	var t SetupToken
	var createdBy sql.NullString
	var expiresAt, createdAt string
	if err := scan(&t.UserID, &t.TokenHash, &expiresAt, &createdAt, &createdBy); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, ErrNotFound
		}
		return nil, fmt.Errorf("store: scan setup token: %w", err)
	}
	t.ExpiresAt, _ = time.Parse(time.RFC3339Nano, expiresAt)
	t.CreatedAt, _ = time.Parse(time.RFC3339Nano, createdAt)
	if createdBy.Valid {
		v := createdBy.String
		t.CreatedBy = &v
	}
	return &t, nil
}
