<?php

if (!defined('ABSPATH')) {
    exit;
}

class BS2FA_WebAuthn {
    public static function registration_options($user_id) {
        $user = get_user_by('id', $user_id);
        if (!$user) {
            throw new RuntimeException('User not found');
        }

        $challenge = random_bytes(32);
        update_user_meta($user_id, 'bs2fa_webauthn_register_challenge', BS2FA_Helpers::base64url_encode($challenge));
        update_user_meta($user_id, 'bs2fa_webauthn_register_challenge_time', time());

        $credentials = self::get_credentials($user_id);
        $exclude = [];
        foreach ($credentials as $credential) {
            $exclude[] = [
                'type' => 'public-key',
                'id'   => $credential['id'],
            ];
        }

        return [
            'challenge' => BS2FA_Helpers::base64url_encode($challenge),
            'rp' => [
                'name' => wp_specialchars_decode(get_bloginfo('name'), ENT_QUOTES),
                'id'   => BS2FA_Helpers::rp_id(),
            ],
            'user' => [
                'id'          => BS2FA_Helpers::base64url_encode('wp-user-' . $user_id),
                'name'        => $user->user_login,
                'displayName' => $user->display_name ?: $user->user_login,
            ],
            'pubKeyCredParams' => [
                ['alg' => -7, 'type' => 'public-key'],
            ],
            'timeout' => 60000,
            'authenticatorSelection' => [
                'residentKey' => 'preferred',
                'userVerification' => BS2FA_Helpers::get_option('passkey_user_verification', 'required'),
            ],
            'attestation' => 'none',
            'excludeCredentials' => $exclude,
        ];
    }

    public static function verify_registration($user_id, $payload) {
        $challenge = get_user_meta($user_id, 'bs2fa_webauthn_register_challenge', true);
        $challenge_time = (int) get_user_meta($user_id, 'bs2fa_webauthn_register_challenge_time', true);
        if (!$challenge || (time() - $challenge_time) > 10 * MINUTE_IN_SECONDS) {
            throw new RuntimeException('Registration challenge expired.');
        }

        $response = isset($payload['response']) && is_array($payload['response']) ? $payload['response'] : [];
        $client_data_json = BS2FA_Helpers::base64url_decode($response['clientDataJSON'] ?? '');
        $attestation_object = BS2FA_Helpers::base64url_decode($response['attestationObject'] ?? '');
        $credential_id = sanitize_text_field($payload['id'] ?? '');
        $raw_id = BS2FA_Helpers::base64url_decode($payload['rawId'] ?? '');

        if (!$client_data_json || !$attestation_object || !$credential_id || !$raw_id) {
            throw new RuntimeException('Invalid passkey registration payload.');
        }

        $client = json_decode($client_data_json, true);
        if (!is_array($client)) {
            throw new RuntimeException('Invalid passkey client data.');
        }

        if (($client['type'] ?? '') !== 'webauthn.create') {
            throw new RuntimeException('Unexpected passkey registration type.');
        }

        if (!hash_equals($challenge, $client['challenge'] ?? '')) {
            throw new RuntimeException('Passkey registration challenge mismatch.');
        }

        if (!hash_equals(BS2FA_Helpers::current_origin(), $client['origin'] ?? '')) {
            throw new RuntimeException('Passkey registration origin mismatch.');
        }

        $decoded = BS2FA_CBOR::decode($attestation_object);
        if (!is_array($decoded) || empty($decoded['authData'])) {
            throw new RuntimeException('Invalid attestation object.');
        }

        $auth_data = $decoded['authData'];
        $parsed = self::parse_auth_data($auth_data, true);

        if (!hash_equals(hash('sha256', BS2FA_Helpers::rp_id(), true), $parsed['rpIdHash'])) {
            throw new RuntimeException('RP ID hash mismatch.');
        }

        if (($parsed['flags'] & 0x01) !== 0x01) {
            throw new RuntimeException('User presence flag missing.');
        }

        if (BS2FA_Helpers::get_option('passkey_user_verification', 'required') === 'required' && (($parsed['flags'] & 0x04) !== 0x04)) {
            throw new RuntimeException('This passkey did not confirm user verification.');
        }

        if (!hash_equals($parsed['credentialId'], $raw_id)) {
            throw new RuntimeException('Credential ID mismatch during passkey registration.');
        }

        $credential = [
            'id'          => $credential_id,
            'public_key'  => self::cose_to_pem($parsed['credentialPublicKey']),
            'sign_count'  => $parsed['signCount'],
            'aaguid'      => BS2FA_Helpers::base64url_encode($parsed['aaguid']),
            'added_at'    => time(),
            'label'       => BS2FA_Helpers::sanitize_device_label($payload['label'] ?? '', __('Passkey', 'brightery-secure-2fa')),
            'last_used_at'=> 0,
        ];

        $credentials = self::get_credentials($user_id);
        foreach ($credentials as $existing) {
            if ($existing['id'] === $credential['id']) {
                throw new RuntimeException('This passkey is already registered.');
            }
        }

        $credentials[] = $credential;
        update_user_meta($user_id, 'bs2fa_webauthn_credentials', $credentials);
        delete_user_meta($user_id, 'bs2fa_webauthn_register_challenge');
        delete_user_meta($user_id, 'bs2fa_webauthn_register_challenge_time');

        return true;
    }

    public static function assertion_options($user_id) {
        $user = get_user_by('id', $user_id);
        if (!$user) {
            throw new RuntimeException('User not found');
        }

        $credentials = self::get_credentials($user_id);
        if (empty($credentials)) {
            throw new RuntimeException('No passkeys are registered for this account.');
        }

        $challenge = random_bytes(32);
        $token = BS2FA_Helpers::get_login_pending();
        if (!$token || (int) $token['user_id'] !== (int) $user_id) {
            throw new RuntimeException('Login challenge not found.');
        }

        set_transient('bs2fa_assertion_' . $token['token'], BS2FA_Helpers::base64url_encode($challenge), 10 * MINUTE_IN_SECONDS);

        $allow = [];
        foreach ($credentials as $credential) {
            $allow[] = [
                'type' => 'public-key',
                'id'   => $credential['id'],
            ];
        }

        return [
            'challenge' => BS2FA_Helpers::base64url_encode($challenge),
            'rpId' => BS2FA_Helpers::rp_id(),
            'timeout' => 60000,
            'allowCredentials' => $allow,
            'userVerification' => BS2FA_Helpers::get_option('passkey_user_verification', 'required'),
        ];
    }

    public static function verify_assertion($user_id, $payload, $pending_token) {
        $stored_challenge = get_transient('bs2fa_assertion_' . $pending_token);
        if (!$stored_challenge) {
            throw new RuntimeException('Passkey login challenge expired.');
        }

        $response = isset($payload['response']) && is_array($payload['response']) ? $payload['response'] : [];
        $client_data_json = BS2FA_Helpers::base64url_decode($response['clientDataJSON'] ?? '');
        $authenticator_data = BS2FA_Helpers::base64url_decode($response['authenticatorData'] ?? '');
        $signature = BS2FA_Helpers::base64url_decode($response['signature'] ?? '');
        $credential_id = sanitize_text_field($payload['id'] ?? '');

        if (!$client_data_json || !$authenticator_data || !$signature || !$credential_id) {
            throw new RuntimeException('Invalid passkey assertion payload.');
        }

        $client = json_decode($client_data_json, true);
        if (!is_array($client)) {
            throw new RuntimeException('Invalid passkey assertion client data.');
        }

        if (($client['type'] ?? '') !== 'webauthn.get') {
            throw new RuntimeException('Unexpected passkey assertion type.');
        }

        if (!hash_equals($stored_challenge, $client['challenge'] ?? '')) {
            throw new RuntimeException('Passkey login challenge mismatch.');
        }

        if (!hash_equals(BS2FA_Helpers::current_origin(), $client['origin'] ?? '')) {
            throw new RuntimeException('Passkey login origin mismatch.');
        }

        $parsed = self::parse_assertion_auth_data($authenticator_data);
        if (!hash_equals(hash('sha256', BS2FA_Helpers::rp_id(), true), $parsed['rpIdHash'])) {
            throw new RuntimeException('Passkey RP ID hash mismatch.');
        }

        if (($parsed['flags'] & 0x01) !== 0x01) {
            throw new RuntimeException('Passkey user presence was not confirmed.');
        }

        if (BS2FA_Helpers::get_option('passkey_user_verification', 'required') === 'required' && (($parsed['flags'] & 0x04) !== 0x04)) {
            throw new RuntimeException('Passkey user verification is required.');
        }

        $credentials = self::get_credentials($user_id);
        $found_index = null;
        $credential = null;
        foreach ($credentials as $index => $item) {
            if ($item['id'] === $credential_id) {
                $found_index = $index;
                $credential = $item;
                break;
            }
        }

        if (!$credential) {
            throw new RuntimeException('Passkey credential was not found.');
        }

        $signed_data = $authenticator_data . hash('sha256', $client_data_json, true);
        $result = openssl_verify($signed_data, $signature, $credential['public_key'], OPENSSL_ALGO_SHA256);
        if (1 !== $result) {
            throw new RuntimeException('Passkey signature verification failed.');
        }

        $new_count = (int) $parsed['signCount'];
        $old_count = (int) ($credential['sign_count'] ?? 0);
        if ($old_count > 0 && $new_count > 0 && $new_count <= $old_count) {
            throw new RuntimeException('Passkey signature counter did not advance.');
        }

        $credentials[$found_index]['sign_count'] = $new_count;
        $credentials[$found_index]['last_used_at'] = time();
        update_user_meta($user_id, 'bs2fa_webauthn_credentials', $credentials);
        delete_transient('bs2fa_assertion_' . $pending_token);

        return true;
    }

    public static function delete_credential($user_id, $credential_id) {
        $credentials = self::get_credentials($user_id);
        $credentials = array_values(array_filter($credentials, function ($credential) use ($credential_id) {
            return $credential['id'] !== $credential_id;
        }));
        update_user_meta($user_id, 'bs2fa_webauthn_credentials', $credentials);
    }


    public static function update_labels($user_id, $labels) {
        $credentials = self::get_credentials($user_id);
        if (empty($credentials) || !is_array($labels)) {
            return 0;
        }

        $changed = 0;
        foreach ($credentials as $index => $credential) {
            $id = (string) ($credential['id'] ?? '');
            if ($id === '' || !isset($labels[$id])) {
                continue;
            }
            $new_label = BS2FA_Helpers::sanitize_device_label($labels[$id], (string) ($credential['label'] ?? __('Passkey', 'brightery-secure-2fa')));
            if ($new_label !== (string) ($credential['label'] ?? '')) {
                $credentials[$index]['label'] = $new_label;
                $changed++;
            }
        }

        if ($changed > 0) {
            update_user_meta($user_id, 'bs2fa_webauthn_credentials', $credentials);
        }

        return $changed;
    }

    public static function get_credentials($user_id) {
        $credentials = get_user_meta($user_id, 'bs2fa_webauthn_credentials', true);
        return is_array($credentials) ? $credentials : [];
    }

    protected static function parse_auth_data($auth_data, $require_attested = false) {
        if (strlen($auth_data) < 37) {
            throw new RuntimeException('Authenticator data is too short.');
        }

        $offset = 0;
        $rp_id_hash = substr($auth_data, $offset, 32);
        $offset += 32;
        $flags = ord($auth_data[$offset]);
        $offset += 1;
        $sign_count = unpack('N', substr($auth_data, $offset, 4))[1];
        $offset += 4;

        $parsed = [
            'rpIdHash' => $rp_id_hash,
            'flags'    => $flags,
            'signCount'=> $sign_count,
        ];

        $has_attested = ($flags & 0x40) === 0x40;
        if ($require_attested && !$has_attested) {
            throw new RuntimeException('Authenticator data does not include attested credential data.');
        }

        if ($has_attested) {
            $aaguid = substr($auth_data, $offset, 16);
            $offset += 16;
            $cred_len = unpack('n', substr($auth_data, $offset, 2))[1];
            $offset += 2;
            $cred_id = substr($auth_data, $offset, $cred_len);
            $offset += $cred_len;
            $credential_public_key = substr($auth_data, $offset);

            $parsed['aaguid'] = $aaguid;
            $parsed['credentialId'] = $cred_id;
            $parsed['credentialPublicKey'] = $credential_public_key;
        }

        return $parsed;
    }

    protected static function parse_assertion_auth_data($auth_data) {
        if (strlen($auth_data) < 37) {
            throw new RuntimeException('Assertion authenticator data is too short.');
        }

        return [
            'rpIdHash'  => substr($auth_data, 0, 32),
            'flags'     => ord($auth_data[32]),
            'signCount' => unpack('N', substr($auth_data, 33, 4))[1],
        ];
    }

    protected static function cose_to_pem($cose_key_bytes) {
        $cose = BS2FA_CBOR::decode($cose_key_bytes);
        if (!is_array($cose)) {
            throw new RuntimeException('Invalid COSE public key.');
        }

        $kty = $cose[1] ?? null;
        $alg = $cose[3] ?? null;
        $crv = $cose[-1] ?? null;
        $x   = $cose[-2] ?? null;
        $y   = $cose[-3] ?? null;

        if ($kty !== 2 || $alg !== -7 || $crv !== 1 || !is_string($x) || !is_string($y)) {
            throw new RuntimeException('Only ES256 passkeys are supported by this lightweight build.');
        }

        $uncompressed = "\x04" . $x . $y;
        $der_prefix = hex2bin('3059301306072A8648CE3D020106082A8648CE3D030107034200');
        $der = $der_prefix . $uncompressed;
        $pem = "-----BEGIN PUBLIC KEY-----\n";
        $pem .= chunk_split(base64_encode($der), 64, "\n");
        $pem .= "-----END PUBLIC KEY-----\n";

        return $pem;
    }
}
