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

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

/**
 * @title AereRaaSFactoryV2 — permissionless Rollup-as-a-Service registry (bug-fixed template)
 * @notice Bug-fix redeploy of AereRaaSFactory (0x8C1b0018ab8C4299a75621a4BdD3cF26971B26Cd,
 *         0 rollups registered). Identical factory model — permissionless
 *         registration, refundable bond, MIN_SINK_BPS floor, no admin pause /
 *         slash / upgrade — with ONE change: it deploys AereRollupSettlementV2
 *         as the per-rollup settlement template instead of the buggy V1.
 *
 *         AereRollupSettlementV2 fixes F6 (last-block challenge grief),
 *         F7 (rejected-epoch finalisation freeze), the isFinalised ancestor
 *         medium, and the settleRevenue stuck-residual low. See that contract.
 *
 *         New immutable DEFAULT_DEFENSE_GRACE is threaded into every settlement
 *         the factory deploys (per-registration override allowed, 0 → default),
 *         mirroring DEFAULT_CHALLENGE_WINDOW.
 *
 *         IMMUTABILITY at factory deploy: BOND_TOKEN, SINK, REGISTRATION_BOND,
 *         MIN_SINK_BPS, DEFAULT_CHALLENGE_WINDOW, DEFAULT_DEFENSE_GRACE,
 *         FOUNDATION. The factory cannot retroactively change a registered
 *         rollup's parameters.
 */
contract AereRaaSFactoryV2 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;
    uint64  public immutable DEFAULT_DEFENSE_GRACE;   // V2: default sequencer defense grace
    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,
        uint64 defaultDefenseGrace
    ) {
        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;
        DEFAULT_DEFENSE_GRACE = defaultDefenseGrace;
    }

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

    function registerRollup(
        bytes32 rollupId,
        address rollupOwner,
        address sequencer,
        address debtToken,
        uint16 sinkBps,
        uint16 rollupBps,
        uint16 sequencerBps,
        uint256 minChallengeBond,
        uint64 challengeWindow,
        uint64 defenseGrace,
        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;
        if (defenseGrace == 0) defenseGrace = DEFAULT_DEFENSE_GRACE;

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

        // Deploy settlement contract (V2 template).
        AereRollupSettlementV2.Config memory cfg = AereRollupSettlementV2.Config({
            rollupId: rollupId,
            rollupOwner: rollupOwner,
            sequencer: sequencer,
            sink: SINK,
            debtToken: debtToken,
            challengeWindow: challengeWindow,
            defenseGrace: defenseGrace,
            sinkBps: sinkBps,
            rollupBps: rollupBps,
            sequencerBps: sequencerBps,
            minChallengeBond: minChallengeBond
        });
        AereRollupSettlementV2 s = new AereRollupSettlementV2(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. Factory verifies signature and
    ///         returns the bond. (Unchanged from V1.)
    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);
    }
}
