package admin

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

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

const sessionLifetime = 30 * 24 * time.Hour

type LoginInput struct {
	Username string `json:"username"`
	Password string `json:"password"`
}

type LoginResult struct {
	User      User      `json:"user"`
	ExpiresAt time.Time `json:"expiresAt"`
	Token     string    `json:"-"`
}

func (s *Store) CreateOrUpdateUser(ctx context.Context, username string, password string) (User, error) {
	if username == "" {
		return User{}, errors.New("username is required")
	}
	passwordHash, err := HashPassword(password)
	if err != nil {
		return User{}, err
	}

	user, err := scanUser(s.db.QueryRow(ctx, `
INSERT INTO users (username, password_hash)
VALUES ($1, $2)
ON CONFLICT (username)
DO UPDATE SET password_hash = excluded.password_hash, updated_at = now()
RETURNING id, username, created_at, updated_at, last_login_at`, username, passwordHash))
	if err != nil {
		return User{}, fmt.Errorf("create or update user: %w", err)
	}
	return user, nil
}

func (s *Store) Login(ctx context.Context, input LoginInput) (LoginResult, error) {
	if input.Username == "" || input.Password == "" {
		return LoginResult{}, ErrInvalidCredentials
	}

	var user User
	var passwordHash string
	var lastLoginAt sql.NullTime
	err := s.db.QueryRow(ctx, `
SELECT id, username, password_hash, created_at, updated_at, last_login_at
FROM users
WHERE username = $1`, input.Username).Scan(
		&user.ID,
		&user.Username,
		&passwordHash,
		&user.CreatedAt,
		&user.UpdatedAt,
		&lastLoginAt,
	)
	if errors.Is(err, pgx.ErrNoRows) {
		return LoginResult{}, ErrInvalidCredentials
	}
	if err != nil {
		return LoginResult{}, fmt.Errorf("find user: %w", err)
	}
	user.LastLoginAt = nullTimePtr(lastLoginAt)

	if !CheckPassword(passwordHash, input.Password) {
		return LoginResult{}, ErrInvalidCredentials
	}

	token, err := NewSessionToken()
	if err != nil {
		return LoginResult{}, err
	}
	loginAt := time.Now()
	expiresAt := loginAt.Add(sessionLifetime)

	tx, err := s.db.Begin(ctx)
	if err != nil {
		return LoginResult{}, fmt.Errorf("begin login: %w", err)
	}
	defer tx.Rollback(ctx)

	if _, err := tx.Exec(ctx, `
UPDATE users
SET last_login_at = now(), updated_at = now()
WHERE id = $1`, user.ID); err != nil {
		return LoginResult{}, fmt.Errorf("update last login: %w", err)
	}
	if _, err := tx.Exec(ctx, `
INSERT INTO admin_sessions (user_id, token_hash, expires_at)
VALUES ($1, $2, $3)`, user.ID, SessionTokenHash(token), expiresAt); err != nil {
		return LoginResult{}, fmt.Errorf("create session: %w", err)
	}
	if err := tx.Commit(ctx); err != nil {
		return LoginResult{}, fmt.Errorf("commit login: %w", err)
	}

	user.LastLoginAt = &loginAt
	return LoginResult{
		User:      user,
		ExpiresAt: expiresAt,
		Token:     token,
	}, nil
}

func (s *Store) UserBySessionToken(ctx context.Context, token string) (User, error) {
	if token == "" {
		return User{}, ErrInvalidCredentials
	}

	user, err := scanUser(s.db.QueryRow(ctx, `
SELECT u.id, u.username, u.created_at, u.updated_at, u.last_login_at
FROM admin_sessions s
JOIN users u ON u.id = s.user_id
WHERE s.token_hash = $1 AND s.expires_at > now()`, SessionTokenHash(token)))
	if errors.Is(err, pgx.ErrNoRows) {
		return User{}, ErrInvalidCredentials
	}
	if err != nil {
		return User{}, fmt.Errorf("find session: %w", err)
	}

	_, _ = s.db.Exec(ctx, `
UPDATE admin_sessions
SET last_seen_at = now()
WHERE token_hash = $1`, SessionTokenHash(token))

	return user, nil
}

func (s *Store) Logout(ctx context.Context, token string) error {
	if token == "" {
		return nil
	}
	if _, err := s.db.Exec(ctx, `
DELETE FROM admin_sessions
WHERE token_hash = $1`, SessionTokenHash(token)); err != nil {
		return fmt.Errorf("delete session: %w", err)
	}
	return nil
}

func scanUser(row postScanner) (User, error) {
	var user User
	var lastLoginAt sql.NullTime
	err := row.Scan(
		&user.ID,
		&user.Username,
		&user.CreatedAt,
		&user.UpdatedAt,
		&lastLoginAt,
	)
	if err != nil {
		return User{}, err
	}
	user.LastLoginAt = nullTimePtr(lastLoginAt)
	return user, nil
}
