<?php

declare(strict_types=1);

final class Auth
{
    private static ?array $user = null;
    public static function start(): void
    {
        if (session_status() === PHP_SESSION_ACTIVE) {
            return;
        }
        ini_set('session.use_strict_mode', '1');
        ini_set('session.use_only_cookies', '1');
        ini_set('session.cookie_httponly', '1');
        session_name('globinours_session');
        session_set_cookie_params([
            'httponly' => true,
            'secure' => self::isSecureRequest(),
            'samesite' => 'Strict',
            'path' => '/',
        ]);
        session_start();
        $now = time();
        $idle = max(300, (int) (getenv('GLOBINOURS_SESSION_IDLE') ?: 1800));
        $absolute = max($idle, (int) (getenv('GLOBINOURS_SESSION_ABSOLUTE') ?: 43200));
        if (
            !empty($_SESSION['user_id']) &&
            ((isset($_SESSION['last_activity']) && $now - (int) $_SESSION['last_activity'] > $idle) ||
                (isset($_SESSION['authenticated_at']) && $now - (int) $_SESSION['authenticated_at'] > $absolute))
        ) {
            self::logout(false);
            session_regenerate_id(true);
        }
        if (
            empty($_SESSION['user_id']) &&
            class_exists(TrustedDeviceService::class) &&
            ($id = TrustedDeviceService::authenticate())
        ) {
            session_regenerate_id(true);
            $_SESSION['user_id'] = $id;
            $_SESSION['authenticated_at'] = $now;
        }
        if (!empty($_SESSION['user_id'])) {
            $_SESSION['last_activity'] = $now;
        }
    }
    public static function countUsers(): int
    {
        return (int) DB::pdo()->query('SELECT COUNT(*) FROM users')->fetchColumn();
    }
    public static function user(): ?array
    {
        if (self::$user !== null) {
            return self::$user;
        }
        $id = (int) ($_SESSION['user_id'] ?? 0);
        if (!$id) {
            return null;
        }
        $s = DB::pdo()->prepare(
            'SELECT id,username,display_name,role,active,last_login_at,animal_list_view,agenda_view FROM users WHERE id=:id AND active=1',
        );
        $s->execute([':id' => $id]);
        $u = $s->fetch(PDO::FETCH_ASSOC);
        if (!$u) {
            self::logout(false);
            return null;
        }
        return self::$user = $u;
    }
    public static function id(): ?int
    {
        return ($u = self::user()) ? (int) $u['id'] : null;
    }
    public static function login(
        string $username,
        string $password,
        bool $remember = false,
        string $deviceName = '',
    ): bool {
        $db = DB::pdo();
        $username = trim($username);
        $ip = mb_substr((string) ($_SERVER['REMOTE_ADDR'] ?? ''), 0, 64);
        $db->exec("DELETE FROM login_attempts WHERE attempted_at<datetime('now','-7 days')");
        $rate = $db->prepare(
            "SELECT COUNT(*) FROM login_attempts WHERE succeeded=0 AND ip_address=? AND attempted_at>=datetime('now','-15 minutes')",
        );
        $rate->execute([$ip ?: null]);
        if ((int) $rate->fetchColumn() >= 20) {
            self::recordAttempt($username, $ip, false);
            return false;
        }
        $s = $db->prepare('SELECT * FROM users WHERE lower(username)=lower(:u) LIMIT 1');
        $s->execute([':u' => $username]);
        $u = $s->fetch(PDO::FETCH_ASSOC);
        $valid =
            $u &&
            (int) $u['active'] === 1 &&
            (empty($u['locked_until']) || strtotime($u['locked_until']) <= time()) &&
            password_verify($password, (string) $u['password_hash']);
        if (!$u) {
            password_verify($password, '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2uheWG/igi.');
        }
        if (!$valid) {
            if ($u) {
                $failed = (int) $u['failed_attempts'] + 1;
                $lock = $failed >= 5 ? date('Y-m-d H:i:s', time() + 900) : null;
                $db->prepare('UPDATE users SET failed_attempts=:f,locked_until=:l WHERE id=:id')->execute([
                    ':f' => $failed,
                    ':l' => $lock,
                    ':id' => $u['id'],
                ]);
            }
            self::recordAttempt($username, $ip, false);
            return false;
        }
        session_regenerate_id(true);
        $_SESSION['user_id'] = (int) $u['id'];
        $_SESSION['authenticated_at'] = time();
        $_SESSION['last_activity'] = time();
        self::$user = null;
        $db->prepare(
            "UPDATE users SET failed_attempts=0,locked_until=NULL,last_login_at=datetime('now') WHERE id=:id",
        )->execute([':id' => $u['id']]);
        self::recordAttempt($username, $ip, true);
        if ($remember && class_exists(TrustedDeviceService::class)) {
            TrustedDeviceService::create((int) $u['id'], $deviceName);
        }
        return true;
    }
    public static function logout(bool $destroy = true): void
    {
        if ($destroy && class_exists(TrustedDeviceService::class)) {
            TrustedDeviceService::revokeCurrent();
        }
        self::$user = null;
        unset($_SESSION['user_id']);
        if ($destroy) {
            $_SESSION = [];
            if (ini_get('session.use_cookies')) {
                $p = session_get_cookie_params();
                setcookie(
                    session_name(),
                    '',
                    time() - 42000,
                    $p['path'],
                    $p['domain'] ?? '',
                    (bool) $p['secure'],
                    (bool) $p['httponly'],
                );
            }
            session_destroy();
        }
    }
    public static function csrf(): string
    {
        if (empty($_SESSION['csrf'])) {
            $_SESSION['csrf'] = bin2hex(random_bytes(32));
        }
        return (string) $_SESSION['csrf'];
    }
    public static function validCsrf(?string $token): bool
    {
        return is_string($token) && hash_equals(self::csrf(), $token);
    }
    public static function is(string $role): bool
    {
        return (self::user()['role'] ?? '') === $role;
    }
    public static function canWrite(string $path): bool
    {
        $module = PermissionService::moduleForPath($path);
        return $module === null || PermissionService::can($module, 'edit');
    }
    public static function isSecureRequest(): bool
    {
        if (!empty($_SERVER['HTTPS']) && strtolower((string) $_SERVER['HTTPS']) !== 'off') {
            return true;
        }
        if (str_starts_with(strtolower((string) getenv('APP_URL')), 'https://')) {
            return true;
        }
        $remote = (string) ($_SERVER['REMOTE_ADDR'] ?? '');
        $trusted = array_filter(array_map('trim', explode(',', (string) getenv('GLOBINOURS_TRUSTED_PROXIES'))));
        if (in_array($remote, ['127.0.0.1', '::1'], true) || in_array($remote, $trusted, true)) {
            return strtolower(trim(explode(',', (string) ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? ''))[0] ?? '')) ===
                'https';
        }
        return false;
    }
    private static function recordAttempt(string $username, string $ip, bool $success): void
    {
        try {
            DB::pdo()
                ->prepare('INSERT INTO login_attempts(username_hash,ip_address,succeeded) VALUES(?,?,?)')
                ->execute([hash('sha256', mb_strtolower($username)), $ip ?: null, $success ? 1 : 0]);
        } catch (Throwable) {
        }
    }
}
