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

import "./paymaster/PaymasterBase.sol";

interface IERC20 {
    function transfer(address to, uint256 amount) external returns (bool);
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
    function approve(address spender, uint256 amount) external returns (bool);
    function balanceOf(address) external view returns (uint256);
    function decimals() external view returns (uint8);
}

interface IAereOracle {
    /// Returns (price scaled 1e8, ts, contributors). Same interface as AereLending used.
    function getPrice(bytes32 symbol) external view returns (uint128 price, uint64 timestamp, uint256 contributors);
}

/**
 * @title AereTokenPaymasterV2
 * @notice Bug-fix redeploy of AereTokenPaymaster (V1 at
 *         0xEb6e2Eb24e597C85392DdCD68a1F9b654FffdcB2).
 *
 *   Pay AERE gas with any whitelisted ERC-20.
 *
 *   ── Fix over V1 (MEDIUM) ──────────────────────────────────────────────────
 *   V1.withdrawToken used IERC20(token).transferFrom(address(this), to, amount),
 *   which requires the paymaster to hold an ERC-20 allowance for itself. No such
 *   self-allowance is ever set, so on standard (OZ) ERC-20s the call reverts with
 *   "ERC20: insufficient allowance" and every token the paymaster collects as gas
 *   revenue is permanently stuck. V2.withdrawToken uses transfer(to, amount) — the
 *   correct primitive when moving the contract's OWN balance — so the owner
 *   (Foundation) can sweep collected tokens.
 *
 *   Everything else (oracle pricing, markup, validate hook, token config) is
 *   byte-for-byte identical to V1.
 *
 *   How it works:
 *     1. User submits a UserOperation with paymasterData = (token, maxTokenCost).
 *     2. We read the token's USD price from AereOracle and AERE/USD price.
 *     3. We compute the AERE cost of the op, convert to token, add markup, charge.
 *     4. We pull tokens from the user via transferFrom (requires prior approve).
 */
contract AereTokenPaymasterV2 is PaymasterBase {
    IAereOracle public oracle;
    bytes32 public constant AERE_USD = keccak256("AERE/USD");

    struct TokenConfig {
        bool enabled;
        bytes32 priceKey; // e.g. keccak256("USDT/USD") - returned by AereOracle
        uint8 decimals;
    }
    mapping(address => TokenConfig) public tokens;

    /// Markup over fair price, in basis points. 500 = 5%.
    uint256 public markupBps = 500;

    /// Fallback AERE/USD price (1e8 scale). Used while oracle has no AERE/USD feed.
    uint256 public fallbackAereUsd1e8 = 10_000_000; // $0.10

    event TokenSet(address indexed token, bool enabled, bytes32 priceKey, uint8 decimals);
    event ConfigChanged(uint256 markupBps);
    event Paid(address indexed sender, address indexed token, uint256 tokenAmount);
    event Withdrawn(address indexed token, address indexed to, uint256 amount);

    constructor(IEntryPoint _ep, IAereOracle _oracle) PaymasterBase(_ep) {
        oracle = _oracle;
    }

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

    function setToken(address token, bytes32 priceKey, uint8 dec, bool enabled) external onlyOwner {
        tokens[token] = TokenConfig(enabled, priceKey, dec);
        emit TokenSet(token, enabled, priceKey, dec);
    }

    function setMarkup(uint256 _markupBps) external onlyOwner {
        require(_markupBps <= 3000, "TokenPM: markup too high");
        markupBps = _markupBps;
        emit ConfigChanged(_markupBps);
    }

    function setFallbackAereUsd(uint256 price1e8) external onlyOwner {
        fallbackAereUsd1e8 = price1e8;
    }

    /// @notice Sweep collected ERC-20 gas revenue to `to`. FIX: transfer (own
    ///         balance), not transferFrom(address(this)) which V1 used and which
    ///         requires a self-allowance that never exists.
    function withdrawToken(address token, address to, uint256 amount) external onlyOwner {
        require(IERC20(token).transfer(to, amount), "TokenPM: withdraw failed");
        emit Withdrawn(token, to, amount);
    }

    // ───────────────────── Pricing ─────────────────────

    function _aereUsd1e8() internal view returns (uint256) {
        try oracle.getPrice(AERE_USD) returns (uint128 p, uint64, uint256) {
            if (p > 0) return uint256(p);
        } catch {}
        return fallbackAereUsd1e8;
    }

    function quoteTokenAmount(address token, uint256 aereWei) public view returns (uint256 tokenAmount) {
        TokenConfig memory cfg = tokens[token];
        require(cfg.enabled, "TokenPM: token disabled");
        (uint128 tokPrice, , ) = oracle.getPrice(cfg.priceKey);
        require(tokPrice > 0, "TokenPM: oracle missing");
        // USD value of the AERE: aereWei * AERE/USD (1e8) / 1e18
        // Token amount = USD / (token/USD * 10^(18 - tokenDecimals))
        // Combine: tokenAmount = aereWei * aereUsd / tokPrice  (both prices share 1e8 scale → cancels)
        // Adjust to token decimals: result is in 1e18 units of USD-AERE; divide by 10^(18-dec) to get token native units.
        uint256 usdAmount = (aereWei * _aereUsd1e8()) / 1e8; // 1e18 USD-scaled
        uint256 amount1e18 = (usdAmount * 1e8) / uint256(tokPrice); // 1e18 token-scaled
        if (cfg.decimals < 18) {
            tokenAmount = amount1e18 / (10 ** (18 - cfg.decimals));
        } else {
            tokenAmount = amount1e18 * (10 ** (cfg.decimals - 18));
        }
        // Apply markup
        tokenAmount = (tokenAmount * (10_000 + markupBps)) / 10_000;
    }

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

    function _validatePaymasterUserOp(
        PackedUserOperation calldata userOp,
        bytes32 /*userOpHash*/,
        uint256 maxCost
    ) internal override returns (bytes memory context, uint256 validationData) {
        // paymasterAndData layout:
        //   bytes 0..20   - paymaster address
        //   bytes 20..36  - paymaster verification gas limit + post-op gas limit (32 bytes packed)
        //   bytes 52..72  - token address (20 bytes)
        //   bytes 72..104 - maxTokenCost (32 bytes)
        require(userOp.paymasterAndData.length >= 104, "TokenPM: bad paymasterData");
        address token = address(bytes20(userOp.paymasterAndData[52:72]));
        uint256 maxTokenCost = uint256(bytes32(userOp.paymasterAndData[72:104]));

        uint256 tokenCost = quoteTokenAmount(token, maxCost);
        require(tokenCost <= maxTokenCost, "TokenPM: token cost exceeds max");

        // Pull tokens up-front. User must have pre-approved this contract.
        require(IERC20(token).transferFrom(userOp.sender, address(this), tokenCost), "TokenPM: pull failed");

        emit Paid(userOp.sender, token, tokenCost);
        return ("", 0);
    }
}
