<?php
declare(strict_types=1);

namespace Celena\Core\Security;

use Celena\Core\Database\Connection;

/**
 * Аутентификация через сессию + remember-me cookie.
 * Хранение пользователей: таблица celena_users (создаётся install.php / миграциями).
 */
final class Auth
{
    private const SESSION_KEY = '_celena_user_id';
    private const REMEMBER_COOKIE = 'celena_remember';
    // Argon2id-хеш строки, которая никогда не равна корректному паролю.
    // Используется только для выравнивания тайминга при отсутствующем пользователе.
    private const DUMMY_HASH = '$argon2id$v=19$m=65536,t=4,p=1$YmJhYmFiYWJhYmFiYWJhYg$nE6V0V3PZRBFSMzj0d8h9N7d4XXqdq9k5hQOI7pq3lk';

    private ?array $user = null;
    private bool $resolved = false;

    public function __construct(private readonly Connection $conn)
    {
    }

    public function attempt(string $email, string $password, bool $remember = false): bool
    {
        $user = $this->conn->builder('celena_users')
            ->where('email', '=', strtolower($email))
            ->where('is_active', '=', true)
            ->first();
        if ($user === null) {
            // Защита от user-enumeration по таймингу: всегда выполняем верификацию.
            // Argon2id-хеш заведомо несовпадающего пароля держит общую длительность ответа близкой
            // к успешной верификации, чтобы атакующий не мог отличить «нет пользователя»
            // от «пароль неверный».
            Password::verify($password, self::DUMMY_HASH);
            return false;
        }
        if (!Password::verify($password, (string) $user['password_hash'])) {
            return false;
        }
        if (Password::needsRehash((string) $user['password_hash'])) {
            $this->conn->builder('celena_users')
                ->where('id', '=', $user['id'])
                ->update(['password_hash' => Password::hash($password)]);
        }
        $this->login((int) $user['id'], $remember);
        return true;
    }

    public function login(int $userId, bool $remember = false): void
    {
        if (PHP_SAPI === 'cli') {
            return;
        }
        if (session_status() === PHP_SESSION_ACTIVE) {
            session_regenerate_id(true);
            $_SESSION[self::SESSION_KEY] = $userId;
        }
        Csrf::rotate();
        if ($remember) {
            $this->setRememberCookie($userId);
        }
        $this->user = null;
        $this->resolved = false;
    }

    public function logout(): void
    {
        if (PHP_SAPI === 'cli') {
            return;
        }
        if (session_status() === PHP_SESSION_ACTIVE) {
            $_SESSION = [];
            session_regenerate_id(true);
        }
        if (isset($_COOKIE[self::REMEMBER_COOKIE])) {
            $token = (string) $_COOKIE[self::REMEMBER_COOKIE];
            [$selector] = explode(':', $token . ':', 2);
            if ($selector !== '') {
                $this->conn->builder('celena_remember_tokens')
                    ->where('selector', '=', $selector)
                    ->delete();
            }
            setcookie(self::REMEMBER_COOKIE, '', [
                'expires' => time() - 3600,
                'path' => '/',
                'secure' => $this->isHttps(),
                'httponly' => true,
                'samesite' => 'Lax',
            ]);
        }
        $this->user = null;
        $this->resolved = true;
    }

    public function check(): bool
    {
        return $this->user() !== null;
    }

    public function id(): ?int
    {
        $u = $this->user();
        return $u === null ? null : (int) $u['id'];
    }

    /** @return array<string, mixed>|null */
    public function user(): ?array
    {
        if ($this->resolved) {
            return $this->user;
        }
        $this->resolved = true;
        $userId = null;
        if (PHP_SAPI !== 'cli' && session_status() === PHP_SESSION_ACTIVE) {
            $userId = $_SESSION[self::SESSION_KEY] ?? null;
        }
        if ($userId === null && isset($_COOKIE[self::REMEMBER_COOKIE])) {
            $userId = $this->resolveRememberCookie((string) $_COOKIE[self::REMEMBER_COOKIE]);
            if ($userId !== null && session_status() === PHP_SESSION_ACTIVE) {
                $_SESSION[self::SESSION_KEY] = $userId;
            }
        }
        if ($userId === null) {
            return null;
        }
        $row = $this->conn->builder('celena_users')->where('id', '=', (int) $userId)->first();
        $this->user = $row ?: null;
        return $this->user;
    }

    private function setRememberCookie(int $userId): void
    {
        $selector = bin2hex(random_bytes(8));
        $validator = bin2hex(random_bytes(32));
        $hash = hash('sha256', $validator);
        $expiresAt = date('Y-m-d H:i:s', time() + 60 * 60 * 24 * 30);
        $this->conn->builder('celena_remember_tokens')->insert([
            'selector' => $selector,
            'token_hash' => $hash,
            'user_id' => $userId,
            'expires_at' => $expiresAt,
        ]);
        setcookie(self::REMEMBER_COOKIE, $selector . ':' . $validator, [
            'expires' => time() + 60 * 60 * 24 * 30,
            'path' => '/',
            'secure' => $this->isHttps(),
            'httponly' => true,
            'samesite' => 'Lax',
        ]);
    }

    /** HTTPS-запрос? Для Secure-флага cookie (за прокси учитываем X-Forwarded-Proto). */
    private function isHttps(): bool
    {
        return (($_SERVER['HTTPS'] ?? 'off') !== 'off')
            || (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https')
            || ((int) ($_SERVER['SERVER_PORT'] ?? 0) === 443);
    }

    private function resolveRememberCookie(string $cookie): ?int
    {
        if (!str_contains($cookie, ':')) {
            return null;
        }
        [$selector, $validator] = explode(':', $cookie, 2);
        $row = $this->conn->builder('celena_remember_tokens')
            ->where('selector', '=', $selector)
            ->first();
        if ($row === null) {
            return null;
        }
        if (strtotime((string) $row['expires_at']) < time()) {
            $this->conn->builder('celena_remember_tokens')->where('id', '=', $row['id'])->delete();
            return null;
        }
        if (!hash_equals((string) $row['token_hash'], hash('sha256', $validator))) {
            return null;
        }
        return (int) $row['user_id'];
    }
}
