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

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

/**
 * @title AereRollupSettlement — per-rollup settlement contract on AERE
 * @notice One deployment per third-party rollup registered through
 *         AereRaaSFactory. Acts as:
 *           1. State-root commitment ledger (optimistic with N-hour challenge)
 *           2. Revenue collector (sequencer collects fees on the rollup,
 *              periodically settles to this contract in DEBT_TOKEN)
 *           3. Revenue splitter (rollupOwner / sinkSplit / sequencerSplit)
 *
 *         IMMUTABILITY at deploy:
 *           - rollupId
 *           - rollupOwner address (settles revenue here)
 *           - sequencer address (proposes state roots)
 *           - DEBT_TOKEN (revenue settlement currency — typically WAERE or USDC.e)
 *           - SINK address
 *           - challengeWindow
 *           - sinkBps + rollupBps + sequencerBps (must sum to 10_000)
 *
 *         GOVERNANCE-LITE:
 *           - sequencer is rotatable by rollupOwner via setSequencer (one operational change permitted)
 *           - rollupOwner is rotatable via transferOwner
 *           - NO param changes (splits, window) post-deploy.
 *
 *         STATE ROOT FLOW:
 *           - sequencer calls proposeStateRoot(epoch, root, l2BlockHash)
 *           - root enters pending state; finalises automatically once
 *             challengeWindow expires (no explicit finaliseAll needed)
 *           - challenger calls challenge(epoch) with a bond (in DEBT_TOKEN) +
 *             a reason hash; sets challenged=true. Resolution is off-chain
 *             via the AERE Foundation's signed attestation (Phase 1) — see
 *             accept/rejectChallenge. Phase 2 will replace with a real
 *             fraud-proof verifier.
 *
 *         REVENUE FLOW:
 *           - rollupOwner calls settleRevenue(amount) — pulls DEBT_TOKEN from
 *             rollupOwner, splits sinkBps to AereSink + sequencerBps to
 *             sequencer + rollupBps to rollupOwner (yes, the same wallet —
 *             this is the operating margin the rollup keeps).
 */
contract AereRollupSettlement is ReentrancyGuard {

    /* ------------------------------- immutable ------------------------------- */

    bytes32 public immutable ROLLUP_ID;
    address public immutable SINK;
    address public immutable DEBT_TOKEN;
    uint64  public immutable CHALLENGE_WINDOW;
    uint16  public immutable SINK_BPS;
    uint16  public immutable ROLLUP_BPS;
    uint16  public immutable SEQUENCER_BPS;
    uint256 public immutable MIN_CHALLENGE_BOND;

    /* --------------------------------- state -------------------------------- */

    address public rollupOwner;
    address public sequencer;

    struct StateRoot {
        bytes32 root;
        bytes32 l2BlockHash;
        uint64 proposedAt;
        bool challenged;
        bool resolvedRejected;
        address challenger;
        uint256 challengeBond;
    }

    /// @notice epoch → state root commitment
    mapping(uint256 => StateRoot) public roots;
    uint256 public latestEpoch;
    uint256 public latestFinalisedEpoch;

    uint256 public totalRevenueSettled;

    /* --------------------------------- events ------------------------------- */

    event StateRootProposed(uint256 indexed epoch, bytes32 root, bytes32 l2BlockHash);
    event StateRootFinalised(uint256 indexed epoch, bytes32 root);
    event Challenged(uint256 indexed epoch, address indexed challenger, uint256 bond, bytes32 reasonHash);
    event ChallengeResolved(uint256 indexed epoch, bool accepted, address indexed bondRecipient);
    event RevenueSettled(uint256 amount, uint256 toSink, uint256 toRollup, uint256 toSequencer);
    event SequencerRotated(address oldSequencer, address newSequencer);
    event OwnerTransferred(address oldOwner, address newOwner);

    /* --------------------------------- errors ------------------------------- */

    error NotSequencer();
    error NotOwner();
    error EpochOutOfOrder();
    error WindowOpen();        // challenge window not yet closed (used in expired path)
    error WindowClosed();      // challenge window already closed (used in normal resolve)
    error UnknownEpoch();
    error AlreadyChallenged();
    error NotChallenged();
    error AlreadyFinalised();
    error ZeroAddress();
    error ZeroAmount();
    error BondTooSmall();
    error TransferFailed();
    error InvalidSplits();

    /* ----------------------------- constructor ------------------------------ */

    struct Config {
        bytes32 rollupId;
        address rollupOwner;
        address sequencer;
        address sink;
        address debtToken;
        uint64 challengeWindow;
        uint16 sinkBps;
        uint16 rollupBps;
        uint16 sequencerBps;
        uint256 minChallengeBond;
    }

    constructor(Config memory c) {
        if (c.rollupOwner == address(0) || c.sequencer == address(0)) revert ZeroAddress();
        if (c.sink == address(0) || c.debtToken == address(0)) revert ZeroAddress();
        if (uint256(c.sinkBps) + uint256(c.rollupBps) + uint256(c.sequencerBps) != 10_000) revert InvalidSplits();
        ROLLUP_ID = c.rollupId;
        rollupOwner = c.rollupOwner;
        sequencer = c.sequencer;
        SINK = c.sink;
        DEBT_TOKEN = c.debtToken;
        CHALLENGE_WINDOW = c.challengeWindow;
        SINK_BPS = c.sinkBps;
        ROLLUP_BPS = c.rollupBps;
        SEQUENCER_BPS = c.sequencerBps;
        MIN_CHALLENGE_BOND = c.minChallengeBond;
    }

    /* ----------------------------- state roots ------------------------------ */

    function proposeStateRoot(uint256 epoch, bytes32 root, bytes32 l2BlockHash) external {
        if (msg.sender != sequencer) revert NotSequencer();
        if (epoch != latestEpoch + 1) revert EpochOutOfOrder();
        roots[epoch] = StateRoot({
            root: root,
            l2BlockHash: l2BlockHash,
            proposedAt: uint64(block.timestamp),
            challenged: false,
            resolvedRejected: false,
            challenger: address(0),
            challengeBond: 0
        });
        latestEpoch = epoch;
        emit StateRootProposed(epoch, root, l2BlockHash);
    }

    /// @notice True once: the challenge window has passed AND the root was
    ///         not rejected. Pure view — no state change needed for downstream
    ///         consumers (e.g. bridge withdrawal verifiers) to read this.
    function isFinalised(uint256 epoch) public view returns (bool) {
        StateRoot memory s = roots[epoch];
        if (s.proposedAt == 0) return false;
        if (s.resolvedRejected) return false;
        if (s.challenged) return false;
        return block.timestamp >= uint256(s.proposedAt) + CHALLENGE_WINDOW;
    }

    /// @notice Bookkeeping advance to compress reads. Anyone can call.
    function advanceFinalisation() external {
        uint256 i = latestFinalisedEpoch + 1;
        while (i <= latestEpoch && isFinalised(i)) {
            emit StateRootFinalised(i, roots[i].root);
            unchecked { i++; }
        }
        unchecked { latestFinalisedEpoch = i - 1; }
    }

    /* ----------------------------- challenges ------------------------------- */

    function challenge(uint256 epoch, uint256 bond, bytes32 reasonHash) external nonReentrant {
        StateRoot storage s = roots[epoch];
        if (s.proposedAt == 0) revert UnknownEpoch();
        if (block.timestamp >= uint256(s.proposedAt) + CHALLENGE_WINDOW) revert AlreadyFinalised();
        if (s.challenged) revert AlreadyChallenged();
        if (bond < MIN_CHALLENGE_BOND) revert BondTooSmall();
        if (!IERC20(DEBT_TOKEN).transferFrom(msg.sender, address(this), bond)) revert TransferFailed();
        s.challenged = true;
        s.challenger = msg.sender;
        s.challengeBond = bond;
        emit Challenged(epoch, msg.sender, bond, reasonHash);
    }

    /// @notice Phase 1 sequencer-side challenge resolution.
    /// @dev    MUST be called STRICTLY within the challenge window.
    ///         If the sequencer fails to resolve before window-end, the
    ///         challenge stays open and `resolveChallengeExpired()` lets
    ///         anyone return the bond to the challenger AND flag the root
    ///         as rejected. This removes the "wait-out-the-window then
    ///         pocket the bond + finalise fraudulent root" exploit path.
    ///         Phase 2 replaces this with an on-chain fraud-proof verifier.
    function resolveChallenge(uint256 epoch, bool accepted) external nonReentrant {
        if (msg.sender != sequencer) revert NotSequencer();
        StateRoot storage s = roots[epoch];
        if (!s.challenged) revert NotChallenged();
        if (block.timestamp >= uint256(s.proposedAt) + CHALLENGE_WINDOW) revert WindowClosed();

        address recipient = accepted ? s.challenger : sequencer;
        uint256 bond = s.challengeBond;
        s.challengeBond = 0;
        s.challenged = false;          // resolution closes the challenge state either way
        if (accepted) {
            s.resolvedRejected = true; // fraudulent root permanently rejected
        }
        if (bond > 0) {
            if (!IERC20(DEBT_TOKEN).transfer(recipient, bond)) revert TransferFailed();
        }
        emit ChallengeResolved(epoch, accepted, recipient);
    }

    /// @notice Permissionless: if the sequencer didn't resolve a challenge
    ///         in time, the challenger gets their bond back AND the root
    ///         is automatically marked rejected. The sequencer is punished
    ///         for inattention by losing the chance to defend.
    function resolveChallengeExpired(uint256 epoch) external nonReentrant {
        StateRoot storage s = roots[epoch];
        if (!s.challenged) revert NotChallenged();
        if (block.timestamp < uint256(s.proposedAt) + CHALLENGE_WINDOW) revert WindowOpen();
        address challenger = s.challenger;
        uint256 bond = s.challengeBond;
        s.challengeBond = 0;
        s.challenged = false;
        s.resolvedRejected = true;     // sequencer didn't defend → reject the root by default
        if (bond > 0) {
            if (!IERC20(DEBT_TOKEN).transfer(challenger, bond)) revert TransferFailed();
        }
        emit ChallengeResolved(epoch, true, challenger);
    }

    /* ----------------------------- revenue ---------------------------------- */

    function settleRevenue(uint256 amount) external nonReentrant {
        if (msg.sender != rollupOwner) revert NotOwner();
        if (amount == 0) revert ZeroAmount();
        if (!IERC20(DEBT_TOKEN).transferFrom(msg.sender, address(this), amount)) revert TransferFailed();

        uint256 toSink = (amount * SINK_BPS) / 10_000;
        uint256 toSeq  = (amount * SEQUENCER_BPS) / 10_000;
        uint256 toRol  = amount - toSink - toSeq; // remainder (handles rounding)

        if (toSink > 0) {
            IERC20(DEBT_TOKEN).approve(SINK, toSink);
            (bool ok, ) = SINK.call(abi.encodeWithSignature("flush(address,uint256)", DEBT_TOKEN, toSink));
            ok; // tolerant; residual stays here flushable later
        }
        if (toSeq > 0) {
            if (!IERC20(DEBT_TOKEN).transfer(sequencer, toSeq)) revert TransferFailed();
        }
        if (toRol > 0) {
            if (!IERC20(DEBT_TOKEN).transfer(rollupOwner, toRol)) revert TransferFailed();
        }
        totalRevenueSettled += amount;
        emit RevenueSettled(amount, toSink, toRol, toSeq);
    }

    /* ----------------------------- governance-lite -------------------------- */

    function setSequencer(address newSeq) external {
        if (msg.sender != rollupOwner) revert NotOwner();
        if (newSeq == address(0)) revert ZeroAddress();
        address old = sequencer;
        sequencer = newSeq;
        emit SequencerRotated(old, newSeq);
    }

    function transferOwner(address newOwner) external {
        if (msg.sender != rollupOwner) revert NotOwner();
        if (newOwner == address(0)) revert ZeroAddress();
        address old = rollupOwner;
        rollupOwner = newOwner;
        emit OwnerTransferred(old, newOwner);
    }
}
