// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

// OpenZeppelin v4.9 ships Base64.encode (standard) but not encodeURL — inline the URL variant.

/// @title WebAuthn — secp256r1 WebAuthn assertion verifier for AERE
/// @notice Precompile-only verification using Fusaka's RIP-7951 at address 0x100.
///         AERE chain 2800 activated Fusaka at unix 1780220351 (block 2,106,606) so the
///         precompile is unconditionally present from there onward — no FreshCryptoLib
///         fallback needed.
///
/// @dev Adapted from Coinbase Smart Wallet's WebAuthn library (base-org/webauthn-sol),
///      which adapted from Daimo's p256-verifier. Verification logic is identical;
///      we strip the FCL_ecdsa fallback path because the precompile is guaranteed on AERE.
///
///      Audited reference: https://github.com/base-org/webauthn-sol (Coinbase, 2024)
library WebAuthn {
    struct WebAuthnAuth {
        /// @dev Authenticator data — at minimum 37 bytes: rpIdHash(32) + flags(1) + signCount(4).
        bytes authenticatorData;
        /// @dev Client data JSON — the browser's WebAuthn response, contains the challenge.
        string clientDataJSON;
        /// @dev Index in clientDataJSON where "challenge":"..." begins.
        uint256 challengeIndex;
        /// @dev Index in clientDataJSON where "type":"..." begins.
        uint256 typeIndex;
        /// @dev secp256r1 signature r component.
        uint256 r;
        /// @dev secp256r1 signature s component.
        uint256 s;
    }

    /// @dev secp256r1 curve order / 2 — used to guard against signature malleability.
    ///      n = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551
    ///      n/2 below.
    uint256 private constant P256_N_DIV_2 =
        0x7fffffff80000000ffffffffffffffffde737d56d38bcf4279dce5617e3192a8;

    /// @dev RIP-7951 secp256r1 verifier precompile address (Fusaka).
    address private constant VERIFIER = address(0x100);

    /// @dev Authenticator data flag bits per WebAuthn spec.
    bytes1 private constant AUTH_FLAG_UP = 0x01; // User Present
    bytes1 private constant AUTH_FLAG_UV = 0x04; // User Verified

    /// @dev Expected "type":"webauthn.get" hash (21 bytes verbatim).
    bytes32 private constant EXPECTED_TYPE_HASH = keccak256('"type":"webauthn.get"');

    /// @notice Verify a WebAuthn assertion signature.
    /// @param challenge   The 32-byte challenge that was passed to navigator.credentials.get().
    ///                    Must match the base64url-encoded value in clientDataJSON.
    /// @param requireUV   If true, require the User Verified flag to be set (Touch ID / Face ID
    ///                    counts as UV; a simple "present" press of a YubiKey does not).
    /// @param auth        Parsed WebAuthn assertion from the browser.
    /// @param x           secp256r1 public key x-coordinate.
    /// @param y           secp256r1 public key y-coordinate.
    /// @return ok         True iff the assertion is valid.
    function verify(
        bytes memory challenge,
        bool requireUV,
        WebAuthnAuth memory auth,
        uint256 x,
        uint256 y
    ) internal view returns (bool ok) {
        // 1. Malleability guard.
        if (auth.s > P256_N_DIV_2) return false;

        // 2. Verify clientDataJSON contains "type":"webauthn.get" at typeIndex.
        bytes memory cd = bytes(auth.clientDataJSON);
        if (auth.typeIndex + 21 > cd.length) return false;
        bytes memory typeSlice = new bytes(21);
        for (uint256 i = 0; i < 21; i++) typeSlice[i] = cd[auth.typeIndex + i];
        if (keccak256(typeSlice) != EXPECTED_TYPE_HASH) return false;

        // 3. Verify clientDataJSON contains the expected challenge at challengeIndex.
        bytes memory expectedChallenge = bytes(
            string.concat('"challenge":"', _base64url(challenge), '"')
        );
        uint256 elen = expectedChallenge.length;
        if (auth.challengeIndex + elen > cd.length) return false;
        bytes memory actualChallenge = new bytes(elen);
        for (uint256 i = 0; i < elen; i++) actualChallenge[i] = cd[auth.challengeIndex + i];
        if (keccak256(actualChallenge) != keccak256(expectedChallenge)) return false;

        // 4. Verify authenticatorData flags.
        if (auth.authenticatorData.length < 33) return false;
        if (auth.authenticatorData[32] & AUTH_FLAG_UP != AUTH_FLAG_UP) return false;
        if (requireUV && (auth.authenticatorData[32] & AUTH_FLAG_UV) != AUTH_FLAG_UV) return false;

        // 5. Compute the message hash that was signed: sha256(authData || sha256(clientDataJSON)).
        bytes32 clientDataHash = sha256(bytes(auth.clientDataJSON));
        bytes32 msgHash = sha256(abi.encodePacked(auth.authenticatorData, clientDataHash));

        // 6. Call the RIP-7951 precompile at 0x100.
        //    Input: msgHash (32) || r (32) || s (32) || x (32) || y (32) — 160 bytes total.
        //    Output: 32 bytes — 0x...01 if valid, empty if invalid (or 0x...00).
        bytes memory args = abi.encode(msgHash, auth.r, auth.s, x, y);
        (bool callOk, bytes memory ret) = VERIFIER.staticcall(args);
        if (!callOk || ret.length == 0) return false;
        return abi.decode(ret, (uint256)) == 1;
    }

    /// @dev Base64-URL encode (no padding, '-_' alphabet) — for the WebAuthn challenge.
    ///      Standard library implementation; gas-light because typical input is 32 bytes.
    function _base64url(bytes memory data) private pure returns (string memory) {
        if (data.length == 0) return "";
        bytes memory table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
        uint256 dataLen = data.length;
        // base64url length without padding = ceil(dataLen * 4 / 3)
        uint256 encodedLen = (dataLen * 4 + 2) / 3;
        bytes memory result = new bytes(encodedLen);
        uint256 j;
        uint256 i;
        for (i = 0; i + 3 <= dataLen; i += 3) {
            uint256 v = (uint256(uint8(data[i])) << 16) | (uint256(uint8(data[i+1])) << 8) | uint256(uint8(data[i+2]));
            result[j++] = table[(v >> 18) & 0x3f];
            result[j++] = table[(v >> 12) & 0x3f];
            result[j++] = table[(v >> 6) & 0x3f];
            result[j++] = table[v & 0x3f];
        }
        uint256 rem = dataLen - i;
        if (rem == 1) {
            uint256 v = uint256(uint8(data[i])) << 16;
            result[j++] = table[(v >> 18) & 0x3f];
            result[j++] = table[(v >> 12) & 0x3f];
        } else if (rem == 2) {
            uint256 v = (uint256(uint8(data[i])) << 16) | (uint256(uint8(data[i+1])) << 8);
            result[j++] = table[(v >> 18) & 0x3f];
            result[j++] = table[(v >> 12) & 0x3f];
            result[j++] = table[(v >> 6) & 0x3f];
        }
        return string(result);
    }
}
