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

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

/**
 * @title AereFeeMonetization
 * @notice Pay developers (and Foundation treasury) automatically when their contracts
 *         get used. Modelled on Mode Network's Sequencer Fee Sharing pattern:
 *
 *           - Developer calls register(contractAddr, payoutAddr) and receives an
 *             ERC-721 NFT representing the right to collect fees from contractAddr.
 *           - An off-chain distributor (running off chain) parses recent blocks,
 *             attributes gas per top-level contract, and calls distribute() with
 *             per-tokenId amounts.
 *           - NFT owner calls claim(tokenId) to withdraw accumulated AERE.
 *
 *         The NFT is transferable, so fee streams become sellable / collateralisable.
 *
 *         Plus: a fixed `treasuryBps` slice that always flows to AERE Foundation
 *         (separate from per-contract dev-share). Default 500 = 5%.
 *
 *   Fee distribution per gas fee paid (set by Foundation governance):
 *     burnBps (Tier 1.8): 3750 = 37.5% (permanent burn)
 *     treasuryBps (this tier): 500 = 5% (always to Foundation)
 *     devShareBps (this tier): 2000 = 20% (to registered NFT owner, else to validator)
 *     validatorBps (residual): 3750 = 37.5% (to block validator)
 *
 *   Foundation registers its own foundational contracts (AereSwapRouter,
 *   AereSettlement, AereMessenger, AereNFTMarketplace, etc.) so their dev-share
 *   flows back to Foundation. External devs register their own; their NFT owner
 *   collects that fee stream.
 */
contract AereFeeMonetization is ERC721, Ownable, ReentrancyGuard {
    /// Treasury address. All `treasuryBps` revenue flows here.
    address payable public treasury;

    /// Dev-share rate in basis points (10000 = 100%). Hard cap at 3000 (30%).
    uint256 public devShareBps = 2000;
    /// Treasury slice in basis points. Hard cap at 1500 (15%).
    uint256 public treasuryBps = 500;
    uint256 public constant BPS = 10_000;

    /// Authorised distributors (Foundation runs these — same trust model as Tier 1.8).
    mapping(address => bool) public isDistributor;

    /// Auto-incrementing tokenId.
    uint256 public nextTokenId = 1;

    /// tokenId → contract that earns this share.
    mapping(uint256 => address) public earningContract;
    /// contractAddr → tokenId (0 if not registered).
    mapping(address => uint256) public tokenIdOf;
    /// tokenId → unclaimed AERE balance.
    mapping(uint256 => uint256) public pendingRewards;

    /// Lifetime cumulative paid to each NFT and to treasury.
    mapping(uint256 => uint256) public totalPaidToToken;
    uint256 public totalPaidToTreasury;

    event Registered(address indexed contractAddr, uint256 indexed tokenId, address indexed payout);
    event Distributed(uint256 indexed tokenId, uint256 amount, uint256 newPending);
    event TreasuryFunded(uint256 amount, uint256 newTotal);
    event Claimed(uint256 indexed tokenId, address indexed to, uint256 amount);
    event RateChanged(string what, uint256 oldValue, uint256 newValue);
    event DistributorChanged(address indexed distributor, bool authorised);
    event TreasuryChanged(address indexed previous, address indexed next);

    modifier onlyDistributor() {
        require(isDistributor[msg.sender], "FeeMon: not a distributor");
        _;
    }

    constructor(address payable _treasury) ERC721("AERE Fee Monetization", "AERE-FM") {
        require(_treasury != address(0), "FeeMon: zero treasury");
        treasury = _treasury;
    }

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

    function setTreasury(address payable _treasury) external onlyOwner {
        require(_treasury != address(0), "FeeMon: zero treasury");
        emit TreasuryChanged(treasury, _treasury);
        treasury = _treasury;
    }

    function setDevShareBps(uint256 _bps) external onlyOwner {
        require(_bps <= 3000, "FeeMon: devShare too high");
        emit RateChanged("devShareBps", devShareBps, _bps);
        devShareBps = _bps;
    }

    function setTreasuryBps(uint256 _bps) external onlyOwner {
        require(_bps <= 1500, "FeeMon: treasuryBps too high");
        emit RateChanged("treasuryBps", treasuryBps, _bps);
        treasuryBps = _bps;
    }

    function setDistributor(address dist, bool ok) external onlyOwner {
        isDistributor[dist] = ok;
        emit DistributorChanged(dist, ok);
    }

    // ───────────────────── Registration ─────────────────────

    /**
     * @notice Register a contract for fee-share. The caller becomes the initial
     *         NFT owner. payoutAddr is informational — the NFT owner at claim time
     *         is who receives the AERE.
     */
    function register(address contractAddr, address payoutAddr) external returns (uint256 tokenId) {
        require(contractAddr != address(0), "FeeMon: zero contract");
        require(tokenIdOf[contractAddr] == 0, "FeeMon: already registered");
        require(payoutAddr != address(0), "FeeMon: zero payout");

        tokenId = nextTokenId++;
        earningContract[tokenId] = contractAddr;
        tokenIdOf[contractAddr] = tokenId;

        _mint(payoutAddr, tokenId);
        emit Registered(contractAddr, tokenId, payoutAddr);
    }

    /// Convenience: Foundation registers in bulk.
    function registerBatch(address[] calldata contractAddrs, address payoutAddr)
        external onlyOwner returns (uint256[] memory tokenIds)
    {
        tokenIds = new uint256[](contractAddrs.length);
        for (uint256 i = 0; i < contractAddrs.length; i++) {
            require(tokenIdOf[contractAddrs[i]] == 0, "FeeMon: already registered");
            uint256 tid = nextTokenId++;
            earningContract[tid] = contractAddrs[i];
            tokenIdOf[contractAddrs[i]] = tid;
            _mint(payoutAddr, tid);
            emit Registered(contractAddrs[i], tid, payoutAddr);
            tokenIds[i] = tid;
        }
    }

    // ───────────────────── Distribution (called by Foundation distributor) ─────────────────────

    /**
     * @notice Credit per-tokenId fee shares. The distributor sends `totalAmount`
     *         as msg.value. Sum of `amounts` plus the treasury slice = totalAmount.
     */
    function distribute(
        uint256[] calldata tokenIds,
        uint256[] calldata amounts,
        uint256 treasuryAmount
    ) external payable onlyDistributor nonReentrant {
        require(tokenIds.length == amounts.length, "FeeMon: length mismatch");
        uint256 sum = treasuryAmount;
        for (uint256 i = 0; i < amounts.length; i++) {
            sum += amounts[i];
        }
        require(sum == msg.value, "FeeMon: amount mismatch");

        // Treasury slice
        if (treasuryAmount > 0) {
            (bool ok, ) = treasury.call{ value: treasuryAmount }("");
            require(ok, "FeeMon: treasury transfer failed");
            totalPaidToTreasury += treasuryAmount;
            emit TreasuryFunded(treasuryAmount, totalPaidToTreasury);
        }

        // Per-token shares (held in contract until claim)
        for (uint256 i = 0; i < tokenIds.length; i++) {
            require(earningContract[tokenIds[i]] != address(0), "FeeMon: bad tokenId");
            pendingRewards[tokenIds[i]] += amounts[i];
            totalPaidToToken[tokenIds[i]] += amounts[i];
            emit Distributed(tokenIds[i], amounts[i], pendingRewards[tokenIds[i]]);
        }
    }

    // ───────────────────── Claim ─────────────────────

    function claim(uint256 tokenId) external nonReentrant returns (uint256 amount) {
        require(ownerOf(tokenId) == msg.sender, "FeeMon: not NFT owner");
        amount = pendingRewards[tokenId];
        require(amount > 0, "FeeMon: nothing to claim");
        pendingRewards[tokenId] = 0;
        (bool ok, ) = payable(msg.sender).call{ value: amount }("");
        require(ok, "FeeMon: claim transfer failed");
        emit Claimed(tokenId, msg.sender, amount);
    }

    /// View: how much would the current NFT owner withdraw if they called claim() now?
    function claimableFor(address holder) external view returns (uint256 total) {
        // Walk via balanceOf — gas-heavy, intended for off-chain UI only.
        uint256 bal = balanceOf(holder);
        if (bal == 0) return 0;
        // ERC-721 doesn't index tokens by owner natively in OZ basic. Caller should
        // pass tokenIds explicitly via pendingRewards mapping for production reads.
        // This helper is a UI-friendly shortcut for small wallets.
        for (uint256 t = 1; t < nextTokenId; t++) {
            if (_ownerOf(t) == holder) {
                total += pendingRewards[t];
            }
        }
    }

    // ───────────────────── Stats ─────────────────────

    function policy() external view returns (uint256 burnBps_, uint256 devShareBps_, uint256 treasuryBps_, uint256 validatorBps_) {
        burnBps_ = 3750; // Tier 1.8 - documented here for clarity only; actual burn lives in AereCoinbaseSplitter
        devShareBps_ = devShareBps;
        treasuryBps_ = treasuryBps;
        // Validator residual = BPS - burn - devShare(if registered) - treasury
        validatorBps_ = BPS - 3750 - devShareBps - treasuryBps;
    }

    receive() external payable {}
}
