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

import "./paymaster/PaymasterBase.sol";

/**
 * @dev CORRECTED interfaces to the REAL deployed staking contracts on chain 2800.
 *
 *  AereStaking (0xAbDb01d9A4f41792129b2654Fb6DDB9689360DEc) does NOT expose
 *  balanceOf(address). A delegator's stake is stored per (validator, delegator)
 *  in the public `delegations` mapping, so a delegator's TOTAL delegated amount
 *  is the sum over every validator of delegations[validator][delegator].amount.
 *  A user that is itself an active validator also has validators[user].selfStake.
 *  There is no single-argument aggregate getter on the real contract, so we read
 *  the real public getters and aggregate (bounded).
 */
interface IAereStaking {
    // public getter for `mapping(address => Validator) public validators`
    function validators(address v) external view returns (
        bool active,
        uint256 selfStake,
        uint256 totalDelegated,
        uint256 commissionBps,
        uint256 lastRewardBlock,
        uint256 accumulatedRewards
    );
    // public getter for `mapping(address => mapping(address => Delegation)) public delegations`
    function delegations(address validator, address delegator) external view returns (
        uint256 amount,
        uint256 stakedAt,
        uint256 lastClaimBlock
    );
    // public getter for `address[] public validatorList`
    function validatorList(uint256 index) external view returns (address);
    function validatorCount() external view returns (uint256);
}

/**
 * @dev CORRECTED getLock field order to match the REAL AereLockedStaking
 *  (0x21108c28A849b05aE6b7a3a5bc435C9Bc897E7Ad), whose Lock struct is:
 *      struct Lock { uint256 amount; uint64 startedAt; uint64 unlockAt; uint8 tier; bool withdrawn; }
 *  getLock returns the struct in THIS order (amount first). The V1 paymaster
 *  declared (uint8 tier, uint256 amount, ...) which mis-decodes the first word
 *  (amount = 1e21) as a uint8, failing the ABI validity check and reverting —
 *  a revert NOT swallowed by the surrounding try/catch, bricking any real locker.
 */
interface IAereLockedStaking {
    function lockCount(address user) external view returns (uint256);
    function getLock(address user, uint256 lockId) external view returns (
        uint256 amount, uint64 startedAt, uint64 unlockAt, uint8 tier, bool withdrawn
    );
}

/**
 * @title AereStakeQuotaPaymasterV2
 * @notice Bug-fix redeploy of AereStakeQuotaPaymaster (V1 at
 *         0xD16C86D792444c2667A26dB62f01b90FC0DaB87b, non-functional).
 *
 *   "Stake AERE, get free transactions" — Tron's killer UX, brought to EVM.
 *
 *   Reads the user's effective AERE stake (delegated AereStaking + own validator
 *   self-stake + active AereLockedStaking locks) and allocates a daily free-tx
 *   quota:
 *
 *     quotaPerDay = stakedAERE * txPerKiloAere / 1000
 *
 *     stake 1,000 AERE  → 20 txs/day (1 free tx per 50 AERE staked, default)
 *     stake 10,000 AERE → 200 txs/day
 *
 *   Quota resets per UTC day. Unused quota does NOT roll over.
 *
 *   ── Fixes over V1 ────────────────────────────────────────────────────────
 *   F1 (HIGH): V1 called IAereStaking.balanceOf(address), which does not exist
 *              on the real AereStaking → the delegated read was always caught and
 *              contributed 0. V2 sums the real delegations across validatorList
 *              plus the user's own validator self-stake.
 *   F2 (HIGH): V1's IAereLockedStaking.getLock field order (tier first) did not
 *              match the real struct (amount first) → decoding a real lock hard-
 *              reverted (not swallowed by try/catch), so ANY user with a real lock
 *              could never be sponsored. V2 matches the real field order exactly.
 *   F3 (MED):  the lock loop was unbounded. V2 bounds both the validator scan and
 *              the lock scan (MAX_*_SCANNED) so a single UserOp validation cannot
 *              be griefed into out-of-gas.
 *
 *   Quota logic, per-op gas cap, daily reset and try/catch tolerance are otherwise
 *   identical to V1.
 */
contract AereStakeQuotaPaymasterV2 is PaymasterBase {
    IAereStaking public immutable staking;
    IAereLockedStaking public immutable lockedStaking;

    /// Bound the per-validation loops so validation can never be griefed OOG.
    uint256 public constant MAX_LOCKS_SCANNED = 100;
    uint256 public constant MAX_VALIDATORS_SCANNED = 100;

    /// How many sponsored UserOps per 1000 AERE staked, per day.
    /// Default 20 = 1 free tx per 50 AERE staked. Owner-tunable.
    uint256 public txPerKiloAerePerDay = 20;

    /// Max gas the paymaster will cover per single UserOp.
    uint256 public maxSponsoredGasWei = 0.01 ether;

    /// (sender, day) => sponsoredCount
    mapping(bytes32 => uint256) public usedQuota;

    event Sponsored(address indexed sender, uint256 dayQuota, uint256 used);
    event ConfigChanged(uint256 txPerKilo, uint256 maxGasWei);

    constructor(IEntryPoint _ep, IAereStaking _staking, IAereLockedStaking _locked) PaymasterBase(_ep) {
        staking = _staking;
        lockedStaking = _locked;
    }

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

    function setQuotaParams(uint256 _txPerKilo, uint256 _maxGasWei) external onlyOwner {
        txPerKiloAerePerDay = _txPerKilo;
        maxSponsoredGasWei = _maxGasWei;
        emit ConfigChanged(_txPerKilo, _maxGasWei);
    }

    // ───────────────────── Quota views ─────────────────────

    /// Effective AERE stake of a user =
    ///   delegated (sum over validators) + own validator self-stake + active locks.
    /// Every external read is wrapped in try/catch so a misbehaving/paused source
    /// degrades that user's quota to 0 rather than bricking validation. Both loops
    /// are bounded by MAX_*_SCANNED.
    function stakedOf(address user) public view returns (uint256 total) {
        // 1. Own validator self-stake (if the user is an active validator).
        try staking.validators(user) returns (
            bool active, uint256 selfStake, uint256, uint256, uint256, uint256
        ) {
            if (active) total += selfStake;
        } catch {}

        // 2. Delegated stake, summed across the validator set (bounded).
        try staking.validatorCount() returns (uint256 vn) {
            uint256 vcap = vn < MAX_VALIDATORS_SCANNED ? vn : MAX_VALIDATORS_SCANNED;
            for (uint256 i = 0; i < vcap; i++) {
                try staking.validatorList(i) returns (address v) {
                    try staking.delegations(v, user) returns (uint256 amount, uint256, uint256) {
                        total += amount;
                    } catch {}
                } catch {}
            }
        } catch {}

        // 3. Active locked stake (bounded).
        try lockedStaking.lockCount(user) returns (uint256 n) {
            uint256 lcap = n < MAX_LOCKS_SCANNED ? n : MAX_LOCKS_SCANNED;
            for (uint256 i = 0; i < lcap; i++) {
                try lockedStaking.getLock(user, i) returns (
                    uint256 amount, uint64 /*startedAt*/, uint64 unlockAt, uint8 /*tier*/, bool withdrawn
                ) {
                    if (!withdrawn && block.timestamp < unlockAt) {
                        total += amount;
                    }
                } catch {}
            }
        } catch {}
    }

    function dailyQuotaOf(address user) public view returns (uint256) {
        return (stakedOf(user) * txPerKiloAerePerDay) / 1000 ether;
    }

    function quotaRemaining(address user) external view returns (uint256 remaining, uint256 dailyQuota) {
        dailyQuota = dailyQuotaOf(user);
        uint256 today = block.timestamp / 1 days;
        bytes32 k = keccak256(abi.encodePacked(user, today));
        uint256 used = usedQuota[k];
        remaining = used >= dailyQuota ? 0 : dailyQuota - used;
    }

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

    function _validatePaymasterUserOp(
        PackedUserOperation calldata userOp,
        bytes32 /*userOpHash*/,
        uint256 maxCost
    ) internal override returns (bytes memory context, uint256 validationData) {
        require(maxCost <= maxSponsoredGasWei, "StakePM: tx too expensive");

        address sender = userOp.sender;
        uint256 dailyQuota = dailyQuotaOf(sender);
        require(dailyQuota > 0, "StakePM: stake more AERE");

        uint256 today = block.timestamp / 1 days;
        bytes32 k = keccak256(abi.encodePacked(sender, today));
        uint256 used = usedQuota[k];
        require(used < dailyQuota, "StakePM: daily quota exhausted");

        usedQuota[k] = used + 1;
        emit Sponsored(sender, dailyQuota, used + 1);
        return ("", 0);
    }
}
