// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;

/**
 * @title AereDelegate7702 — EIP-7702 delegation target for native AERE EOAs
 * @notice This is the contract code that an AERE EOA points its 7702
 *         delegation tuple at. When a user signs the EIP-7702 authorisation
 *         tuple and includes it in a transaction, their EOA's code becomes
 *         the *deployed* bytecode of this contract for the lifetime of
 *         that authorisation. Their EOA can then expose all the smart-
 *         account functionality below — batched calls, session keys,
 *         passkey signature verification — while remaining controllable
 *         by the original secp256k1 EOA key.
 *
 * @dev    PHASE 1 surface (Q3 2026 ship):
 *
 *           1. executeBatch(Call[]) — atomic multi-call from the EOA. The
 *              EOA's own ECDSA signature on the surrounding transaction is
 *              the authorisation; no additional signature is needed inside.
 *
 *           2. executeWithPasskey(Call, P256Signature, bytes challenge)
 *              — for users who have ALSO registered a WebAuthn passkey
 *              (registerPasskey() below). The transaction itself is paid
 *              by a relayer (Foundation paymaster), the passkey signature
 *              authorises the inner call. This is the bridge between
 *              "I have MetaMask" and "I have Touch ID / Face ID".
 *
 *           3. addSessionKey(address key, uint48 expiry, bytes4[] allowedSelectors)
 *              — register a secondary key with a narrow scope (e.g. swap
 *              router only) and a short expiry (e.g. 24h). Used for
 *              dApp UX flows where you want to skip the wallet popup
 *              for a defined set of low-risk operations.
 *
 *           4. revokeSessionKey(address key) — kill switch.
 *
 *           5. setPasskey(bytes32 pubkeyX, bytes32 pubkeyY) — add a single
 *              WebAuthn passkey. Phase 1 supports one key; Phase 2 (Q4
 *              2026) extends to a MultiOwnable-style set via the same
 *              pattern as AerePasskeyAccountFactoryV2.
 *
 *         STORAGE LAYOUT (7702-safe):
 *           7702 transmits ONLY code, not storage. Every read of state
 *           below happens at the EOA's account storage. That means
 *           different EOAs delegating to this same code get independent
 *           storage. No upgrade migration needed; the EOA can re-point
 *           its delegation tuple at a new code address at any time.
 *
 *         REENTRANCY:
 *           Calls are external; the contract sets a transient _entered
 *           flag for the executeBatch path. Session-key invocation goes
 *           through the same path.
 *
 *         GAS:
 *           Phase 1 verifies P-256 via the RIP-7951 precompile at 0x100
 *           (~6,900 gas per signature). The chain has supported this
 *           since the Tier 1.15 deploy (block 2,157,502).
 */
contract AereDelegate7702 {

    /* --------------------------------- types --------------------------------- */

    struct Call {
        address target;
        uint256 value;
        bytes data;
    }

    struct SessionKey {
        uint48 expiry;           // unix seconds; 0 = revoked
        bool active;
        mapping(bytes4 => bool) allowedSelectors;
    }

    struct P256Signature {
        bytes32 r;
        bytes32 s;
        bytes authenticatorData;
        bytes clientDataJSON;
    }

    /* ------------------------------- storage ------------------------------- */

    /// @notice One WebAuthn (P-256) public key per delegating EOA. Phase 1
    ///         supports a single key; Phase 2 extends to a set.
    bytes32 public passkeyPubX;
    bytes32 public passkeyPubY;

    /// @notice Anti-replay nonce for passkey-authorised calls.
    uint256 public passkeyNonce;

    mapping(address => SessionKey) internal _sessionKeys;

    /// @dev Transient reentrancy guard.
    bool private _entered;

    /// @notice RIP-7951 P256Verify precompile address on AERE.
    address internal constant P256_VERIFY = 0x0000000000000000000000000000000000000100;

    /* ---------------------------------- events --------------------------------- */

    event BatchExecuted(uint256 callCount);
    event SessionKeyAdded(address indexed key, uint48 expiry);
    event SessionKeyRevoked(address indexed key);
    event PasskeySet(bytes32 indexed pubX, bytes32 indexed pubY);
    event PasskeyExecuted(uint256 nonce, address indexed target, uint256 value);

    /* ---------------------------------- errors -------------------------------- */

    error Reentrant();
    error NotEoaOwner();
    error SessionKeyExpired();
    error SelectorNotAllowed();
    error CallFailed(uint256 idx, bytes reason);
    error InvalidPasskeySignature();
    error PasskeyNotConfigured();
    error InvalidChallenge();

    /* -------------------------------- modifiers ------------------------------- */

    modifier nonReentrant() {
        if (_entered) revert Reentrant();
        _entered = true;
        _;
        _entered = false;
    }

    /**
     * @dev The EOA itself is identified by address(this) under EIP-7702. So
     *      msg.sender == address(this) (i.e. the EOA owner sending a
     *      directly-signed tx to themselves) is the canonical owner check.
     */
    modifier onlyOwner() {
        if (msg.sender != address(this)) revert NotEoaOwner();
        _;
    }

    /* --------------------------------- core: batch ---------------------------- */

    /// @notice Atomic batched-call entrypoint. Authorised by the EOA's own
    ///         ECDSA signature (the tx is signed by the EOA).
    function executeBatch(Call[] calldata calls) external payable nonReentrant onlyOwner {
        for (uint256 i = 0; i < calls.length; i++) {
            (bool ok, bytes memory ret) = calls[i].target.call{value: calls[i].value}(calls[i].data);
            if (!ok) revert CallFailed(i, ret);
        }
        emit BatchExecuted(calls.length);
    }

    /* ------------------------------- session keys ----------------------------- */

    function addSessionKey(
        address key,
        uint48 expiry,
        bytes4[] calldata allowedSelectors
    ) external onlyOwner {
        SessionKey storage sk = _sessionKeys[key];
        sk.expiry = expiry;
        sk.active = true;
        for (uint256 i = 0; i < allowedSelectors.length; i++) {
            sk.allowedSelectors[allowedSelectors[i]] = true;
        }
        emit SessionKeyAdded(key, expiry);
    }

    function revokeSessionKey(address key) external onlyOwner {
        SessionKey storage sk = _sessionKeys[key];
        sk.active = false;
        emit SessionKeyRevoked(key);
    }

    /// @notice Public read for dashboards.
    function sessionKeyInfo(address key, bytes4 selector) external view returns (bool active, uint48 expiry, bool selectorAllowed) {
        SessionKey storage sk = _sessionKeys[key];
        return (sk.active, sk.expiry, sk.allowedSelectors[selector]);
    }

    /// @notice A session key invokes itself as msg.sender. Verifies the key
    ///         is active and the call's selector is allowed.
    function executeWithSessionKey(Call calldata callItem) external payable nonReentrant {
        SessionKey storage sk = _sessionKeys[msg.sender];
        if (!sk.active || sk.expiry < block.timestamp) revert SessionKeyExpired();
        if (callItem.data.length < 4) revert SelectorNotAllowed();
        bytes4 selector;
        bytes calldata d = callItem.data;
        assembly { selector := calldataload(d.offset) }
        if (!sk.allowedSelectors[selector]) revert SelectorNotAllowed();

        (bool ok, bytes memory ret) = callItem.target.call{value: callItem.value}(callItem.data);
        if (!ok) revert CallFailed(0, ret);
    }

    /* -------------------------------- passkeys ------------------------------- */

    function setPasskey(bytes32 pubX, bytes32 pubY) external onlyOwner {
        passkeyPubX = pubX;
        passkeyPubY = pubY;
        emit PasskeySet(pubX, pubY);
    }

    /**
     * @notice Execute a single call authorised by a WebAuthn (P-256) signature.
     * @param  callItem  The call to perform.
     * @param  sig       The P-256 signature components.
     * @dev    The signed challenge is computed from the current passkeyNonce
     *         and the call payload. After verification, nonce is incremented
     *         (replay-protection).
     */
    function executeWithPasskey(Call calldata callItem, P256Signature calldata sig) external payable nonReentrant {
        if (passkeyPubX == bytes32(0) && passkeyPubY == bytes32(0)) revert PasskeyNotConfigured();

        // Build the WebAuthn signed digest: SHA256(authenticatorData || SHA256(clientDataJSON))
        bytes32 clientDataHash = sha256(sig.clientDataJSON);
        bytes32 digest = sha256(abi.encodePacked(sig.authenticatorData, clientDataHash));

        // ROUND-3 FIX: previously the contract verified the P-256 signature
        // over the WebAuthn digest but NEVER bound the signed clientDataJSON
        // to the call payload. That allowed any valid WebAuthn signature from
        // this passkey (e.g. an unrelated dApp login) to be replayed here as
        // authorisation for arbitrary calls. We now compute the expected
        // challenge (keccak256(this, nonce, call)) base64url-encoded and
        // require clientDataJSON to contain the canonical
        // `"challenge":"<expected>"` substring.
        bytes32 expectedChallenge = keccak256(abi.encode(address(this), passkeyNonce, callItem));
        if (!_clientDataBindsChallenge(sig.clientDataJSON, expectedChallenge)) {
            revert InvalidChallenge();
        }

        // Verify via RIP-7951 P256Verify at 0x100.
        // Calldata layout: 160 bytes — msgHash || r || s || qx || qy.
        bytes memory input = abi.encodePacked(digest, sig.r, sig.s, passkeyPubX, passkeyPubY);
        (bool ok, bytes memory ret) = P256_VERIFY.staticcall(input);
        if (!ok || ret.length == 0 || ret[31] != 0x01) revert InvalidPasskeySignature();

        // Replay-protect.
        uint256 nonce = passkeyNonce++;

        // Perform the call.
        (bool ok2, bytes memory r2) = callItem.target.call{value: callItem.value}(callItem.data);
        if (!ok2) revert CallFailed(0, r2);

        emit PasskeyExecuted(nonce, callItem.target, callItem.value);
    }

    /// @notice Convenience for relayers / dApps: recover the expected
    ///         challenge bytes32 for the NEXT passkey-authorised call.
    function nextPasskeyChallenge(Call calldata callItem) external view returns (bytes32) {
        return keccak256(abi.encode(address(this), passkeyNonce, callItem));
    }

    /* ----------------------- challenge-binding helpers ---------------------- */

    /// @dev base64url alphabet (RFC 4648 §5, no padding).
    bytes internal constant B64URL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";

    /// @dev Encode a 32-byte value as 43 chars of base64url (no padding).
    function _b64url32(bytes32 v) internal pure returns (bytes memory out) {
        out = new bytes(43);
        bytes memory tbl = B64URL;
        uint256 j = 0;
        for (uint256 i = 0; i + 3 <= 30; i += 3) {
            uint256 a = uint256(uint8(v[i]));
            uint256 b = uint256(uint8(v[i + 1]));
            uint256 c = uint256(uint8(v[i + 2]));
            out[j++] = tbl[a >> 2];
            out[j++] = tbl[((a & 0x03) << 4) | (b >> 4)];
            out[j++] = tbl[((b & 0x0f) << 2) | (c >> 6)];
            out[j++] = tbl[c & 0x3f];
        }
        // bytes 30, 31 → 3 b64url chars
        uint256 a2 = uint256(uint8(v[30]));
        uint256 b2 = uint256(uint8(v[31]));
        out[j++] = tbl[a2 >> 2];
        out[j++] = tbl[((a2 & 0x03) << 4) | (b2 >> 4)];
        out[j++] = tbl[(b2 & 0x0f) << 2];
    }

    /// @dev Check that `clientDataJSON` contains the canonical substring
    ///      `"challenge":"<base64url(expected)>"`. Webauthn's CollectedClientData
    ///      always encodes the challenge as base64url without padding (W3C spec).
    function _clientDataBindsChallenge(bytes calldata clientDataJSON, bytes32 expected) internal pure returns (bool) {
        bytes memory enc = _b64url32(expected);
        // Build the literal pattern `"challenge":"<enc>"` — 14 fixed chars + 43.
        bytes memory needle = new bytes(14 + 43);
        bytes memory prefix = bytes('"challenge":"');
        for (uint256 i = 0; i < 13; i++) needle[i] = prefix[i];
        for (uint256 i = 0; i < 43; i++) needle[13 + i] = enc[i];
        needle[56] = bytes1('"');
        return _contains(clientDataJSON, needle);
    }

    /// @dev Naive substring search; clientDataJSON is small (<512 bytes typical)
    ///      and needle is fixed 57 bytes → bounded gas.
    function _contains(bytes calldata hay, bytes memory needle) internal pure returns (bool) {
        uint256 hLen = hay.length;
        uint256 nLen = needle.length;
        if (nLen == 0 || hLen < nLen) return false;
        uint256 last = hLen - nLen;
        for (uint256 i = 0; i <= last; i++) {
            bool match_ = true;
            for (uint256 j = 0; j < nLen; j++) {
                if (hay[i + j] != needle[j]) { match_ = false; break; }
            }
            if (match_) return true;
        }
        return false;
    }

    /* ---------------------------------- receive ------------------------------- */

    receive() external payable {}
}
