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

import "./paymaster/PaymasterBase.sol";

/**
 * @title AereAppPaymaster
 * @notice Generic dApp-funded paymaster. Each dApp on AERE deploys one of these
 *         and funds it from their own treasury. The paymaster sponsors gas only
 *         for UserOps calling contracts on the dApp's whitelist, and (optionally)
 *         only for senders on the dApp's allowlist.
 *
 *   Use case: Bank28 deploys their own AereAppPaymaster, funds it with X AERE
 *   per month from their CAC budget, whitelists Bank28's contracts as sponsorship
 *   targets, and optionally allowlists the senders they've authenticated through
 *   the Bank28 app. Foundation contributes zero.
 *
 *   Same pattern works for any dApp: NFT marketplaces, games, DeFi apps.
 */
contract AereAppPaymaster is PaymasterBase {
    /// Optional sender allowlist. If enabled, only allowlisted senders can be sponsored.
    bool public senderAllowlistEnabled;
    mapping(address => bool) public senderAllowlist;

    /// Required target whitelist. UserOps must invoke an allowed target contract.
    mapping(address => bool) public targetWhitelist;

    /// Per-sender lifetime sponsorship cap. 0 = unlimited.
    uint256 public maxOpsPerSender;
    mapping(address => uint256) public opsCountBySender;

    event SenderAllowlistChanged(bool enabled);
    event SenderAllowlist(address indexed sender, bool allowed);
    event TargetWhitelist(address indexed target, bool allowed);
    event ConfigChanged(uint256 maxOpsPerSender);
    event Sponsored(address indexed sender, address indexed target, uint256 newSenderCount);

    constructor(IEntryPoint _ep, address _owner) PaymasterBase(_ep) {
        _transferOwnership(_owner);
    }

    function setSenderAllowlist(bool enabled) external onlyOwner {
        senderAllowlistEnabled = enabled;
        emit SenderAllowlistChanged(enabled);
    }

    function setAllowedSender(address sender, bool allowed) external onlyOwner {
        senderAllowlist[sender] = allowed;
        emit SenderAllowlist(sender, allowed);
    }

    function setTargetWhitelist(address target, bool allowed) external onlyOwner {
        targetWhitelist[target] = allowed;
        emit TargetWhitelist(target, allowed);
    }

    function setMaxOpsPerSender(uint256 max) external onlyOwner {
        maxOpsPerSender = max;
        emit ConfigChanged(max);
    }

    function _validatePaymasterUserOp(
        PackedUserOperation calldata userOp,
        bytes32 /*userOpHash*/,
        uint256 /*maxCost*/
    ) internal override returns (bytes memory context, uint256 validationData) {
        address sender = userOp.sender;

        if (senderAllowlistEnabled) {
            require(senderAllowlist[sender], "AppPM: sender not allowed");
        }

        if (maxOpsPerSender > 0) {
            require(opsCountBySender[sender] < maxOpsPerSender, "AppPM: sender quota exhausted");
        }

        // Decode target from SimpleAccount-compatible execute(target, value, data) callData layout.
        bytes calldata cd = userOp.callData;
        require(cd.length >= 36, "AppPM: bad callData");
        address target;
        assembly {
            target := shr(96, calldataload(add(cd.offset, 16)))
        }
        require(targetWhitelist[target], "AppPM: target not allowed");

        opsCountBySender[sender] += 1;
        emit Sponsored(sender, target, opsCountBySender[sender]);
        return ("", 0);
    }
}

/**
 * @title AereAppPaymasterFactory
 * @notice One-call factory for any dApp to spin up its own AereAppPaymaster.
 *         The dApp's deployer ends up as owner of the new paymaster.
 *         No registration fee. Anyone can deploy.
 */
contract AereAppPaymasterFactory {
    IEntryPoint public immutable entryPoint;

    event PaymasterCreated(address indexed deployer, address indexed paymaster);

    constructor(IEntryPoint _ep) {
        entryPoint = _ep;
    }

    function createPaymaster() external returns (address paymaster) {
        AereAppPaymaster p = new AereAppPaymaster(entryPoint, msg.sender);
        paymaster = address(p);
        emit PaymasterCreated(msg.sender, paymaster);
    }
}
