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

import "@openzeppelin/contracts/access/Ownable.sol";

/// @dev Minimal subset of ERC-4337 v0.7 IEntryPoint needed by paymasters.
interface IEntryPoint {
    function depositTo(address account) external payable;
    function balanceOf(address account) external view returns (uint256);
    function withdrawTo(address payable withdrawAddress, uint256 amount) external;
    function addStake(uint32 unstakeDelaySec) external payable;
    function unlockStake() external;
    function withdrawStake(address payable withdrawAddress) external;
}

/// @dev ERC-4337 v0.7 PackedUserOperation struct (used by paymaster hook).
struct PackedUserOperation {
    address sender;
    uint256 nonce;
    bytes initCode;
    bytes callData;
    bytes32 accountGasLimits;
    uint256 preVerificationGas;
    bytes32 gasFees;
    bytes paymasterAndData;
    bytes signature;
}

/**
 * @title PaymasterBase
 * @notice Minimal in-house BasePaymaster compatible with OpenZeppelin v4.9.
 *         Avoids the OZ-v5 dependency that the upstream eth-infinitism package pulls in.
 *
 *   ERC-4337 v0.7 spec:
 *     - validatePaymasterUserOp returns (context, validationData)
 *     - postOp is called with (mode, context, actualGasCost, actualUserOpFeePerGas)
 *
 *   Subclasses override `_validatePaymasterUserOp` and (optionally) `_postOp`.
 */
abstract contract PaymasterBase is Ownable {
    IEntryPoint public immutable entryPoint;

    enum PostOpMode { opSucceeded, opReverted, postOpReverted }

    modifier onlyEntryPoint() {
        require(msg.sender == address(entryPoint), "Paymaster: not from EntryPoint");
        _;
    }

    constructor(IEntryPoint _entryPoint) {
        entryPoint = _entryPoint;
    }

    /// EntryPoint calls this during UserOp validation.
    function validatePaymasterUserOp(
        PackedUserOperation calldata userOp,
        bytes32 userOpHash,
        uint256 maxCost
    ) external onlyEntryPoint returns (bytes memory context, uint256 validationData) {
        return _validatePaymasterUserOp(userOp, userOpHash, maxCost);
    }

    /// EntryPoint calls this after UserOp execution (if the paymaster requested it via context).
    function postOp(
        PostOpMode mode,
        bytes calldata context,
        uint256 actualGasCost,
        uint256 actualUserOpFeePerGas
    ) external onlyEntryPoint {
        _postOp(mode, context, actualGasCost, actualUserOpFeePerGas);
    }

    function _validatePaymasterUserOp(
        PackedUserOperation calldata userOp,
        bytes32 userOpHash,
        uint256 maxCost
    ) internal virtual returns (bytes memory context, uint256 validationData);

    function _postOp(
        PostOpMode /*mode*/,
        bytes calldata /*context*/,
        uint256 /*actualGasCost*/,
        uint256 /*actualUserOpFeePerGas*/
    ) internal virtual {
        // Default: no-op. Most paymasters don't need post-op logic.
    }

    // ───────────────────── EntryPoint deposit helpers ─────────────────────

    /// Fund the paymaster's gas deposit at the EntryPoint.
    function deposit() external payable {
        entryPoint.depositTo{ value: msg.value }(address(this));
    }

    function depositBalance() external view returns (uint256) {
        return entryPoint.balanceOf(address(this));
    }

    function withdrawDeposit(address payable to, uint256 amount) external onlyOwner {
        entryPoint.withdrawTo(to, amount);
    }

    function addStake(uint32 unstakeDelaySec) external payable onlyOwner {
        entryPoint.addStake{ value: msg.value }(unstakeDelaySec);
    }

    function unlockStake() external onlyOwner { entryPoint.unlockStake(); }

    function withdrawStake(address payable to) external onlyOwner {
        entryPoint.withdrawStake(to);
    }

    receive() external payable {}
}
