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

import "./paymaster/PaymasterBase.sol";

/**
 * @title AereOnboardingPaymaster
 * @notice Foundation-funded paymaster for first-time AERE users.
 *
 *   Hard-capped onboarding paymaster:
 *     - Each address gets exactly N sponsored UserOps (lifetime — never resets)
 *     - Sitewide daily UserOp cap (Sybil-attack protection)
 *     - Optional whitelist of contract addresses we'll sponsor calls to
 *     - Total balance is whatever Foundation pre-funded. When drained, paymaster
 *       refuses all UserOps until Foundation tops up (which requires a governance
 *       decision, not an automated refill).
 *
 *   Math example: with 3 sponsored txs/address and a 100-tx/day sitewide cap,
 *   100,000 unique addresses cost ~300,000 gas-paid-by-paymaster txs. Even at
 *   0.001 AERE/tx that's 300 AERE total to onboard 100k users.
 */
contract AereOnboardingPaymaster is PaymasterBase {
    /// Lifetime sponsored UserOp cap per sender address.
    uint256 public sponsoredOpsPerSender = 3;

    /// Sitewide UserOp cap per UTC day (rolling: floor(block.timestamp / 1 days)).
    uint256 public dailySitewideCap = 100;

    /// Set to true to limit sponsorship to specific target contracts only.
    bool public targetWhitelistEnabled = false;

    /// Whitelist of target contracts whose function calls we will sponsor.
    mapping(address => bool) public targetWhitelist;

    /// Per-sender lifetime count of UserOps the paymaster has sponsored.
    mapping(address => uint256) public sponsoredCount;

    /// Per-day sitewide counter (key = unix day).
    mapping(uint256 => uint256) public dailyCount;

    event Sponsored(address indexed sender, uint256 newSenderCount, uint256 todayCount);
    event ConfigChanged(uint256 perSender, uint256 dailyCap, bool whitelistEnabled);
    event TargetWhitelist(address indexed target, bool allowed);

    constructor(IEntryPoint _ep) PaymasterBase(_ep) {}

    // ───────────────────── Admin ─────────────────────

    function setLimits(uint256 _perSender, uint256 _dailyCap, bool _whitelist) external onlyOwner {
        sponsoredOpsPerSender = _perSender;
        dailySitewideCap = _dailyCap;
        targetWhitelistEnabled = _whitelist;
        emit ConfigChanged(_perSender, _dailyCap, _whitelist);
    }

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

    // ───────────────────── Paymaster hook ─────────────────────

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

        // Per-sender lifetime cap
        require(sponsoredCount[sender] < sponsoredOpsPerSender, "OnboardingPM: sender exhausted");

        // Sitewide daily cap
        uint256 today = block.timestamp / 1 days;
        require(dailyCount[today] < dailySitewideCap, "OnboardingPM: daily cap reached");

        // Optional target whitelist — first 4 bytes of callData are the function selector;
        // the called contract is encoded in `userOp.callData` after the selector. We don't
        // decode the inner call here for gas reasons; whitelist by target is enforced via
        // the SmartAccount's `execute(target, value, data)` callData convention which
        // places `target` at bytes 16..36 of callData (standard ERC-4337 SimpleAccount).
        if (targetWhitelistEnabled) {
            bytes calldata cd = userOp.callData;
            require(cd.length >= 36, "OnboardingPM: bad callData");
            address target;
            // solhint-disable-next-line no-inline-assembly
            assembly {
                // callData layout: 4 bytes selector + 12 padding + 20 bytes target + ...
                target := shr(96, calldataload(add(cd.offset, 16)))
            }
            require(targetWhitelist[target], "OnboardingPM: target not allowed");
        }

        sponsoredCount[sender] += 1;
        dailyCount[today] += 1;

        emit Sponsored(sender, sponsoredCount[sender], dailyCount[today]);
        return ("", 0); // validationData=0 means: no signature, no time range, valid
    }
}
