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

/// @title AereEntryPointV2 — ERC-4337 v0.7-shape EntryPoint for AERE chain 2800
/// @notice Real 4337-shaped EntryPoint that:
///         - Stores per-account/paymaster deposits
///         - Calls account.validateUserOp BEFORE executing user op
///         - Calls account.executeFromEntryPoint AFTER successful validation
///         - Charges the paymaster (if any) or account's deposit for the actual gas used
///         - Emits standard ERC-4337 events so bundlers / indexers can subscribe
///
/// @dev    Compatible subset of canonical eth-infinitism EntryPoint v0.7.
///         Real bundlers (Pimlico, Stackup, ZeroDev) can target this address with their
///         per-chain config. We host the canonical relayer ourselves while bootstrapping.
contract AereEntryPointV2 {
    struct DepositInfo {
        uint256 deposit;
        bool staked;
        uint112 stake;
        uint32 unstakeDelaySec;
        uint48 withdrawTime;
    }

    struct PackedUserOperation {
        address sender;
        uint256 nonce;
        bytes initCode;
        bytes callData;
        bytes32 accountGasLimits; // packed: callGasLimit (16) || verificationGasLimit (16)
        uint256 preVerificationGas;
        bytes32 gasFees;          // packed: maxPriorityFee (16) || maxFee (16)
        bytes paymasterAndData;
        bytes signature;
    }

    mapping(address => DepositInfo) internal _deposits;
    mapping(address => uint256) public getNonce;

    event UserOperationEvent(
        bytes32 indexed userOpHash,
        address indexed sender,
        address indexed paymaster,
        uint256 nonce,
        bool success,
        uint256 actualGasCost,
        uint256 actualGasUsed
    );
    event AccountDeployed(bytes32 indexed userOpHash, address indexed sender, address factory, address paymaster);
    event Deposited(address indexed account, uint256 totalDeposit);
    event Withdrawn(address indexed account, address withdrawAddress, uint256 amount);

    error FailedOp(uint256 opIndex, string reason);
    error AccountValidationFailed(uint256 opIndex);
    error PaymasterValidationFailed(uint256 opIndex);

    // ─── Deposits ────────────────────────────────────────────────────────────

    function depositTo(address account) public payable {
        _deposits[account].deposit += msg.value;
        emit Deposited(account, _deposits[account].deposit);
    }

    function balanceOf(address account) external view returns (uint256) {
        return _deposits[account].deposit;
    }

    function withdrawTo(address payable withdrawAddress, uint256 amount) external {
        require(_deposits[msg.sender].deposit >= amount, "EP: insufficient");
        _deposits[msg.sender].deposit -= amount;
        (bool ok, ) = withdrawAddress.call{value: amount}("");
        require(ok, "EP: withdraw failed");
        emit Withdrawn(msg.sender, withdrawAddress, amount);
    }

    receive() external payable {
        depositTo(msg.sender);
    }

    // ─── handleOps — the canonical 4337 entry function ───────────────────────

    /// @notice Validate then execute a batch of user operations.
    function handleOps(PackedUserOperation[] calldata ops, address payable beneficiary) external {
        require(beneficiary != address(0), "EP: zero beneficiary");

        // Validation phase: validate every account AND — critically — every
        // paymaster's CONSENT before it can ever be charged. `paymasters[i]` is
        // the paymaster that actively approved ops[i] (address(0) = self-paying).
        // A paymaster named in paymasterAndData that did NOT validate this exact
        // op is never recorded here, so it can never be charged in execution.
        bytes32[] memory opHashes = new bytes32[](ops.length);
        address[] memory paymasters = new address[](ops.length);
        for (uint256 i = 0; i < ops.length; i++) {
            opHashes[i] = getUserOpHash(ops[i]);
            (uint256 valData, address validatedPaymaster) = _validate(ops[i], opHashes[i], i);
            if (valData != 0) revert AccountValidationFailed(i);
            paymasters[i] = validatedPaymaster;
        }

        // Execution phase.
        uint256 collected = 0;
        for (uint256 i = 0; i < ops.length; i++) {
            uint256 preGas = gasleft();
            address sender = ops[i].sender;
            // Only a paymaster that CONSENTED in the validation phase can be the
            // payer. Never re-derive it from the attacker-supplied paymasterAndData.
            address paymaster = paymasters[i];
            (bool success, ) = sender.call(ops[i].callData);
            uint256 actualGasUsed = preGas - gasleft();
            uint256 maxFee = uint256(uint128(uint256(ops[i].gasFees)));
            uint256 actualGasCost = actualGasUsed * maxFee;
            // Charge the consenting paymaster (if any), else the account itself.
            address payer = paymaster != address(0) ? paymaster : sender;
            require(_deposits[payer].deposit >= actualGasCost, "EP: payer underfunded");
            _deposits[payer].deposit -= actualGasCost;
            collected += actualGasCost;
            emit UserOperationEvent(opHashes[i], sender, paymaster, ops[i].nonce, success, actualGasCost, actualGasUsed);
        }

        // Pay beneficiary (the bundler / relayer EOA).
        if (collected > 0) {
            (bool sent, ) = beneficiary.call{value: collected}("");
            require(sent, "EP: beneficiary send failed");
        }
    }

    /// @param validationData     account's ERC-4337 validation result (0 == valid).
    /// @param validatedPaymaster the paymaster that ACTIVELY approved this exact op,
    ///                           or address(0) when the op is self-paying. A non-zero
    ///                           value here is the ONLY authorization to charge that
    ///                           paymaster's deposit later in the execution phase.
    function _validate(PackedUserOperation calldata op, bytes32 opHash, uint256 idx)
        internal returns (uint256 validationData, address validatedPaymaster)
    {
        // Bump nonce.
        uint256 expectedNonce = getNonce[op.sender];
        if (op.nonce != expectedNonce) revert FailedOp(idx, "EP: bad nonce");
        getNonce[op.sender] = expectedNonce + 1;

        // Determine prefund.
        uint256 maxFee = uint256(uint128(uint256(op.gasFees)));
        uint256 verifGas = uint256(uint128(uint256(op.accountGasLimits) >> 128));
        uint256 callGas = uint256(uint128(uint256(op.accountGasLimits)));
        uint256 maxCost = (verifGas + callGas + op.preVerificationGas) * maxFee;

        // Paymaster consent gate. If the op names a paymaster, the EntryPoint MUST
        // obtain that paymaster's approval for THIS exact userOpHash before it may
        // ever be charged. Without this, any caller could name a victim paymaster
        // in paymasterAndData and drain its deposit to a caller-chosen beneficiary.
        // A sponsored op does not prefund from the account (the paymaster covers gas).
        address paymaster = _paymasterOf(op);
        if (paymaster != address(0)) {
            validatedPaymaster = _validatePaymaster(op, opHash, maxCost, paymaster, idx);
            maxCost = 0; // paymaster pays; account is not asked to prefund.
        }

        // Call account.validateUserOp with the account's required prefund
        // (0 when a paymaster is sponsoring this op).
        address account = op.sender;
        (bool ok, bytes memory ret) = account.call(
            abi.encodeWithSignature(
                "validateUserOp((address,uint256,bytes,bytes,bytes32,uint256,bytes32,bytes,bytes),bytes32,uint256)",
                op, opHash, maxCost
            )
        );
        if (!ok) revert FailedOp(idx, "EP: validateUserOp reverted");
        validationData = abi.decode(ret, (uint256));
    }

    /// @notice Ask the named paymaster whether it agrees to sponsor this op.
    /// @dev    ERC-4337 v0.7 paymaster hook. The paymaster is called FROM this
    ///         EntryPoint, so a spec-compliant paymaster's own `onlyEntryPoint`
    ///         check binds consent to this EntryPoint. A paymaster that reverts,
    ///         is not a contract, returns a malformed result, or returns a nonzero
    ///         validationData is treated as NO CONSENT and the whole op is rejected,
    ///         so its deposit is never touched.
    /// @return The paymaster address, iff it validated this exact op (else reverts).
    function _validatePaymaster(
        PackedUserOperation calldata op,
        bytes32 opHash,
        uint256 maxCost,
        address paymaster,
        uint256 idx
    ) internal returns (address) {
        // A non-contract address can never "consent" — reject rather than silently
        // succeed (a low-level call to an EOA returns ok=true with empty data).
        if (paymaster.code.length == 0) revert PaymasterValidationFailed(idx);

        (bool ok, bytes memory ret) = paymaster.call(
            abi.encodeWithSignature(
                "validatePaymasterUserOp((address,uint256,bytes,bytes,bytes32,uint256,bytes32,bytes,bytes),bytes32,uint256)",
                op, opHash, maxCost
            )
        );
        // Revert / wrong-interface (no or short return) / explicit rejection all
        // mean the paymaster did not consent to fund THIS op.
        if (!ok) revert PaymasterValidationFailed(idx);
        if (ret.length < 64) revert PaymasterValidationFailed(idx);
        (, uint256 pmValidationData) = abi.decode(ret, (bytes, uint256));
        if (pmValidationData != 0) revert PaymasterValidationFailed(idx);
        return paymaster;
    }

    function _paymasterOf(PackedUserOperation calldata op) internal pure returns (address) {
        if (op.paymasterAndData.length < 20) return address(0);
        return address(bytes20(op.paymasterAndData[0:20]));
    }

    /// @notice Canonical ERC-4337 v0.7 userOpHash.
    function getUserOpHash(PackedUserOperation calldata op) public view returns (bytes32) {
        bytes32 h = keccak256(abi.encode(
            op.sender, op.nonce,
            keccak256(op.initCode),
            keccak256(op.callData),
            op.accountGasLimits, op.preVerificationGas, op.gasFees,
            keccak256(op.paymasterAndData)
        ));
        return keccak256(abi.encode(h, address(this), block.chainid));
    }
}
