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

import "./paymaster/PaymasterBase.sol";

interface IAereStaking {
    function balanceOf(address user) external view returns (uint256);
}

interface IAereLockedStaking {
    function lockCount(address user) external view returns (uint256);
    function getLock(address user, uint256 lockId) external view returns (
        uint8 tier, uint256 amount, uint64 startTime, uint64 unlockTime, bool withdrawn
    );
}

/**
 * @title AereStakeQuotaPaymaster
 * @notice "Stake AERE, get free transactions" — Tron's killer UX, brought to EVM.
 *
 *   Reads the user's effective AERE stake (delegated AereStaking + active
 *   AereLockedStaking locks) and allocates a daily free-tx quota:
 *
 *     quotaPerDay = stakedAERE * txPerKiloAere / 1000
 *
 *   Default: stake 1 AERE  → 0.02 txs/day (i.e. 1 free tx per 50 AERE staked, recommended floor)
 *            stake 1,000 AERE → 20 txs/day
 *            stake 10,000 AERE → 200 txs/day
 *
 *   Quota resets per UTC day. Unused quota does NOT roll over.
 *
 *   Funding: self-funding from validator-reward share (5% of block rewards routed
 *   here automatically). Foundation pre-funds the seed balance, then the validator
 *   share keeps the pool topped up.
 *
 *   Safety: per-UserOp gas-spend cap prevents one user from draining the pool with a
 *   single huge transaction.
 */
contract AereStakeQuotaPaymaster is PaymasterBase {
    IAereStaking public immutable staking;
    IAereLockedStaking public immutable lockedStaking;

    /// 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 of active locks.
    function stakedOf(address user) public view returns (uint256 total) {
        try staking.balanceOf(user) returns (uint256 d) { total += d; } catch {}
        try lockedStaking.lockCount(user) returns (uint256 n) {
            for (uint256 i = 0; i < n; i++) {
                try lockedStaking.getLock(user, i) returns (
                    uint8 /*tier*/, uint256 amount, uint64 /*start*/, uint64 unlockTime, bool withdrawn
                ) {
                    if (!withdrawn && block.timestamp < unlockTime) {
                        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);
    }
}
