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

import {WebAuthn} from "./WebAuthn.sol";

/// @title AerePasskeyAccount — passkey-controlled smart account
/// @notice An on-chain account whose only signer is a WebAuthn passkey (Touch ID, Face ID,
///         Windows Hello, YubiKey, EU Digital Identity Wallet, Android biometric).
///         Verification runs through the Fusaka RIP-7951 secp256r1 precompile at address
///         0x100 for ~3,450 gas — vs ~250,000 gas for a pure-Solidity P-256 verifier.
///
///         The account exposes a single public entry point: `execute()`. To call it, the
///         caller must supply a WebAuthn assertion whose challenge is the hash of the
///         call parameters (target, value, data, nonce, chainId). Anyone may be the
///         msg.sender — typically a Foundation-funded relayer or any third party. The
///         account itself enforces auth via the passkey signature.
///
///         Counterfactual: the account address is fully deterministic from
///         (factoryAddress, pubkey, salt) via CREATE2 — so dApps can compute and
///         show the user's address BEFORE the account is deployed, then fund it /
///         interact with it under the assumption it will exist.
contract AerePasskeyAccount {
    /// @notice The secp256r1 (P-256) public key bound to this account. Immutable once set.
    uint256 public publicKeyX;
    uint256 public publicKeyY;

    /// @notice Replay-protection counter. Each successful execute() increments.
    uint256 public nonce;

    /// @notice The factory that deployed this account. Trusted only for one purpose:
    ///         a constructor-time initialization can occur via the factory's clone proxy.
    address public immutable factory;

    event Executed(uint256 indexed nonce, address indexed target, uint256 value, bytes data);
    event ExecutedBatch(uint256 indexed nonce, uint256 count);

    error InvalidSignature();
    error CallFailed(bytes returnData);
    error NotInitialized();
    error AlreadyInitialized();

    constructor() {
        factory = msg.sender;
        // pub key not set here — initialised via initialize() by the factory in the same tx.
    }

    /// @notice Initializes the account with its passkey owner. Callable only once,
    ///         only by the factory, in the deploy transaction.
    function initialize(uint256 _x, uint256 _y) external {
        if (msg.sender != factory) revert AlreadyInitialized();
        if (publicKeyX != 0 || publicKeyY != 0) revert AlreadyInitialized();
        publicKeyX = _x;
        publicKeyY = _y;
    }

    /// @notice Hash that the user's passkey must sign to authorize `execute()`.
    /// @dev The challenge passed to navigator.credentials.get() must be this exact bytes32.
    function getExecuteChallenge(
        address target,
        uint256 value,
        bytes calldata data,
        uint256 expectedNonce
    ) public view returns (bytes32) {
        return keccak256(abi.encode(
            address(this),
            block.chainid,
            expectedNonce,
            target,
            value,
            data
        ));
    }

    /// @notice Execute an arbitrary call from this account, authorized by a passkey signature.
    /// @param auth   The WebAuthn assertion from the user's authenticator.
    /// @param target Destination contract / EOA.
    /// @param value  AERE to send (wei).
    /// @param data   Calldata for the target.
    function execute(
        WebAuthn.WebAuthnAuth calldata auth,
        address target,
        uint256 value,
        bytes calldata data
    ) external returns (bytes memory result) {
        if (publicKeyX == 0 && publicKeyY == 0) revert NotInitialized();

        bytes32 challenge = getExecuteChallenge(target, value, data, nonce);

        // Verify passkey signature over the challenge.
        // Require User Verified (UV) — Touch ID / Face ID etc., not just touch.
        bool ok = WebAuthn.verify(
            abi.encodePacked(challenge),
            true, // requireUV
            _toMem(auth),
            publicKeyX,
            publicKeyY
        );
        if (!ok) revert InvalidSignature();

        uint256 n = nonce;
        nonce = n + 1;

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

        emit Executed(n, target, value, data);
        return ret;
    }

    /// @notice Execute a batch of calls under a single passkey assertion.
    /// @dev Challenge for a batch = keccak256(this, chainid, nonce, abi.encode(Call[])).
    struct Call {
        address target;
        uint256 value;
        bytes data;
    }

    function getExecuteBatchChallenge(
        Call[] calldata calls,
        uint256 expectedNonce
    ) public view returns (bytes32) {
        return keccak256(abi.encode(
            address(this),
            block.chainid,
            expectedNonce,
            keccak256(abi.encode(calls))
        ));
    }

    function executeBatch(
        WebAuthn.WebAuthnAuth calldata auth,
        Call[] calldata calls
    ) external returns (bytes[] memory results) {
        if (publicKeyX == 0 && publicKeyY == 0) revert NotInitialized();

        bytes32 challenge = getExecuteBatchChallenge(calls, nonce);

        bool ok = WebAuthn.verify(
            abi.encodePacked(challenge),
            true,
            _toMem(auth),
            publicKeyX,
            publicKeyY
        );
        if (!ok) revert InvalidSignature();

        uint256 n = nonce;
        nonce = n + 1;

        results = new bytes[](calls.length);
        for (uint256 i = 0; i < calls.length; i++) {
            (bool callOk, bytes memory ret) = calls[i].target.call{value: calls[i].value}(calls[i].data);
            if (!callOk) revert CallFailed(ret);
            results[i] = ret;
        }

        emit ExecutedBatch(n, calls.length);
    }

    /// @dev Calldata → memory adapter for WebAuthn.verify which takes memory struct.
    function _toMem(WebAuthn.WebAuthnAuth calldata auth) private pure returns (WebAuthn.WebAuthnAuth memory m) {
        m.authenticatorData = auth.authenticatorData;
        m.clientDataJSON = auth.clientDataJSON;
        m.challengeIndex = auth.challengeIndex;
        m.typeIndex = auth.typeIndex;
        m.r = auth.r;
        m.s = auth.s;
    }

    /// @notice Receive AERE — accounts can hold native balance.
    receive() external payable {}
}
