<?php

declare(strict_types=1);

namespace Budget;

use PDO;

final class Auth
{
    public function __construct(private PDO $pdo, private Settings $settings)
    {
    }

    public function needsSetup(): bool
    {
        return (int) $this->pdo->query('SELECT COUNT(*) FROM users')->fetchColumn() === 0;
    }

    public function setup(string $email, string $password, bool $enableTwoFactor): ?string
    {
        if (!$this->needsSetup()) {
            throw new \RuntimeException('The administrator account already exists.');
        }
        $secret = $enableTwoFactor ? Totp::generateSecret() : null;
        $statement = $this->pdo->prepare(
            'INSERT INTO users (email, password_hash, totp_secret) VALUES (:email, :password_hash, :secret)'
        );
        $statement->execute([
            'email' => strtolower(trim($email)),
            'password_hash' => password_hash($password, PASSWORD_DEFAULT),
            'secret' => $secret,
        ]);
        $this->settings->set('alerts.email', strtolower(trim($email)));
        $this->settings->set('security.2fa_required', $enableTwoFactor);
        $_SESSION['user_id'] = (int) $this->pdo->lastInsertId();
        session_regenerate_id(true);
        return $secret;
    }

    public function attempt(string $email, string $password): string
    {
        $statement = $this->pdo->prepare('SELECT * FROM users WHERE email = :email LIMIT 1');
        $statement->execute(['email' => strtolower(trim($email))]);
        $user = $statement->fetch();
        if (!$user || !password_verify($password, (string) $user['password_hash'])) {
            return 'invalid';
        }

        if ((bool) $this->settings->get('security.2fa_required', false)) {
            if (empty($user['totp_secret'])) {
                return 'unconfigured_2fa';
            }
            $_SESSION['pending_2fa_user'] = (int) $user['id'];
            return '2fa';
        }

        $this->completeLogin((int) $user['id']);
        return 'ok';
    }

    public function verifyTwoFactor(string $code): bool
    {
        $userId = (int) ($_SESSION['pending_2fa_user'] ?? 0);
        if ($userId === 0) {
            return false;
        }
        $statement = $this->pdo->prepare('SELECT totp_secret FROM users WHERE id = :id');
        $statement->execute(['id' => $userId]);
        $secret = (string) $statement->fetchColumn();
        if ($secret === '' || !Totp::verify($secret, $code)) {
            return false;
        }
        unset($_SESSION['pending_2fa_user']);
        $this->completeLogin($userId);
        return true;
    }

    public function check(): bool
    {
        return isset($_SESSION['user_id']);
    }

    public function id(): ?int
    {
        return $this->check() ? (int) $_SESSION['user_id'] : null;
    }

    public function user(): ?array
    {
        if (!$this->check()) {
            return null;
        }
        $statement = $this->pdo->prepare('SELECT id, email, totp_secret, created_at FROM users WHERE id = :id');
        $statement->execute(['id' => $this->id()]);
        return $statement->fetch() ?: null;
    }

    public function logout(): void
    {
        $_SESSION = [];
        if (ini_get('session.use_cookies')) {
            $parameters = session_get_cookie_params();
            setcookie(session_name(), '', time() - 42000, $parameters['path'], $parameters['domain'], $parameters['secure'], $parameters['httponly']);
        }
        session_destroy();
    }

    private function completeLogin(int $userId): void
    {
        $_SESSION['user_id'] = $userId;
        session_regenerate_id(true);
    }
}
