<?php

namespace KeyLair;

use KeyLair\TwoFactor\TwoFactorManager;
use PDO;

class Auth
{
    private static ?array $user = null;

    /**
     * A method being enrolled (two_factor_method !== 'none') isn't the same
     * as it actually being in effect -- an admin can administratively
     * suspend another account's 2FA (two_factor_disabled_at set) without
     * clearing the enrolled method/secret, specifically so it can be
     * switched back on later without the account re-enrolling. Login-time
     * checks need the effective state; completeLogin()'s own "must set up
     * 2FA" check deliberately keeps reading the raw column instead (see
     * there) since a suspended account still has something to restore, not
     * nothing to set up.
     */
    private static function twoFactorActive(array $admin): bool
    {
        return $admin['two_factor_method'] !== 'none' && empty($admin['two_factor_disabled_at']);
    }

    /**
     * Step 1 of login: verify username-or-email + password.
     *
     * @return array{status: string, admin?: array} status is one of:
     *   'invalid'      — wrong username/email/password
     *   'inactive'     — account disabled
     *   'needs_2fa'    — password correct, must now verify a 2FA code
     *   'success'      — fully logged in (no 2FA configured)
     */
    public static function attemptPassword(string $usernameOrEmail, string $password): array
    {
        $pdo = Database::connection();
        $stmt = $pdo->prepare('SELECT * FROM admin_users WHERE username = :u1 OR email = :u2 LIMIT 1');
        $stmt->execute(['u1' => $usernameOrEmail, 'u2' => $usernameOrEmail]);
        $admin = $stmt->fetch();

        if (!$admin || !password_verify($password, $admin['password_hash'])) {
            // Constant-ish time regardless of whether the user exists.
            password_verify($password, '$2y$10$invalidsaltinvalidsaltinvalidsaltuO');
            return ['status' => 'invalid'];
        }

        if (!$admin['is_active']) {
            return ['status' => 'inactive'];
        }

        if (self::twoFactorActive($admin)) {
            $_SESSION['pending_2fa_admin_id'] = (int) $admin['id'];
            session_regenerate_id(true);
            return ['status' => 'needs_2fa'];
        }

        self::completeLogin($admin);
        return ['status' => 'success', 'admin' => $admin];
    }

    /** Step 2 of login (only reached if attemptPassword() returned 'needs_2fa'). */
    public static function attemptTwoFactor(string $code): bool
    {
        $adminId = $_SESSION['pending_2fa_admin_id'] ?? null;
        if (!$adminId) {
            return false;
        }

        $pdo = Database::connection();
        $stmt = $pdo->prepare('SELECT * FROM admin_users WHERE id = :id LIMIT 1');
        $stmt->execute(['id' => $adminId]);
        $admin = $stmt->fetch();

        if (!$admin || !self::twoFactorActive($admin)) {
            return false;
        }

        $provider = TwoFactorManager::get($admin['two_factor_method']);
        if (!$provider) {
            return false;
        }

        $secret = Encryption::decrypt($admin['two_factor_secret']);
        if ($secret === null || !$provider->verifyCode($secret, $code)) {
            return false;
        }

        unset($_SESSION['pending_2fa_admin_id']);
        self::completeLogin($admin);
        return true;
    }

    private static function completeLogin(array $admin): void
    {
        session_regenerate_id(true);
        $_SESSION['admin_id'] = (int) $admin['id'];
        self::$user = $admin;
        AuditLog::log((int) $admin['id'], 'login', null);

        // Demo accounts can never set up 2FA themselves (canEditOwnAccount()
        // blocks it), so exempt them -- everyone else must set it up before
        // reaching anything past account.php.
        if ($admin['two_factor_method'] === 'none' && $admin['role'] !== 'demo') {
            $_SESSION['must_setup_2fa'] = true;
        } else {
            unset($_SESSION['must_setup_2fa']);
        }
    }

    public static function logout(): void
    {
        if (self::check()) {
            AuditLog::log(self::user()['id'], 'logout', null);
        }
        $_SESSION = [];
        session_regenerate_id(true);
    }

    public static function check(): bool
    {
        return !empty($_SESSION['admin_id']);
    }

    public static function user(): ?array
    {
        if (self::$user !== null) {
            return self::$user;
        }

        if (empty($_SESSION['admin_id'])) {
            return null;
        }

        $pdo = Database::connection();
        $stmt = $pdo->prepare('SELECT * FROM admin_users WHERE id = :id AND is_active = 1 LIMIT 1');
        $stmt->execute(['id' => $_SESSION['admin_id']]);
        $admin = $stmt->fetch();

        if (!$admin) {
            self::logout();
            return null;
        }

        self::$user = $admin;
        I18n::setLocale($admin['language'] ?? null);
        return $admin;
    }

    public static function requireLogin(): array
    {
        $user = self::user();
        if (!$user) {
            Helpers::redirect('login.php');
        }

        if (!empty($_SESSION['must_setup_2fa'])) {
            $currentScript = basename($_SERVER['SCRIPT_NAME'] ?? '');
            if (!in_array($currentScript, ['account.php', 'logout.php'], true)) {
                Helpers::redirect('account.php');
            }
        }

        return $user;
    }

    public static function requireRole(string $role): array
    {
        $user = self::requireLogin();
        if ($user['role'] !== $role) {
            http_response_code(403);
            die('You do not have permission to view this page.');
        }
        return $user;
    }

    /**
     * Role hierarchy, least to most privileged. 'content_manager' is what
     * 'admin' always effectively was (everything except managing other
     * accounts) — kept as a separate, lower tier once 'admin' gained the
     * ability to manage content_manager/reviewer accounts, so the two
     * aren't identical roles under different names. 'demo' is even more
     * restricted than 'reviewer': same read-only access to everything, but
     * can't touch even their own account settings — a stable, shareable
     * login for a prospective buyer to click around in, that they also
     * can't accidentally (or deliberately) change the password on.
     */
    private const ROLE_LEVELS = [
        'demo' => 1,
        'reviewer' => 2,
        'content_manager' => 3,
        'admin' => 4,
        'master_admin' => 5,
    ];

    public static function isMasterAdmin(): bool
    {
        $user = self::user();
        return $user && $user['role'] === 'master_admin';
    }

    public static function roleLevel(): int
    {
        $user = self::user();
        return $user ? (self::ROLE_LEVELS[$user['role']] ?? 0) : 0;
    }

    /** master_admin and admin can manage content_manager/reviewer accounts (not each other). */
    public static function canManageUsers(): bool
    {
        return self::roleLevel() >= self::ROLE_LEVELS['admin'];
    }

    /** Everyone except reviewer/demo, who are read-only outside their own account. */
    public static function canEdit(): bool
    {
        return self::roleLevel() >= self::ROLE_LEVELS['content_manager'];
    }

    /** Everyone except demo can change their own profile/password/2FA — even a reviewer can manage their own account security. */
    public static function canEditOwnAccount(): bool
    {
        $user = self::user();
        return $user && $user['role'] !== 'demo';
    }

    /** Server-side enforcement that a demo account can't submit a change to its own profile/password/2FA. */
    public static function requireOwnAccountEditAccess(): array
    {
        $user = self::requireLogin();
        if (!self::canEditOwnAccount()) {
            http_response_code(403);
            die('Demo accounts cannot change their own login details.');
        }
        return $user;
    }

    /** Requires at least the given role level in the hierarchy above, e.g. requireMinRole('admin') also admits master_admin. */
    public static function requireMinRole(string $role): array
    {
        $user = self::requireLogin();
        $required = self::ROLE_LEVELS[$role] ?? PHP_INT_MAX;
        if (self::roleLevel() < $required) {
            http_response_code(403);
            die('You do not have permission to view this page.');
        }
        return $user;
    }

    /** Server-side enforcement that a reviewer can't submit an edit even if the UI somehow let them try. */
    public static function requireEditAccess(): array
    {
        $user = self::requireLogin();
        if (!self::canEdit()) {
            http_response_code(403);
            die('Your account has read-only access.');
        }
        return $user;
    }

    /**
     * Demo accounts can look around every admin-tier page (System Users,
     * Branding, Legal & Imprint, Migrations, Updates, Mail Server, API
     * Access) the same way they can everything else -- "quasi a
     * read-only account" -- but never submit anything there. Distinct
     * from canManageUsers(), which still gates the actual manage/edit
     * actions on those pages. Every one of those pages treats 'admin'
     * and 'master_admin' identically (full read/write, nothing gated any
     * tighter than "admin or above") -- there's no page in this app that
     * needs master_admin specifically and refuses plain admin.
     */
    public static function canViewAdminTier(): bool
    {
        $user = self::user();
        return $user !== null && ($user['role'] === 'demo' || self::canManageUsers());
    }

    public static function requireAdminTierView(): array
    {
        $user = self::requireLogin();
        if (!self::canViewAdminTier()) {
            http_response_code(403);
            die('You do not have permission to view this page.');
        }
        return $user;
    }

    public static function roleLabel(string $role): string
    {
        $key = [
            'master_admin' => 'role.master_admin',
            'admin' => 'role.admin',
            'content_manager' => 'role.content_manager',
            'reviewer' => 'role.reviewer',
            'demo' => 'role.demo',
        ][$role] ?? null;

        return $key !== null ? I18n::t($key) : $role;
    }
}
