package auth

import (
	"context"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"errors"
	"fmt"
	"time"

	"backend/internal/db/sqlc"

	"github.com/google/uuid"
	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgtype"
)

// Store manages session lifecycle: create, lookup, delete, rotate, and
// lazy-extend. It uses the sqlc-generated Queries for all DB operations.
//
// The raw 32-byte token is held only in memory (as the cookie value);
// the DB row stores hex(sha256(token)) so a DB-read leak does not expose
// live sessions (D-05).
type Store struct {
	q   *sqlc.Queries
	now func() time.Time // injectable for testing (D-09 MaybeExtend)
}

// NewStore returns a Store backed by q. The store's clock defaults to
// time.Now; tests may override store.now after construction.
func NewStore(q *sqlc.Queries) *Store {
	return &Store{q: q, now: time.Now}
}

// Create generates a fresh 32-byte crypto/rand token, stores its SHA-256 hash
// in the sessions table, and returns the base64url-encoded cookie value plus
// the session expiry time.
//
// This is the only place tokens are generated. The cookie value is the raw
// token (base64url); the DB stores hex(sha256(rawToken)) only (D-05).
func (s *Store) Create(ctx context.Context, userID uuid.UUID) (cookieValue string, expiresAt time.Time, err error) {
	raw := make([]byte, 32)
	if _, err = rand.Read(raw); err != nil {
		return "", time.Time{}, fmt.Errorf("auth: generate session token: %w", err)
	}

	cookieValue = base64.RawURLEncoding.EncodeToString(raw)
	sum := sha256.Sum256(raw) // D-05: store hash, never raw token
	id := hex.EncodeToString(sum[:])
	expiresAt = s.now().Add(SessionTTL)

	pgExpires := pgtype.Timestamptz{Time: expiresAt, Valid: true}
	if err = s.q.InsertSession(ctx, sqlc.InsertSessionParams{
		ID:        id,
		UserID:    userID,
		ExpiresAt: pgExpires,
	}); err != nil {
		return "", time.Time{}, fmt.Errorf("auth: insert session: %w", err)
	}
	return cookieValue, expiresAt, nil
}

// Lookup decodes the cookie value, hashes it, and retrieves the matching
// live session + user from the DB. Returns ErrSessionNotFound if the cookie
// is malformed, the session does not exist, or it has expired (D-07).
func (s *Store) Lookup(ctx context.Context, cookieValue string) (*Session, *User, error) {
	raw, err := base64.RawURLEncoding.DecodeString(cookieValue)
	if err != nil || len(raw) != 32 {
		return nil, nil, ErrSessionNotFound
	}

	sum := sha256.Sum256(raw) // D-05: hash on every lookup, never store raw token
	id := hex.EncodeToString(sum[:])
	row, err := s.q.GetSessionWithUser(ctx, id)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, nil, ErrSessionNotFound
		}
		return nil, nil, fmt.Errorf("auth: lookup session: %w", err)
	}

	sess := &Session{
		ID:        row.ID,
		UserID:    row.UserID,
		CreatedAt: row.CreatedAt.Time,
		ExpiresAt: row.ExpiresAt.Time,
	}
	user := &User{
		ID:           row.UID,
		Email:        row.Email,
		PasswordHash: row.PasswordHash,
		CreatedAt:    row.UCreatedAt.Time,
		UpdatedAt:    row.UUpdatedAt.Time,
	}
	return sess, user, nil
}

// Delete hard-deletes the session row by its hashed ID (D-06).
func (s *Store) Delete(ctx context.Context, id string) error {
	if err := s.q.DeleteSession(ctx, id); err != nil {
		return fmt.Errorf("auth: delete session: %w", err)
	}
	return nil
}

// Rotate deletes the old session row (best-effort) and creates a new one.
// This mitigates session fixation on every login and signup (D-10, T-2-04).
// If oldID is empty the delete step is skipped.
func (s *Store) Rotate(ctx context.Context, oldID string, userID uuid.UUID) (string, time.Time, error) {
	if oldID != "" {
		// Best-effort: ignore delete error; even if it fails the new session is safe.
		_ = s.q.DeleteSession(ctx, oldID)
	}
	return s.Create(ctx, userID)
}

// MaybeExtend updates expires_at only when the remaining session lifetime drops
// below SessionExtendThreshold (~7 days). This provides a sliding-window TTL
// with at most one DB write per ~23 days (D-09).
func (s *Store) MaybeExtend(ctx context.Context, id string, expiresAt time.Time) error {
	remaining := expiresAt.Sub(s.now())
	if remaining >= SessionExtendThreshold {
		// Plenty of time left — no update needed.
		return nil
	}

	newExpiry := s.now().Add(SessionTTL)
	if err := s.q.ExtendSession(ctx, sqlc.ExtendSessionParams{
		ID:        id,
		ExpiresAt: pgtype.Timestamptz{Time: newExpiry, Valid: true},
	}); err != nil {
		return fmt.Errorf("auth: extend session: %w", err)
	}
	return nil
}

