<?php
declare(strict_types=1);

class WebAuthnController
{
    private static function make(): \lbuchs\WebAuthn\WebAuthn
    {
        // Prefer the admin-configured app_url over HTTP_HOST: a misconfigured
        // catch-all vhost (or a Host-header injected by an upstream proxy)
        // could otherwise let an attacker pre-bind credentials under a host
        // they control. Only fall back to HTTP_HOST when no app_url is set.
        $configured = Helper::getSetting('app_url', '');
        $host = '';
        if ($configured !== '') {
            $host = (string)parse_url($configured, PHP_URL_HOST);
        }
        if ($host === '') {
            $host = explode(':', (string)($_SERVER['HTTP_HOST'] ?? 'localhost'))[0];
        }
        return new \lbuchs\WebAuthn\WebAuthn(
            Helper::getSetting('app_name', 'Plenvo'),
            $host,
            ['none', 'packed', 'fido-u2f'],
            true  // useBase64UrlEncoding
        );
    }

    private static function b64uEncode(string $bin): string
    {
        return rtrim(strtr(base64_encode($bin), '+/', '-_'), '=');
    }

    private static function b64uDecode(string $s): string
    {
        return base64_decode(str_pad(strtr($s, '-_', '+/'), strlen($s) + (4 - strlen($s) % 4) % 4, '='));
    }

    // ── Registration: generate challenge ─────────────────────────────────────

    public static function registerChallenge(): void
    {
        header('Content-Type: application/json');
        if (!Auth::check()) { http_response_code(401); echo json_encode(['error' => 'Unauthorized']); exit; }

        $db   = Database::getInstance();
        $user = $db->fetch('SELECT id, username FROM users WHERE id = ?', [Auth::id()]);

        $existing   = $db->fetchAll('SELECT credential_id FROM webauthn_credentials WHERE user_id = ?', [Auth::id()]);
        $excludeIds = array_map(fn($c) => self::b64uDecode($c['credential_id']), $existing);

        $wa         = self::make();
        $createArgs = $wa->getCreateArgs(
            pack('N', (int)$user['id']),
            $user['username'],
            $user['username'],
            60,
            true,   // requireResidentKey
            true,   // requireUserVerification
            null,   // authenticatorAttachment: null = all
            $excludeIds
        );

        $_SESSION['webauthn_reg_challenge'] = $wa->getChallenge();
        echo json_encode($createArgs);
        exit;
    }

    // ── Registration: verify and store ───────────────────────────────────────

    public static function registerVerify(): void
    {
        header('Content-Type: application/json');
        if (!Auth::check()) { http_response_code(401); echo json_encode(['ok' => false]); exit; }

        $input = json_decode(file_get_contents('php://input'), true);
        // CSRF defence-in-depth: SameSite=Lax + WebAuthn origin binding already
        // block the obvious cross-origin paths, but keep the token check in
        // line with /webauthn/delete and the rest of the state-changing API.
        $csrf  = (is_array($input) ? ($input['csrf_token'] ?? '') : '')
              ?: ($_SERVER['HTTP_X_CSRF_TOKEN'] ?? '');
        if (!Auth::verifyCsrfToken((string)$csrf)) {
            http_response_code(403);
            echo json_encode(['ok' => false, 'error' => Lang::get('invalid_request')]); exit;
        }
        if (!$input || empty($_SESSION['webauthn_reg_challenge'])) {
            echo json_encode(['ok' => false, 'error' => Lang::get('invalid_request')]); exit;
        }

        try {
            $wa   = self::make();
            $data = $wa->processCreate(
                self::b64uDecode($input['response']['clientDataJSON']),
                self::b64uDecode($input['response']['attestationObject']),
                $_SESSION['webauthn_reg_challenge'],
                true,   // requireUserVerification
                true,   // requireUserPresent
                false   // failIfRootMismatch
            );
            unset($_SESSION['webauthn_reg_challenge']);

            $name = mb_substr(trim($input['name'] ?? ''), 0, 60) ?: Lang::get('webauthn_default_key_name');

            Database::getInstance()->execute(
                'INSERT INTO webauthn_credentials (user_id, credential_id, public_key, sign_count, name) VALUES (?,?,?,?,?)',
                [Auth::id(), self::b64uEncode($data->credentialId), $data->credentialPublicKey, (int)($data->signCount ?? 0), $name]
            );

            echo json_encode(['ok' => true]);
        } catch (\Throwable $e) {
            error_log('WebAuthn register: ' . $e->getMessage());
            echo json_encode(['ok' => false, 'error' => Lang::get('webauthn_register_error')]);
        }
        exit;
    }

    // ── Login: generate challenge ─────────────────────────────────────────────

    public static function loginChallenge(): void
    {
        header('Content-Type: application/json');

        $wa      = self::make();
        $getArgs = $wa->getGetArgs(
            [],     // empty = discoverable (usernameless)
            60,
            true,   // usb
            true,   // nfc
            false,  // ble
            false,  // hybrid
            false,  // internal (no platform authenticators)
            true    // requireUserVerification
        );

        $_SESSION['webauthn_login_challenge'] = $wa->getChallenge();
        echo json_encode($getArgs);
        exit;
    }

    // ── Login: verify assertion and create session ───────────────────────────

    public static function loginVerify(): void
    {
        header('Content-Type: application/json');

        // IP rate limit (shared with password login) — see security-audit #13.
        if (Auth::isRateLimited()) {
            echo json_encode(['ok' => false, 'error' => Lang::get('login_rate_limited')]); exit;
        }

        $input = json_decode(file_get_contents('php://input'), true);
        if (!$input || empty($_SESSION['webauthn_login_challenge'])) {
            Auth::recordIpFailure();
            echo json_encode(['ok' => false, 'error' => Lang::get('login_error')]); exit;
        }

        $credentialId = $input['id'] ?? ''; // base64url from browser

        $db         = Database::getInstance();
        $credential = $db->fetch(
            'SELECT wc.*, u.id AS uid, u.language, u.theme, u.is_active, u.email_verify_token
             FROM webauthn_credentials wc
             JOIN users u ON u.id = wc.user_id
             WHERE wc.credential_id = ?',
            [$credentialId]
        );

        if (!$credential || !$credential['is_active']) {
            Auth::recordIpFailure();
            SecurityLog::log('login_failure', SecurityLog::SEV_WARN, ['method' => 'webauthn', 'reason' => 'unknown_or_inactive_credential'], $credential['uid'] ?? null);
            echo json_encode(['ok' => false, 'error' => Lang::get('login_error')]); exit;
        }
        if (!empty($credential['email_verify_token'])) {
            Auth::recordIpFailure();
            SecurityLog::log('login_failure', SecurityLog::SEV_WARN, ['method' => 'webauthn', 'reason' => 'email_unverified'], (int)$credential['uid']);
            echo json_encode(['ok' => false, 'error' => Lang::get('verify_email_required')]); exit;
        }

        try {
            $wa = self::make();
            $wa->processGet(
                self::b64uDecode($input['response']['clientDataJSON']),
                self::b64uDecode($input['response']['authenticatorData']),
                self::b64uDecode($input['response']['signature']),
                $credential['public_key'],
                $_SESSION['webauthn_login_challenge'],
                (int)$credential['sign_count'],
                true    // requireUserVerification
            );
            unset($_SESSION['webauthn_login_challenge']);

            $db->execute(
                'UPDATE webauthn_credentials SET sign_count=?, last_used_at=datetime("now") WHERE id=?',
                [$wa->getSignatureCounter() ?? $credential['sign_count'], $credential['id']]
            );
            $db->execute('UPDATE users SET last_login=CURRENT_TIMESTAMP, inactive_warning_sent_at=NULL WHERE id=?', [$credential['uid']]);

            session_regenerate_id(true);
            $_SESSION['user_id'] = $credential['uid'];
            $_SESSION['lang']    = $credential['language'];
            $_SESSION['theme']   = $credential['theme'];
            Auth::stampSessionLogin();
            Auth::clearAccountFailures((int)$credential['uid']);

            SecurityLog::log('login_success', SecurityLog::SEV_INFO, ['method' => 'webauthn', 'credential_id' => (int)$credential['id']], (int)$credential['uid']);
            echo json_encode(['ok' => true, 'redirect' => url('dashboard')]);
        } catch (\Throwable $e) {
            Auth::recordIpFailure();
            error_log('WebAuthn login: ' . $e->getMessage());
            SecurityLog::log('login_failure', SecurityLog::SEV_WARN, ['method' => 'webauthn', 'reason' => 'crypto_verification_failed'], (int)$credential['uid']);
            echo json_encode(['ok' => false, 'error' => Lang::get('login_error')]);
        }
        exit;
    }

    // ── Delete a credential ───────────────────────────────────────────────────

    public static function delete(): void
    {
        header('Content-Type: application/json');
        if (!Auth::check() || !Auth::verifyCsrfToken($_POST['csrf_token'] ?? '')) {
            echo json_encode(['ok' => false]); exit;
        }
        $credId = (int)($_POST['credential_id'] ?? 0);
        SecurityLog::log('webauthn_delete', SecurityLog::SEV_WARN, ['credential_id' => $credId]);
        Database::getInstance()->execute(
            'DELETE FROM webauthn_credentials WHERE id=? AND user_id=?',
            [$credId, Auth::id()]
        );
        echo json_encode(['ok' => true]);
        exit;
    }
}
