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

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

/**
 * @title AereRaaSFactory — permissionless registry for Rollup-as-a-Service
 *                          deployments on AERE
 * @notice Anyone can register a new rollup by:
 *           1. Posting a registration bond in BOND_TOKEN (refundable on
 *              graceful exit, slashable on fraud).
 *           2. Choosing a rollupOwner + sequencer address.
 *           3. Choosing revenue splits (sink/rollup/sequencer) subject to
 *              MIN_SINK_BPS — the factory enforces a floor so the sink
 *              always gets at least the protocol's mandatory share.
 *
 *         Factory deploys a fresh AereRollupSettlement, records the rollup,
 *         and returns its address. Bond is held by the factory.
 *
 *         IMMUTABILITY at factory deploy:
 *           - BOND_TOKEN
 *           - SINK
 *           - REGISTRATION_BOND
 *           - MIN_SINK_BPS (e.g. 1000 = 10% floor to AereSink)
 *           - DEFAULT_CHALLENGE_WINDOW
 *
 *         NO admin pause, no admin slash, no upgrade. Factory cannot
 *         retroactively change a registered rollup's parameters.
 *
 * @dev    Bond retrieval requires a Foundation-attested "graceful exit"
 *         signature in Phase 1. Phase 2 replaces with an on-chain
 *         no-pending-challenges + N-epoch-quiet check.
 */
contract AereRaaSFactory is ReentrancyGuard {

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

    address public immutable BOND_TOKEN;
    address public immutable SINK;
    uint256 public immutable REGISTRATION_BOND;
    uint16  public immutable MIN_SINK_BPS;
    uint64  public immutable DEFAULT_CHALLENGE_WINDOW;
    address public immutable FOUNDATION;            // signs graceful-exit attestations in Phase 1

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

    struct Rollup {
        bool registered;
        bool exited;
        address settlement;
        address rollupOwner;
        uint256 bond;
        uint64 registeredAt;
        string metadataUri;
    }

    /// @notice rollupId → Rollup
    mapping(bytes32 => Rollup) public rollups;
    bytes32[] public rollupIds;

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

    event RollupRegistered(
        bytes32 indexed rollupId,
        address indexed rollupOwner,
        address indexed settlement,
        uint256 bond,
        string metadataUri
    );
    event RollupExited(bytes32 indexed rollupId, address recipient, uint256 bondReturned);
    event MetadataUpdated(bytes32 indexed rollupId, string newMetadataUri);

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

    error AlreadyRegistered();
    error NotRegistered();
    error AlreadyExited();
    error NotOwner();
    error BondMismatch();
    error InvalidSplits();
    error SinkBpsTooLow();
    error InvalidExitSig();
    error TransferFailed();
    error ZeroAddress();

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

    constructor(
        address bondToken,
        address sink,
        address foundation,
        uint256 registrationBond,
        uint16 minSinkBps,
        uint64 defaultChallengeWindow
    ) {
        if (bondToken == address(0) || sink == address(0) || foundation == address(0)) revert ZeroAddress();
        BOND_TOKEN = bondToken;
        SINK = sink;
        FOUNDATION = foundation;
        REGISTRATION_BOND = registrationBond;
        MIN_SINK_BPS = minSinkBps;
        DEFAULT_CHALLENGE_WINDOW = defaultChallengeWindow;
    }

    /* ----------------------------- registration ----------------------------- */

    function registerRollup(
        bytes32 rollupId,
        address rollupOwner,
        address sequencer,
        address debtToken,
        uint16 sinkBps,
        uint16 rollupBps,
        uint16 sequencerBps,
        uint256 minChallengeBond,
        uint64 challengeWindow,
        string calldata metadataUri
    ) external nonReentrant returns (address settlementAddr) {
        if (rollups[rollupId].registered) revert AlreadyRegistered();
        if (rollupOwner == address(0) || sequencer == address(0) || debtToken == address(0)) revert ZeroAddress();
        if (uint256(sinkBps) + uint256(rollupBps) + uint256(sequencerBps) != 10_000) revert InvalidSplits();
        if (sinkBps < MIN_SINK_BPS) revert SinkBpsTooLow();
        if (challengeWindow == 0) challengeWindow = DEFAULT_CHALLENGE_WINDOW;

        // Pull bond.
        if (REGISTRATION_BOND > 0) {
            if (!IERC20(BOND_TOKEN).transferFrom(msg.sender, address(this), REGISTRATION_BOND)) revert TransferFailed();
        }

        // Deploy settlement contract.
        AereRollupSettlement.Config memory cfg = AereRollupSettlement.Config({
            rollupId: rollupId,
            rollupOwner: rollupOwner,
            sequencer: sequencer,
            sink: SINK,
            debtToken: debtToken,
            challengeWindow: challengeWindow,
            sinkBps: sinkBps,
            rollupBps: rollupBps,
            sequencerBps: sequencerBps,
            minChallengeBond: minChallengeBond
        });
        AereRollupSettlement s = new AereRollupSettlement(cfg);
        settlementAddr = address(s);

        rollups[rollupId] = Rollup({
            registered: true,
            exited: false,
            settlement: settlementAddr,
            rollupOwner: rollupOwner,
            bond: REGISTRATION_BOND,
            registeredAt: uint64(block.timestamp),
            metadataUri: metadataUri
        });
        rollupIds.push(rollupId);

        emit RollupRegistered(rollupId, rollupOwner, settlementAddr, REGISTRATION_BOND, metadataUri);
    }

    function updateMetadata(bytes32 rollupId, string calldata newMetadataUri) external {
        Rollup storage r = rollups[rollupId];
        if (!r.registered) revert NotRegistered();
        if (msg.sender != r.rollupOwner) revert NotOwner();
        r.metadataUri = newMetadataUri;
        emit MetadataUpdated(rollupId, newMetadataUri);
    }

    /* -------------------------------- exit ---------------------------------- */

    /// @notice Phase 1: rollupOwner submits a Foundation-signed attestation
    ///         that the rollup has gracefully exited (no pending challenges,
    ///         N quiet epochs since last state root). Factory verifies
    ///         signature and returns the bond.
    function exitRollup(
        bytes32 rollupId,
        address recipient,
        uint64 attestationDeadline,
        bytes calldata foundationSig
    ) external nonReentrant {
        Rollup storage r = rollups[rollupId];
        if (!r.registered) revert NotRegistered();
        if (r.exited) revert AlreadyExited();
        if (msg.sender != r.rollupOwner) revert NotOwner();
        if (recipient == address(0)) revert ZeroAddress();
        if (block.timestamp > attestationDeadline) revert InvalidExitSig();

        bytes32 hash = keccak256(abi.encode(
            "AereRaaSFactory:exit",
            block.chainid,
            address(this),
            rollupId,
            recipient,
            attestationDeadline
        ));
        bytes32 ethHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
        address recovered = _recover(ethHash, foundationSig);
        if (recovered == address(0) || recovered != FOUNDATION) revert InvalidExitSig();

        uint256 bond = r.bond;
        r.bond = 0;
        r.exited = true;
        if (bond > 0) {
            if (!IERC20(BOND_TOKEN).transfer(recipient, bond)) revert TransferFailed();
        }
        emit RollupExited(rollupId, recipient, bond);
    }

    /* ---------------------------------- views ------------------------------- */

    function rollupCount() external view returns (uint256) { return rollupIds.length; }

    /* ------------------------------- internal ------------------------------- */

    function _recover(bytes32 hash, bytes calldata sig) internal pure returns (address) {
        if (sig.length != 65) return address(0);
        bytes32 r; bytes32 s; uint8 v;
        assembly {
            r := calldataload(sig.offset)
            s := calldataload(add(sig.offset, 32))
            v := byte(0, calldataload(add(sig.offset, 64)))
        }
        if (v < 27) v += 27;
        return ecrecover(hash, v, r, s);
    }
}
