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

/**
 * @title AereDelegate7702V2 — scope-hardened EIP-7702 delegation target
 * @notice Drop-in successor to AereDelegate7702. Same owner and passkey
 *         surface, but session keys are now properly SCOPED so a delegated
 *         key can no longer exceed the authority the owner intended.
 *
 * @dev    WHY V2 (two closed session-key escapes vs the original):
 *
 *         FACET 1 - unbounded target + unbounded value. The original bound a
 *         session key ONLY by a 4-byte selector allow-list. There was no
 *         target allow-list and no value cap, so a key granted selector S
 *         could point `target` at ANY contract exposing S and set `value` to
 *         the account's ENTIRE native balance, draining it to an arbitrary
 *         address. V2 requires (a) the target to be in a per-key target
 *         allow-list and (b) the cumulative native value moved by the key to
 *         stay within a per-key spend cap (cap defaults to 0, i.e. a key can
 *         move NO native value unless the owner explicitly grants a cap).
 *
 *         FACET 2 - self-call privilege escalation. onlyOwner is
 *         msg.sender == address(this), and executeWithSessionKey performs its
 *         outbound call with msg.sender == address(this). A key granted a
 *         selector colliding with an owner-only function (setPasskey /
 *         addSessionKey / revokeSessionKey) could therefore target the account
 *         itself and pass onlyOwner, taking the account over. V2 forbids
 *         target == address(this) for every session-key call, so a session key
 *         can never invoke the account's own owner-gated functions. The owner
 *         (a real EIP-7702 self-transaction) is unaffected.
 *
 *         STORAGE LAYOUT (7702-safe): as with V1, 7702 transmits only code;
 *         all state reads/writes hit the delegating EOA's own account storage.
 *         This is a FRESH code address; delegating EOAs opt in by re-pointing
 *         their own 7702 tuple (owner-supervised), never migrated for them.
 */
contract AereDelegate7702V2 {

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

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

    struct SessionKey {
        uint48 expiry;                 // unix seconds; key inactive once expiry < now
        bool active;                   // owner kill switch; false = revoked
        uint256 scopeEpoch;            // current scope generation; bumped on every add + revoke
        uint256 spendCap;              // cumulative native-value ceiling for this key (0 = no native value)
        uint256 spent;                 // cumulative native value already moved by this key (current grant)
        // Scope is keyed by scopeEpoch so a revoke + re-grant starts a FRESH,
        // empty scope: only the newly-listed selectors/targets are live and no
        // historical grant can ever be reactivated (the union-escape fix).
        mapping(uint256 => mapping(bytes4 => bool)) allowedSelectorsAt;
        mapping(uint256 => mapping(address => bool)) allowedTargetsAt;
    }

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

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

    /// @notice One WebAuthn (P-256) public key per delegating EOA.
    bytes32 public passkeyPubX;
    bytes32 public passkeyPubY;

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

    mapping(address => SessionKey) internal _sessionKeys;

    /// @dev Reentrancy guard shared by executeBatch and executeWithSessionKey.
    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, uint256 spendCap);
    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 TargetNotAllowed();
    error SpendCapExceeded();
    error CallFailed(uint256 idx, bytes reason);
    error InvalidPasskeySignature();
    error PasskeyNotConfigured();
    error InvalidChallenge();

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

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

    /**
     * @dev Under EIP-7702 the EOA is identified by address(this), so the owner
     *      check is msg.sender == address(this) (the EOA owner sending a
     *      self-signed tx). Session keys are explicitly barred from targeting
     *      address(this), so they can never satisfy this modifier by self-call.
     */
    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 ----------------------------- */

    /**
     * @notice Register / re-grant a scoped session key.
     * @param key              The session-key address.
     * @param expiry           Unix seconds after which the key cannot act.
     * @param spendCap         Cumulative native-value ceiling for this key. Pass
     *                         0 to forbid the key from moving any native value.
     * @param allowedSelectors 4-byte selectors the key may invoke.
     * @param allowedTargets   Contract addresses the key may call. address(this)
     *                         is rejected: a session key can never call the
     *                         account itself.
     * @dev Each grant is AUTHORITATIVE: it bumps the key's scopeEpoch and writes
     *      the selectors/targets under the fresh epoch, so a re-grant does NOT
     *      accumulate the union of past scopes. Only the newly-listed selectors
     *      and targets are live; expiry, active and spendCap are overwritten, and
     *      the spent counter resets so a re-grant issues a fresh native budget.
     */
    function addSessionKey(
        address key,
        uint48 expiry,
        uint256 spendCap,
        bytes4[] calldata allowedSelectors,
        address[] calldata allowedTargets
    ) external onlyOwner {
        SessionKey storage sk = _sessionKeys[key];
        // Fresh scope generation: everything written below lives ONLY at this
        // epoch, so no selector/target from any prior grant carries over.
        uint256 epoch = ++sk.scopeEpoch;
        sk.expiry = expiry;
        sk.active = true;
        sk.spendCap = spendCap;
        sk.spent = 0;
        for (uint256 i = 0; i < allowedSelectors.length; i++) {
            sk.allowedSelectorsAt[epoch][allowedSelectors[i]] = true;
        }
        for (uint256 i = 0; i < allowedTargets.length; i++) {
            // A session key may never target the account itself (escalation guard).
            if (allowedTargets[i] == address(this)) revert TargetNotAllowed();
            sk.allowedTargetsAt[epoch][allowedTargets[i]] = true;
        }
        emit SessionKeyAdded(key, expiry, spendCap);
    }

    function revokeSessionKey(address key) external onlyOwner {
        SessionKey storage sk = _sessionKeys[key];
        sk.active = false;
        // Advance the scope generation so the just-revoked scope is orphaned
        // immediately and can never be read again, even on a later re-grant.
        sk.scopeEpoch++;
        emit SessionKeyRevoked(key);
    }

    /// @notice Public read for dashboards: selector-level view (V1-compatible).
    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.allowedSelectorsAt[sk.scopeEpoch][selector]);
    }

    /// @notice Full per-key scope view: selector + target + native budget.
    function sessionKeyScope(address key, bytes4 selector, address target)
        external
        view
        returns (
            bool active,
            uint48 expiry,
            bool selectorAllowed,
            bool targetAllowed,
            uint256 spendCap,
            uint256 spent
        )
    {
        SessionKey storage sk = _sessionKeys[key];
        uint256 epoch = sk.scopeEpoch;
        return (
            sk.active,
            sk.expiry,
            sk.allowedSelectorsAt[epoch][selector],
            sk.allowedTargetsAt[epoch][target],
            sk.spendCap,
            sk.spent
        );
    }

    /// @notice A session key invokes itself as msg.sender. The call is allowed
    ///         only if the key is active, unexpired, the target is in the key's
    ///         target allow-list (and is NOT the account itself), the selector
    ///         is in the key's selector allow-list, and the cumulative native
    ///         value stays within the key's spend cap.
    function executeWithSessionKey(Call calldata callItem) external payable nonReentrant {
        SessionKey storage sk = _sessionKeys[msg.sender];
        if (!sk.active || sk.expiry < block.timestamp) revert SessionKeyExpired();

        // Only the key's CURRENT scope generation authorizes; any target/selector
        // from a prior (revoked or superseded) grant lives at a stale epoch and
        // is unreachable here.
        uint256 epoch = sk.scopeEpoch;

        // FACET 2 guard: a session key can never call the account itself, so it
        // can never reach an owner-only function via msg.sender == address(this).
        if (callItem.target == address(this)) revert TargetNotAllowed();

        // FACET 1 guard (target): the target must be explicitly allow-listed.
        if (!sk.allowedTargetsAt[epoch][callItem.target]) revert TargetNotAllowed();

        if (callItem.data.length < 4) revert SelectorNotAllowed();
        bytes4 selector;
        bytes calldata d = callItem.data;
        assembly { selector := calldataload(d.offset) }
        if (!sk.allowedSelectorsAt[epoch][selector]) revert SelectorNotAllowed();

        // FACET 1 guard (value): cumulative native spend may not exceed the cap.
        if (callItem.value != 0) {
            uint256 newSpent = sk.spent + callItem.value;
            if (newSpent > sk.spendCap) revert SpendCapExceeded();
            sk.spent = newSpent;
        }

        (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). Unchanged from V1.
     */
    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));

        // Bind the signed clientDataJSON to (this, nonce, call) so an unrelated
        // WebAuthn signature cannot be replayed as authorisation for a call.
        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)>"`.
    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 and needle is fixed
    ///      57 bytes so gas is bounded.
    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 {}
}
