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

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

/**
 * @title AereMiningDistributor
 * @notice Merkle-claim payout distributor for AireFlow / mining-subscription
 *         epoch rewards.
 *
 *  Workflow each epoch (default = 30 days, matches AereMiningSubscription):
 *
 *    1. Foundation runs an off-chain scheduler that walks all active
 *       subscriptions, computes each subscriber's reward share (a function
 *       of tier + days-active), produces a Merkle tree of
 *       (subscriber, amount) leaves and a root.
 *    2. Foundation funds this contract with the epoch's total AERE payout
 *       by plain transfer or calling `fundEpoch{value:X}()`.
 *    3. Foundation calls `postEpoch(root, totalAllocated, claimDeadline)`.
 *       The new epoch id is returned. The root is immutable for that epoch.
 *    4. Each subscriber claims via `claim(epochId, index, amount, proof)`.
 *       The contract pays them in native AERE.
 *    5. After `claimDeadline`, Foundation may `sweepUnclaimed(epochId)`
 *       to recover unclaimed funds (rolled into the next epoch's bucket).
 *
 *  Funding source: AereMiningReserve wallet (whitepaper §3.2, 1.4B AERE
 *  earmarked over the network's life).
 *
 *  This is not in custody of subscribers — they pull, contract never pushes.
 *  Single-claim guard: bitmap per epoch indexed by leaf index.
 */
contract AereMiningDistributor is Ownable, ReentrancyGuard {
    struct Epoch {
        bytes32 merkleRoot;
        uint256 totalAllocated;  // funds reserved for this epoch's leaves
        uint256 totalClaimed;
        uint64  claimDeadline;
        bool    swept;
    }

    Epoch[] public epochs;
    /// epochId => packed bitmap (1 bit per leaf index claimed)
    mapping(uint256 => mapping(uint256 => uint256)) private claimedBitmap;

    event EpochPosted(uint256 indexed epochId, bytes32 merkleRoot, uint256 totalAllocated, uint64 claimDeadline);
    event Funded(address indexed from, uint256 amount);
    event Claimed(uint256 indexed epochId, uint256 indexed index, address indexed account, uint256 amount);
    event Swept(uint256 indexed epochId, uint256 unclaimed);

    receive() external payable { emit Funded(msg.sender, msg.value); }
    function fundEpoch() external payable { emit Funded(msg.sender, msg.value); }

    function epochCount() external view returns (uint256) { return epochs.length; }

    function postEpoch(bytes32 merkleRoot, uint256 totalAllocated, uint64 claimDeadline)
        external onlyOwner returns (uint256 epochId)
    {
        require(merkleRoot != bytes32(0), "MiningDist: zero root");
        require(totalAllocated > 0, "MiningDist: zero total");
        require(claimDeadline > block.timestamp, "MiningDist: deadline past");
        require(address(this).balance >= _liveFunds() + totalAllocated, "MiningDist: underfunded");
        epochs.push(Epoch({
            merkleRoot: merkleRoot,
            totalAllocated: totalAllocated,
            totalClaimed: 0,
            claimDeadline: claimDeadline,
            swept: false
        }));
        epochId = epochs.length - 1;
        emit EpochPosted(epochId, merkleRoot, totalAllocated, claimDeadline);
    }

    function isClaimed(uint256 epochId, uint256 index) public view returns (bool) {
        uint256 wordIndex = index / 256;
        uint256 bitIndex  = index % 256;
        uint256 word = claimedBitmap[epochId][wordIndex];
        return (word & (1 << bitIndex)) != 0;
    }

    function _setClaimed(uint256 epochId, uint256 index) private {
        uint256 wordIndex = index / 256;
        uint256 bitIndex  = index % 256;
        claimedBitmap[epochId][wordIndex] |= (1 << bitIndex);
    }

    function claim(uint256 epochId, uint256 index, address account, uint256 amount, bytes32[] calldata proof)
        external nonReentrant
    {
        require(epochId < epochs.length, "MiningDist: bad epoch");
        Epoch storage e = epochs[epochId];
        require(block.timestamp <= e.claimDeadline, "MiningDist: expired");
        require(!isClaimed(epochId, index), "MiningDist: already claimed");

        bytes32 leaf = keccak256(abi.encodePacked(index, account, amount));
        require(_verify(proof, e.merkleRoot, leaf), "MiningDist: bad proof");

        _setClaimed(epochId, index);
        e.totalClaimed += amount;
        require(e.totalClaimed <= e.totalAllocated, "MiningDist: over-allocated");

        (bool ok, ) = account.call{ value: amount }("");
        require(ok, "MiningDist: payout failed");
        emit Claimed(epochId, index, account, amount);
    }

    function sweepUnclaimed(uint256 epochId) external onlyOwner nonReentrant {
        require(epochId < epochs.length, "MiningDist: bad epoch");
        Epoch storage e = epochs[epochId];
        require(!e.swept, "MiningDist: already swept");
        require(block.timestamp > e.claimDeadline, "MiningDist: too early");
        uint256 unclaimed = e.totalAllocated - e.totalClaimed;
        e.swept = true;
        if (unclaimed > 0) {
            (bool ok, ) = msg.sender.call{ value: unclaimed }("");
            require(ok, "MiningDist: sweep failed");
        }
        emit Swept(epochId, unclaimed);
    }

    /// Funds still reserved by every un-swept epoch, INCLUDING epochs whose
    /// claimDeadline has passed but that have not yet been swept. An expired
    /// epoch still physically holds its unclaimed AERE (sweepUnclaimed sends
    /// exactly that reserve to the owner), so it must keep reserving those
    /// funds until it is actually swept — otherwise the same wei could both
    /// back a freshly posted epoch AND be swept to the owner, double-counting
    /// it and stranding the new epoch's honest claimants. Reservation leaves
    /// _liveFunds exactly once: when sweepUnclaimed sets `swept = true`.
    function _liveFunds() internal view returns (uint256 reserved) {
        for (uint256 i = 0; i < epochs.length; i++) {
            Epoch storage e = epochs[i];
            if (!e.swept) {
                reserved += e.totalAllocated - e.totalClaimed;
            }
        }
    }

    function _verify(bytes32[] calldata proof, bytes32 root, bytes32 leaf) internal pure returns (bool) {
        bytes32 hash = leaf;
        for (uint256 i = 0; i < proof.length; i++) {
            bytes32 p = proof[i];
            hash = hash < p ? keccak256(abi.encodePacked(hash, p)) : keccak256(abi.encodePacked(p, hash));
        }
        return hash == root;
    }
}
