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

import "./PaymasterBase.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

/**
 * @title AereEntryPoint
 * @notice Lightweight ERC-4337-style EntryPoint compatible with our PaymasterBase.
 *
 *   This is NOT the canonical eth-infinitism v0.7 EntryPoint at
 *   0x0000000071727De22E5E9d8BAf0edAc6f37da032 — that contract pulls in
 *   OpenZeppelin v5 which conflicts with the rest of our codebase (OZ v4.9).
 *
 *   Instead, this is a feature-equivalent EntryPoint that supports the exact
 *   subset of ERC-4337 v0.7 that AERE's paymasters and smart accounts need:
 *     - depositTo / balanceOf / withdrawTo / addStake / unlockStake / withdrawStake
 *     - validatePaymasterUserOp / postOp call dispatch
 *
 *   Bundlers (Pimlico, Biconomy, ZeroDev) can be configured to use this EntryPoint
 *   on AERE chain 2800 instead of the canonical mainnet address. They support
 *   per-chain EntryPoint addresses out of the box.
 *
 *   Once eth-infinitism publishes an OZ-v5-free version, or once we migrate to
 *   OZ v5 chain-wide, we'll redeploy at the canonical address via CREATE2 and
 *   migrate users via a one-time bundler config change.
 */
contract AereEntryPoint is ReentrancyGuard {
    // ───────────────────── Deposits & stakes ─────────────────────

    struct DepositInfo {
        uint256 deposit;
        bool staked;
        uint112 stake;
        uint32 unstakeDelaySec;
        uint48 withdrawTime;
    }

    mapping(address => DepositInfo) internal _deposits;

    event Deposited(address indexed account, uint256 totalDeposit);
    event Withdrawn(address indexed account, address withdrawAddress, uint256 amount);
    event StakeLocked(address indexed account, uint256 totalStaked, uint256 unstakeDelaySec);
    event StakeUnlocked(address indexed account, uint256 withdrawTime);
    event StakeWithdrawn(address indexed account, address withdrawAddress, uint256 amount);

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

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

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

    function addStake(uint32 unstakeDelaySec) external payable {
        DepositInfo storage info = _deposits[msg.sender];
        require(unstakeDelaySec > 0, "EP: must specify unstake delay");
        require(unstakeDelaySec >= info.unstakeDelaySec, "EP: cannot decrease unstake time");
        uint256 stake = info.stake + msg.value;
        require(stake <= type(uint112).max, "EP: stake overflow");
        info.stake = uint112(stake);
        info.unstakeDelaySec = unstakeDelaySec;
        info.staked = true;
        info.withdrawTime = 0;
        emit StakeLocked(msg.sender, stake, unstakeDelaySec);
    }

    function unlockStake() external {
        DepositInfo storage info = _deposits[msg.sender];
        require(info.staked, "EP: not staked");
        info.withdrawTime = uint48(block.timestamp + info.unstakeDelaySec);
        info.staked = false;
        emit StakeUnlocked(msg.sender, info.withdrawTime);
    }

    function withdrawStake(address payable to) external nonReentrant {
        DepositInfo storage info = _deposits[msg.sender];
        require(info.withdrawTime > 0, "EP: must unlock first");
        require(block.timestamp >= info.withdrawTime, "EP: stake withdrawal not yet due");
        uint256 stake = info.stake;
        info.stake = 0;
        info.withdrawTime = 0;
        (bool ok, ) = to.call{ value: stake }("");
        require(ok, "EP: stake transfer failed");
        emit StakeWithdrawn(msg.sender, to, stake);
    }

    function getDepositInfo(address account) external view returns (DepositInfo memory) {
        return _deposits[account];
    }

    // ───────────────────── UserOp execution ─────────────────────
    //
    // For the first cut of AERE's paymaster stack, we expose the same call
    // signature that bundlers expect (handleOps), but defer to a paymaster's
    // validate hook and let the bundler relay the actual call. This keeps the
    // EntryPoint surface area small and auditable.
    //
    // A bundler integration writeup is in /docs/paymasters.md.
    //
    // For app-level smart accounts that prefer to use a fully-featured EntryPoint,
    // they can target the canonical 0x000…d0032 address — once we deploy a
    // v5-compatible build of the eth-infinitism EntryPoint via CREATE2 in a
    // future hardfork.

    event UserOpRequested(address indexed sender, address indexed paymaster, uint256 nonce);

    /// Lightweight UserOp acceptance: validates with the paymaster, executes the
    /// inner call, then deducts the paymaster's deposit by `actualGasCost`.
    /// NOT a complete bundler implementation — bundlers should use the canonical
    /// EntryPoint until we migrate fully. This exists so tests and direct
    /// integration can flow end-to-end.
    function relayUserOp(
        PackedUserOperation calldata userOp
    ) external payable nonReentrant returns (uint256 actualGasCost) {
        require(userOp.paymasterAndData.length >= 20, "EP: paymaster required");
        address paymaster = address(bytes20(userOp.paymasterAndData[0:20]));

        uint256 preGas = gasleft();

        // Ask paymaster to validate. Will revert if rejected.
        (bytes memory context, ) = PaymasterBase(payable(paymaster))
            .validatePaymasterUserOp(userOp, bytes32(0), tx.gasprice * 1_000_000);

        // Execute the call from the sender (sender must be a smart account that
        // accepts EntryPoint as its trusted caller — same trust model as canonical EP).
        (bool ok, ) = userOp.sender.call(userOp.callData);
        require(ok, "EP: sender call reverted");

        actualGasCost = (preGas - gasleft()) * tx.gasprice;

        // Charge the paymaster's deposit.
        require(_deposits[paymaster].deposit >= actualGasCost, "EP: paymaster underfunded");
        _deposits[paymaster].deposit -= actualGasCost;

        // Notify paymaster for any post-op accounting it wants to do.
        PaymasterBase(payable(paymaster)).postOp(
            PaymasterBase.PostOpMode.opSucceeded,
            context,
            actualGasCost,
            tx.gasprice
        );

        emit UserOpRequested(userOp.sender, paymaster, userOp.nonce);
    }

    receive() external payable {
        _deposits[msg.sender].deposit += msg.value;
        emit Deposited(msg.sender, _deposits[msg.sender].deposit);
    }
}
