package store

import (
	"context"
	"crypto/rand"
	"errors"
	"fmt"

	"github.com/jackc/pgx/v5"
)

// ErrSubjectRegistered is returned by RegisterUser when the Dex subject already
// maps to a tapir user. Registration is explicit and once-per-subject (ADR-012).
var ErrSubjectRegistered = errors.New("store: subject already registered")

// UserBySubject resolves a Dex subject to its tapir user_id via the un-RLS'd
// user_identities map. It runs as a plain pool query WITHOUT withUser: this is
// the pre-scope lookup whose result becomes the GUC for every subsequent
// user-scoped access, so it cannot itself depend on that GUC being set. found is
// false (no error) when the subject has no mapping yet — the caller routes such
// requests to registration.
func (s *Store) UserBySubject(ctx context.Context, subject string) (userID string, found bool, err error) {
	err = s.pool.QueryRow(ctx,
		`SELECT user_id FROM user_identities WHERE dex_subject = $1`, subject).Scan(&userID)
	if errors.Is(err, pgx.ErrNoRows) {
		return "", false, nil
	}
	if err != nil {
		return "", false, fmt.Errorf("store: user by subject: %w", err)
	}
	return userID, true, nil
}

// UserIdentity is one (userID, dexSubject) pair from the un-RLS'd
// user_identities map — the unit the scheduler enumerates to run a discovery
// pass per user (ADR-018).
type UserIdentity struct {
	UserID     string
	DexSubject string
}

// ListAllUsers returns every (userID, dexSubject) pair from user_identities. It
// runs as a plain pool query WITHOUT withUser — intentional and legitimate:
// user_identities is un-RLS'd auth plumbing (like UserBySubject), and the
// scheduler enumerating all users to run their discovery passes is an admin
// operation that cannot be scoped to any single user. Order is unspecified.
func (s *Store) ListAllUsers(ctx context.Context) ([]UserIdentity, error) {
	rows, err := s.pool.Query(ctx,
		`SELECT user_id, dex_subject FROM user_identities`)
	if err != nil {
		return nil, fmt.Errorf("store: list all users: %w", err)
	}
	defer rows.Close()

	var users []UserIdentity
	for rows.Next() {
		var u UserIdentity
		if err := rows.Scan(&u.UserID, &u.DexSubject); err != nil {
			return nil, fmt.Errorf("store: scan user identity: %w", err)
		}
		users = append(users, u)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("store: iterate user identities: %w", err)
	}
	return users, nil
}

// RegisterUser creates the tapir user for a Dex subject and the identity mapping
// that points to it, returning the new user_id. It errors with
// ErrSubjectRegistered if the subject already maps.
//
// Bootstrapping note (generate-uuid-then-scope): the users table is FORCE'd RLS
// with a WITH CHECK that defaults to the USING predicate id =
// current_setting('tapir.current_user_id') (migration 003). A users row can
// therefore only be inserted while the connection is ALREADY scoped to that
// row's own id — a chicken-and-egg if the id were DB-generated. So we generate
// the UUID app-side, scope to it via withUser(newID, ...), and insert the users
// row inside that scope so the WITH CHECK passes. The user_identities row is
// un-RLS'd auth plumbing; it is written in the SAME transaction so a user and
// its mapping are always consistent.
func (s *Store) RegisterUser(ctx context.Context, subject, displayName string) (userID string, err error) {
	newID, err := newUUIDv4()
	if err != nil {
		return "", fmt.Errorf("store: register user: %w", err)
	}

	// Fast, clear rejection of a re-registration. The dex_subject PRIMARY KEY is
	// the authoritative guard (a concurrent insert would still violate it); this
	// check just turns the common case into a meaningful error instead of a raw
	// constraint violation.
	if _, found, err := s.UserBySubject(ctx, subject); err != nil {
		return "", err
	} else if found {
		return "", fmt.Errorf("%w: %q", ErrSubjectRegistered, subject)
	}

	if err := s.withUser(ctx, newID, func(tx pgx.Tx) error {
		if _, err := tx.Exec(ctx,
			`INSERT INTO users (id, display_name) VALUES ($1, $2)`, newID, displayName); err != nil {
			return fmt.Errorf("store: insert user: %w", err)
		}
		if _, err := tx.Exec(ctx,
			`INSERT INTO user_identities (dex_subject, user_id) VALUES ($1, $2)`,
			subject, newID); err != nil {
			return fmt.Errorf("store: insert identity: %w", err)
		}
		return nil
	}); err != nil {
		return "", err
	}
	return newID, nil
}

// newUUIDv4 returns a random RFC-4122 v4 UUID string. Generated app-side (stdlib
// crypto/rand, no new dependency) so the id is known before the row is scoped and
// inserted — see RegisterUser's bootstrapping note.
func newUUIDv4() (string, error) {
	var b [16]byte
	if _, err := rand.Read(b[:]); err != nil {
		return "", fmt.Errorf("generate uuid: %w", err)
	}
	b[6] = (b[6] & 0x0f) | 0x40 // version 4
	b[8] = (b[8] & 0x3f) | 0x80 // variant 10
	return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]), nil
}
