package auth

import (
	"context"
	"errors"
	"time"

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

var ErrEmailTaken = errors.New("email is already registered")

type User struct {
	ID           string    `json:"id"`
	Email        string    `json:"email"`
	DisplayName  string    `json:"displayName"`
	Role         string    `json:"role"`
	PasswordHash string    `json:"-"`
	CreatedAt    time.Time `json:"createdAt"`
}

type Store interface {
	CreateUser(ctx context.Context, email, displayName, role, passwordHash string) (User, error)
	FindUserByEmail(ctx context.Context, email string) (User, error)
	CreateSession(ctx context.Context, tokenHash []byte, userID string, expiresAt time.Time) error
	FindUserBySession(ctx context.Context, tokenHash []byte, now time.Time) (User, error)
	DeleteSession(ctx context.Context, tokenHash []byte) error
}

type PostgresStore struct {
	pool *pgxpool.Pool
}

func NewPostgresStore(pool *pgxpool.Pool) *PostgresStore {
	return &PostgresStore{pool: pool}
}

func (store *PostgresStore) Migrate(ctx context.Context) error {
	_, err := store.pool.Exec(ctx, `
		CREATE TABLE IF NOT EXISTS users (
			id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
			email TEXT NOT NULL,
			display_name TEXT NOT NULL,
			role TEXT NOT NULL CHECK (role IN ('user', 'creator')),
			password_hash TEXT NOT NULL,
			created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
		);

		CREATE UNIQUE INDEX IF NOT EXISTS users_email_lower_idx
			ON users (LOWER(email));

		CREATE TABLE IF NOT EXISTS sessions (
			token_hash BYTEA PRIMARY KEY,
			user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
			expires_at TIMESTAMPTZ NOT NULL,
			created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
		);

		CREATE INDEX IF NOT EXISTS sessions_user_id_idx ON sessions(user_id);
		CREATE INDEX IF NOT EXISTS sessions_expires_at_idx ON sessions(expires_at);
	`)
	return err
}

func (store *PostgresStore) CreateUser(
	ctx context.Context,
	email string,
	displayName string,
	role string,
	passwordHash string,
) (User, error) {
	var user User
	err := store.pool.QueryRow(
		ctx,
		`INSERT INTO users (email, display_name, role, password_hash)
		 VALUES ($1, $2, $3, $4)
		 RETURNING id::text, email, display_name, role, password_hash, created_at`,
		email,
		displayName,
		role,
		passwordHash,
	).Scan(
		&user.ID,
		&user.Email,
		&user.DisplayName,
		&user.Role,
		&user.PasswordHash,
		&user.CreatedAt,
	)
	if err != nil {
		var postgresError *pgconn.PgError
		if errors.As(err, &postgresError) && postgresError.Code == "23505" {
			return User{}, ErrEmailTaken
		}
		return User{}, err
	}

	return user, nil
}

func (store *PostgresStore) FindUserByEmail(ctx context.Context, email string) (User, error) {
	var user User
	err := store.pool.QueryRow(
		ctx,
		`SELECT id::text, email, display_name, role, password_hash, created_at
		 FROM users
		 WHERE LOWER(email) = LOWER($1)`,
		email,
	).Scan(
		&user.ID,
		&user.Email,
		&user.DisplayName,
		&user.Role,
		&user.PasswordHash,
		&user.CreatedAt,
	)
	return user, err
}

func (store *PostgresStore) CreateSession(
	ctx context.Context,
	tokenHash []byte,
	userID string,
	expiresAt time.Time,
) error {
	_, err := store.pool.Exec(
		ctx,
		`INSERT INTO sessions (token_hash, user_id, expires_at) VALUES ($1, $2, $3)`,
		tokenHash,
		userID,
		expiresAt,
	)
	return err
}

func (store *PostgresStore) FindUserBySession(
	ctx context.Context,
	tokenHash []byte,
	now time.Time,
) (User, error) {
	var user User
	err := store.pool.QueryRow(
		ctx,
		`SELECT users.id::text, users.email, users.display_name, users.role,
		        users.password_hash, users.created_at
		 FROM sessions
		 JOIN users ON users.id = sessions.user_id
		 WHERE sessions.token_hash = $1 AND sessions.expires_at > $2`,
		tokenHash,
		now,
	).Scan(
		&user.ID,
		&user.Email,
		&user.DisplayName,
		&user.Role,
		&user.PasswordHash,
		&user.CreatedAt,
	)
	return user, err
}

func (store *PostgresStore) DeleteSession(ctx context.Context, tokenHash []byte) error {
	command, err := store.pool.Exec(ctx, `DELETE FROM sessions WHERE token_hash = $1`, tokenHash)
	if err != nil {
		return err
	}
	if command.RowsAffected() == 0 {
		return pgx.ErrNoRows
	}
	return nil
}
