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

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

/// @title AerePasskeyAccountV2 — production-grade passkey-controlled smart account
/// @notice Adds to V1:
///         - MultiOwnable: passkey + EOA + multiple keys, full recovery
///         - ERC-4337 validateUserOp shim → Pimlico/Stackup/ZeroDev bundlers compatible
///         - EIP-1271 isValidSignature → DeFi integrations (Permit2, OpenSea, Snapshot)
///         - Direct executeWithPasskey path for our own Foundation relayer
///         - Owner-selecting signature wrapper (which key signed?)
///
/// @dev Architecture follows Coinbase Smart Wallet (audited by ChainSecurity, OpenZeppelin)
///      with WebAuthn library = base-org/webauthn-sol (precompile-only port).
contract AerePasskeyAccountV2 is MultiOwnable {
    /// @notice ERC-4337 EntryPoint that's allowed to call validateUserOp + executeFromEntryPoint.
    ///         Set once at construction by the factory.
    address public entryPoint;

    /// @notice Replay counter for the direct passkey path (executeWithPasskey).
    uint256 public passkeyNonce;

    /// @notice Factory that deployed this account (for initialize gate).
    address public immutable factory;

    /// @notice EIP-1271 magic value returned by isValidSignature on success.
    bytes4 internal constant EIP1271_MAGIC = 0x1626ba7e;

    /// @notice Signature wrapper — selects which owner signed.
    ///         For passkey owners: signatureData is abi.encode(WebAuthn.WebAuthnAuth).
    ///         For EOA owners: signatureData is the raw 65-byte ECDSA signature.
    struct SignatureWrapper {
        uint256 ownerIndex;
        bytes signatureData;
    }

    /// @notice Generic call struct (used by executeBatch).
    struct Call {
        address target;
        uint256 value;
        bytes data;
    }

    /// @notice ERC-4337 v0.7 PackedUserOperation shape (minimal subset we use).
    struct PackedUserOperation {
        address sender;
        uint256 nonce;
        bytes initCode;
        bytes callData;
        bytes32 accountGasLimits;
        uint256 preVerificationGas;
        bytes32 gasFees;
        bytes paymasterAndData;
        bytes signature;
    }

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

    error InvalidSignature();
    error CallFailed(bytes returnData);
    error NotInitialized();
    error AlreadyInitialized();
    error InvalidEntryPoint();
    error InvalidOwnerForSignature();
    error NoOwners();

    constructor() {
        factory = msg.sender;
    }

    /// @notice One-shot initializer called by the factory at deploy time.
    function initialize(address _entryPoint, bytes[] calldata initialOwners) external {
        if (msg.sender != factory) revert AlreadyInitialized();
        if (entryPoint != address(0)) revert AlreadyInitialized();
        if (_entryPoint == address(0)) revert InvalidEntryPoint();
        // Defense in depth: an empty owner set bricks the account permanently
        // (funds at its address can never be moved). The factory blocks this too.
        if (initialOwners.length == 0) revert NoOwners();
        entryPoint = _entryPoint;
        emit EntryPointSet(_entryPoint);
        bytes[] memory owners = new bytes[](initialOwners.length);
        for (uint256 i = 0; i < initialOwners.length; i++) owners[i] = initialOwners[i];
        _initializeOwners(owners);
    }

    // ─── ERC-4337 path ────────────────────────────────────────────────────────

    /// @notice Called by the EntryPoint to validate a user operation.
    /// @dev    Returns 0 on success per ERC-4337. Does NOT revert on bad signature
    ///         (per spec — caller handles the failure code).
    function validateUserOp(
        PackedUserOperation calldata userOp,
        bytes32 userOpHash,
        uint256 missingAccountFunds
    ) external returns (uint256 validationData) {
        if (msg.sender != entryPoint) revert Unauthorized();

        // Verify the signature over userOpHash.
        bool ok = _verifySignature(userOpHash, userOp.signature);
        if (!ok) return 1; // SIG_VALIDATION_FAILED per ERC-4337

        // Pay the EntryPoint any prefund it needs (uncapped — EntryPoint enforces gas budget).
        if (missingAccountFunds > 0) {
            (bool sent, ) = payable(entryPoint).call{value: missingAccountFunds}("");
            (sent); // ignore — EntryPoint will revert if underfunded
        }
        return 0;
    }

    /// @notice Called by the EntryPoint after validation. Dispatches the user's intended call.
    /// @dev    The userOp.callData should encode a call to executeFromEntryPoint(target, value, data)
    ///         or executeBatchFromEntryPoint(calls).
    function executeFromEntryPoint(address target, uint256 value, bytes calldata data) external {
        if (msg.sender != entryPoint) revert Unauthorized();
        (bool ok, bytes memory ret) = target.call{value: value}(data);
        if (!ok) revert CallFailed(ret);
    }

    function executeBatchFromEntryPoint(Call[] calldata calls) external {
        if (msg.sender != entryPoint) revert Unauthorized();
        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(ret);
        }
    }

    // ─── Direct passkey path (Foundation relayer / self-relay) ────────────────

    /// @notice Hash that the user's signing key must sign for an executeWithPasskey() call.
    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 a single call, authorized by any registered owner.
    function executeWithPasskey(
        SignatureWrapper calldata sw,
        address target,
        uint256 value,
        bytes calldata data
    ) external returns (bytes memory result) {
        if (ownerCount == 0) revert NotInitialized();
        bytes32 challenge = getExecuteChallenge(target, value, data, passkeyNonce);
        if (!_verifyWrappedSignature(challenge, sw)) revert InvalidSignature();

        uint256 n = passkeyNonce;
        passkeyNonce = n + 1;

        (bool ok, bytes memory ret) = target.call{value: value}(data);
        if (!ok) revert CallFailed(ret);
        emit Executed(n, target, value, data);
        return ret;
    }

    /// @notice Hash for an executeBatchWithPasskey call.
    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 executeBatchWithPasskey(SignatureWrapper calldata sw, Call[] calldata calls)
        external returns (bytes[] memory results)
    {
        if (ownerCount == 0) revert NotInitialized();
        bytes32 challenge = getExecuteBatchChallenge(calls, passkeyNonce);
        if (!_verifyWrappedSignature(challenge, sw)) revert InvalidSignature();

        uint256 n = passkeyNonce;
        passkeyNonce = n + 1;

        results = new bytes[](calls.length);
        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(ret);
            results[i] = ret;
        }
        emit ExecutedBatch(n, calls.length);
    }

    // ─── EIP-1271 ─────────────────────────────────────────────────────────────

    /// @notice EIP-1271: validate an off-chain signature for the given hash.
    ///         Used by Permit2, OpenSea, Snapshot, dapp login flows.
    function isValidSignature(bytes32 hash, bytes calldata signature)
        external view returns (bytes4)
    {
        // Wrap the hash in a domain-separated digest to prevent cross-account replay.
        bytes32 wrappedHash = keccak256(abi.encode(
            keccak256("AereAccount1271(address account,uint256 chainid,bytes32 hash)"),
            address(this), block.chainid, hash
        ));
        return _verifyWrappedSignature(wrappedHash, abi.decode(signature, (SignatureWrapper)))
            ? EIP1271_MAGIC : bytes4(0);
    }

    // ─── Signature verification (internal) ────────────────────────────────────

    function _verifySignature(bytes32 hash, bytes calldata signature) internal view returns (bool) {
        SignatureWrapper memory sw = abi.decode(signature, (SignatureWrapper));
        return _verifyWrappedSignature(hash, sw);
    }

    function _verifyWrappedSignature(bytes32 hash, SignatureWrapper memory sw)
        internal view returns (bool)
    {
        bytes memory owner = _ownerAtIndex[sw.ownerIndex];
        if (owner.length == 0) return false;

        if (owner.length == 32) {
            // EOA owner — recover from 65-byte ECDSA sig.
            address signer = abi.decode(owner, (address));
            address recovered = _recoverEOA(hash, sw.signatureData);
            // Reject address(0): a malformed signature or an ecrecover failure returns
            // address(0), which must never authorize (belt-and-suspenders with the
            // address(0)-owner ban in MultiOwnable).
            return recovered != address(0) && recovered == signer;
        } else if (owner.length == 64) {
            // Passkey owner — decode WebAuthnAuth from signatureData.
            (bytes32 x, bytes32 y) = abi.decode(owner, (bytes32, bytes32));
            WebAuthn.WebAuthnAuth memory auth = abi.decode(sw.signatureData, (WebAuthn.WebAuthnAuth));
            return WebAuthn.verify(abi.encodePacked(hash), true, auth, uint256(x), uint256(y));
        }
        return false;
    }

    function _recoverEOA(bytes32 hash, bytes memory sig) internal pure returns (address) {
        if (sig.length != 65) return address(0);
        bytes32 r; bytes32 s; uint8 v;
        // solhint-disable-next-line no-inline-assembly
        assembly {
            r := mload(add(sig, 32))
            s := mload(add(sig, 64))
            v := byte(0, mload(add(sig, 96)))
        }
        if (v < 27) v += 27;
        // Use EIP-191 personal_sign envelope for EOA signers (compatible with MetaMask).
        bytes32 ethHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
        return ecrecover(ethHash, v, r, s);
    }

    receive() external payable {}
}
