package admin

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

	"galaxy/backend/internal/postgres/jet/backend/model"
	"galaxy/backend/internal/postgres/jet/backend/table"

	"github.com/go-jet/jet/v2/postgres"
	"github.com/go-jet/jet/v2/qrm"
)

// adminAccountsPrimaryKey is the constraint name surfaced on the
// primary-key UNIQUE violation when a duplicate username is inserted.
// Postgres synthesises the constraint name as `<table>_pkey` for
// primary-key constraints, which matches the migration in
// `backend/internal/postgres/migrations/00001_init.sql:199`.
const adminAccountsPrimaryKey = "admin_accounts_pkey"

// Store is the Postgres-backed query surface for the admin package.
// Queries are built through go-jet against the generated table
// bindings under `backend/internal/postgres/jet/backend/table`.
type Store struct {
	db *sql.DB
}

// NewStore constructs a Store wrapping db.
func NewStore(db *sql.DB) *Store {
	return &Store{db: db}
}

// adminColumnList is the canonical projection used by every read path.
// The slice ordering matches the destination struct fields.
func adminColumnList() postgres.ColumnList {
	return postgres.ColumnList{
		table.AdminAccounts.Username,
		table.AdminAccounts.PasswordHash,
		table.AdminAccounts.CreatedAt,
		table.AdminAccounts.LastUsedAt,
		table.AdminAccounts.DisabledAt,
	}
}

// Lookup returns the admin row and its bcrypt hash for username.
// Returns ErrNotFound when no row matches.
func (s *Store) Lookup(ctx context.Context, username string) (Admin, []byte, error) {
	stmt := postgres.SELECT(adminColumnList()).
		FROM(table.AdminAccounts).
		WHERE(table.AdminAccounts.Username.EQ(postgres.String(username))).
		LIMIT(1)

	var row model.AdminAccounts
	if err := stmt.QueryContext(ctx, s.db, &row); err != nil {
		if errors.Is(err, qrm.ErrNoRows) {
			return Admin{}, nil, ErrNotFound
		}
		return Admin{}, nil, fmt.Errorf("admin store: lookup %q: %w", username, err)
	}
	admin, hash := modelToAdmin(row)
	return admin, hash, nil
}

// ListAll returns every admin row paired with its bcrypt hash, ordered
// by username ASC. Used by Cache.Warm and by the List handler (the
// hashes are dropped before the handler sends a response, but Warm
// needs them so Verify can match without a follow-up query).
func (s *Store) ListAll(ctx context.Context) ([]Admin, [][]byte, error) {
	stmt := postgres.SELECT(adminColumnList()).
		FROM(table.AdminAccounts).
		ORDER_BY(table.AdminAccounts.Username.ASC())

	var rows []model.AdminAccounts
	if err := stmt.QueryContext(ctx, s.db, &rows); err != nil {
		return nil, nil, fmt.Errorf("admin store: list: %w", err)
	}
	admins := make([]Admin, 0, len(rows))
	hashes := make([][]byte, 0, len(rows))
	for _, row := range rows {
		admin, hash := modelToAdmin(row)
		admins = append(admins, admin)
		hashes = append(hashes, hash)
	}
	return admins, hashes, nil
}

// Insert persists a fresh admin row. Returns ErrUsernameTaken when the
// primary-key UNIQUE constraint is violated (concurrent or repeat
// Create).
func (s *Store) Insert(ctx context.Context, username string, passwordHash []byte) (Admin, error) {
	stmt := table.AdminAccounts.
		INSERT(table.AdminAccounts.Username, table.AdminAccounts.PasswordHash).
		VALUES(username, passwordHash).
		RETURNING(adminColumnList())

	var row model.AdminAccounts
	if err := stmt.QueryContext(ctx, s.db, &row); err != nil {
		if isUniqueViolation(err, adminAccountsPrimaryKey) {
			return Admin{}, ErrUsernameTaken
		}
		return Admin{}, fmt.Errorf("admin store: insert %q: %w", username, err)
	}
	admin, _ := modelToAdmin(row)
	return admin, nil
}

// UpdatePasswordHash replaces the stored bcrypt hash for username.
// Returns ErrNotFound when no row matches.
func (s *Store) UpdatePasswordHash(ctx context.Context, username string, passwordHash []byte) (Admin, error) {
	stmt := table.AdminAccounts.
		UPDATE(table.AdminAccounts.PasswordHash).
		SET(passwordHash).
		WHERE(table.AdminAccounts.Username.EQ(postgres.String(username))).
		RETURNING(adminColumnList())

	var row model.AdminAccounts
	if err := stmt.QueryContext(ctx, s.db, &row); err != nil {
		if errors.Is(err, qrm.ErrNoRows) {
			return Admin{}, ErrNotFound
		}
		return Admin{}, fmt.Errorf("admin store: update password for %q: %w", username, err)
	}
	admin, _ := modelToAdmin(row)
	return admin, nil
}

// SetDisabledAt patches `disabled_at` for username. Pass `&time` to
// disable, `nil` to re-enable. Returns the refreshed Admin together
// with its bcrypt hash so the cache stays consistent. Returns
// ErrNotFound when no row matches.
func (s *Store) SetDisabledAt(ctx context.Context, username string, disabledAt *time.Time) (Admin, []byte, error) {
	var disabledExpr postgres.Expression
	if disabledAt != nil {
		disabledExpr = postgres.TimestampzT(*disabledAt)
	} else {
		disabledExpr = postgres.TimestampzExp(postgres.NULL)
	}
	stmt := table.AdminAccounts.
		UPDATE(table.AdminAccounts.DisabledAt).
		SET(disabledExpr).
		WHERE(table.AdminAccounts.Username.EQ(postgres.String(username))).
		RETURNING(adminColumnList())

	var row model.AdminAccounts
	if err := stmt.QueryContext(ctx, s.db, &row); err != nil {
		if errors.Is(err, qrm.ErrNoRows) {
			return Admin{}, nil, ErrNotFound
		}
		return Admin{}, nil, fmt.Errorf("admin store: set disabled_at for %q: %w", username, err)
	}
	admin, hash := modelToAdmin(row)
	return admin, hash, nil
}

// TouchLastUsed bumps last_used_at on a successful Verify. The caller
// runs the update fire-and-forget; errors are returned for logging
// but never propagated to the request.
func (s *Store) TouchLastUsed(ctx context.Context, username string, now time.Time) error {
	stmt := table.AdminAccounts.
		UPDATE(table.AdminAccounts.LastUsedAt).
		SET(postgres.TimestampzT(now)).
		WHERE(table.AdminAccounts.Username.EQ(postgres.String(username)))
	if _, err := stmt.ExecContext(ctx, s.db); err != nil {
		return fmt.Errorf("admin store: touch last_used_at for %q: %w", username, err)
	}
	return nil
}

// BootstrapInsert inserts the seed admin row when no row with the
// supplied username exists. The boolean reports whether the insert
// happened (true) or was skipped because of an existing row (false).
//
// Idempotent across restarts: subsequent calls with the same username
// return false without modifying the password hash. Operators rotate
// the seed admin's password through `ResetPassword`, not by editing
// env vars and restarting.
func (s *Store) BootstrapInsert(ctx context.Context, username string, passwordHash []byte) (bool, error) {
	stmt := table.AdminAccounts.
		INSERT(table.AdminAccounts.Username, table.AdminAccounts.PasswordHash).
		VALUES(username, passwordHash).
		ON_CONFLICT(table.AdminAccounts.Username).
		DO_NOTHING()
	res, err := stmt.ExecContext(ctx, s.db)
	if err != nil {
		return false, fmt.Errorf("admin store: bootstrap insert %q: %w", username, err)
	}
	affected, err := res.RowsAffected()
	if err != nil {
		return false, fmt.Errorf("admin store: bootstrap rows-affected: %w", err)
	}
	return affected > 0, nil
}

// modelToAdmin projects a generated model row into the public Admin
// struct plus the raw password hash. The conversion centralises the
// pointer-copy of nullable timestamps so each method stays a one-liner.
func modelToAdmin(row model.AdminAccounts) (Admin, []byte) {
	admin := Admin{
		Username:  row.Username,
		CreatedAt: row.CreatedAt,
	}
	if row.LastUsedAt != nil {
		t := *row.LastUsedAt
		admin.LastUsedAt = &t
	}
	if row.DisabledAt != nil {
		t := *row.DisabledAt
		admin.DisabledAt = &t
	}
	return admin, row.PasswordHash
}
