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

/**
 * @title AereDeathSwitch — single shared circuit breaker for AERE bridges + DEX
 * @notice One contract every safety-critical primitive can opt into reading.
 *         When tripped, calls to opted-in contracts revert. Foundation can
 *         arm it manually for declared emergencies; anyone can permissionlessly
 *         trip it when a registered condition oracle reports a breach.
 *
 *         WHY a shared switch rather than per-contract pause?
 *           - One observable signal off-chain (Foundation pager + Forta bot
 *             can monitor a single `Tripped` event instead of N).
 *           - Bridges that share liquidity can co-fail safely (cross-bridge
 *             arbitrage during a partial pause is the classic exploit
 *             vector — e.g. Multichain July 2023).
 *           - Re-arm policy lives in one place; impossible to forget to
 *             un-pause a sibling contract.
 *
 *         WHAT trips it:
 *           1. Foundation calls `armForDeclaredEmergency(reason)` — emergency
 *              declaration. Requires owner; no precondition.
 *           2. Anyone calls `permissionlessTrip(conditionId)` and the
 *              registered IDeathSwitchCondition.isBreached() returns true.
 *              Conditions are registered immutably at deploy; cannot be
 *              added or removed post-deploy. Examples:
 *                - bridge collateral ratio < 1.0
 *                - sink burn rate < floor over 24h window
 *                - oracle staleness across all Wave 1 markets
 *
 *         WHAT happens when tripped:
 *           - `isTripped()` returns true. Opted-in contracts MUST read this
 *             in their state-mutating entry points and revert on true.
 *           - Optionally per-bucket: bridges and DEX may want to revert
 *             independently. `isBucketTripped(bytes32 bucket)` allows
 *             per-vertical kill, but `isTripped()` is the master.
 *
 *         RE-ARM (post-incident recovery):
 *           - Foundation calls `requestRearm(reason)` to start a 7-day timelock.
 *           - After timelock, Foundation calls `finalizeRearm()` which atomically
 *             clears `tripped`. Anyone observing the chain has 7 days to publish
 *             evidence + escalate before Foundation can resume normal ops.
 *           - The 7-day window is immutable; Foundation cannot shorten it.
 *
 *         IMMUTABILITY:
 *           - REARM_TIMELOCK constant.
 *           - Condition registry frozen at constructor — cannot add or remove.
 *           - Foundation has ARM authority but no SUPERUSER bypass on rearm.
 *
 *         GOTCHA — read-only paths:
 *           Opted-in contracts read `isTripped()` in WRITES only. View
 *           functions (balance queries, market state) MUST continue to work
 *           when tripped; the dashboards stay informative during incidents.
 */

interface IDeathSwitchCondition {
    /// @notice Returns true when the condition's invariant is breached.
    function isBreached() external view returns (bool);

    /// @notice Optional bucket tag (e.g. keccak256("bridge")) to scope the trip.
    function bucket() external view returns (bytes32);

    /// @notice Optional human-readable label for the trip event.
    function label() external view returns (string memory);
}

contract AereDeathSwitch {

    /* ================================ config ================================ */

    address public immutable FOUNDATION;
    uint256 public constant REARM_TIMELOCK = 7 days;

    /// @notice immutable list of registered conditions. Index = conditionId.
    address[] internal _conditions;

    /// @notice optional per-condition cached `bucket()` to spare extra calls.
    mapping(uint256 => bytes32) internal _conditionBucket;

    /* ================================= state ================================= */

    bool   public tripped;
    bytes32 public lastTripReason;          // bucket OR keccak256("declared")
    uint64 public lastTripAt;
    string  public lastTripLabel;

    /// @notice tripped flag per bucket. Master `tripped` is union of all + manual.
    mapping(bytes32 => bool) public bucketTripped;

    /// @notice re-arm request state.
    uint64 public rearmReadyAt;
    bytes32 public rearmReason;

    /* ================================= events ================================= */

    event ConditionRegistered(uint256 indexed conditionId, address condition, bytes32 bucket);
    event Tripped(bytes32 indexed bucketOrDeclared, address indexed by, uint256 indexed conditionId, string label);
    event RearmRequested(uint64 readyAt, bytes32 reason);
    event RearmCancelled(bytes32 reason); // AUDIT FIX (MED #37)
    event Rearmed(address indexed by);

    /* ================================= errors ================================= */

    error NotFoundation();
    error AlreadyTripped();
    error NotTripped();
    error UnknownCondition();
    error ConditionNotBreached();
    error RearmAlreadyRequested();
    error RearmTimelockOpen(uint256 nowTs, uint256 readyAt);
    error NoRearmRequest();
    error ZeroCondition();

    /* =============================== modifiers ============================== */

    modifier onlyFoundation() {
        if (msg.sender != FOUNDATION) revert NotFoundation();
        _;
    }

    /* ============================== constructor ============================= */

    /// @param foundation Foundation Ledger address — sole arm authority.
    /// @param conditions Immutable list of IDeathSwitchCondition addresses.
    ///                   Pass [] to deploy with no automatic-trip conditions
    ///                   (Foundation manual arming only).
    constructor(address foundation, address[] memory conditions) {
        require(foundation != address(0), "zero-foundation");
        FOUNDATION = foundation;
        for (uint256 i = 0; i < conditions.length; i++) {
            address c = conditions[i];
            if (c == address(0)) revert ZeroCondition();
            bytes32 b = IDeathSwitchCondition(c).bucket();
            _conditions.push(c);
            _conditionBucket[i] = b;
            emit ConditionRegistered(i, c, b);
        }
    }

    /* ============================= arm / trip path ============================= */

    /// @notice Foundation declares an emergency. No on-chain condition needed.
    function armForDeclaredEmergency(string calldata reasonLabel) external onlyFoundation {
        if (tripped) revert AlreadyTripped();
        tripped = true;
        bytes32 reason = keccak256("declared");
        lastTripReason = reason;
        lastTripAt = uint64(block.timestamp);
        lastTripLabel = reasonLabel;
        emit Tripped(reason, msg.sender, type(uint256).max, reasonLabel);
    }

    /// @notice Permissionless trip when a registered condition is breached.
    ///         Anyone can spend gas to validate + trip. No reward — being a
    ///         Citizen is the reward.
    function permissionlessTrip(uint256 conditionId) external {
        if (tripped) revert AlreadyTripped();
        if (conditionId >= _conditions.length) revert UnknownCondition();
        IDeathSwitchCondition c = IDeathSwitchCondition(_conditions[conditionId]);
        if (!c.isBreached()) revert ConditionNotBreached();

        bytes32 b = _conditionBucket[conditionId];
        tripped = true;
        bucketTripped[b] = true;
        lastTripReason = b;
        lastTripAt = uint64(block.timestamp);
        lastTripLabel = c.label();
        emit Tripped(b, msg.sender, conditionId, lastTripLabel);
    }

    /* ================================ re-arm ================================ */

    /// @notice Foundation requests re-arm. Starts the 7-day timelock.
    function requestRearm(bytes32 reason) external onlyFoundation {
        if (!tripped) revert NotTripped();
        if (rearmReadyAt != 0) revert RearmAlreadyRequested();
        rearmReadyAt = uint64(block.timestamp + REARM_TIMELOCK);
        rearmReason = reason;
        emit RearmRequested(rearmReadyAt, reason);
    }

    /// @notice Cancel a pending rearm request (e.g. another incident found).
    ///         Foundation only; no timelock needed for cancel.
    function cancelRearmRequest() external onlyFoundation {
        if (rearmReadyAt == 0) revert NoRearmRequest();
        bytes32 prev = rearmReason;
        rearmReadyAt = 0;
        rearmReason = bytes32(0);
        emit RearmCancelled(prev); // AUDIT FIX (MED #37)
    }

    /// AUDIT FIX (HIGH #19): permissionless unarm after 24h when a DECLARED
    /// emergency stays armed without any registered condition currently
    /// breached. Prevents compromised Foundation from indefinite halt.
    uint64 public constant DECLARED_UNARM_DELAY = 24 hours;
    error NoConditionsBreached();

    function permissionlessUnarmDeclared() external {
        if (!tripped) revert NotTripped();
        // Only applies to declared (manual) trips, not condition-tripped.
        if (lastTripReason != keccak256("declared")) revert ConditionNotBreached();
        if (block.timestamp < uint256(lastTripAt) + DECLARED_UNARM_DELAY) {
            revert RearmTimelockOpen(block.timestamp, uint256(lastTripAt) + DECLARED_UNARM_DELAY);
        }
        // Verify no registered condition is currently breached (else manual arm
        // remains justified).
        for (uint256 i = 0; i < _conditions.length; i++) {
            if (IDeathSwitchCondition(_conditions[i]).isBreached()) revert NoConditionsBreached();
        }
        tripped = false;
        for (uint256 i = 0; i < _conditions.length; i++) {
            bucketTripped[_conditionBucket[i]] = false;
        }
        rearmReadyAt = 0;
        rearmReason = bytes32(0);
        emit Rearmed(msg.sender);
    }

    /// @notice Finalize the rearm after the timelock has elapsed. Foundation only.
    ///         Clears tripped + all bucket flags + the rearm request.
    function finalizeRearm() external onlyFoundation {
        if (rearmReadyAt == 0) revert NoRearmRequest();
        if (block.timestamp < rearmReadyAt) revert RearmTimelockOpen(block.timestamp, rearmReadyAt);

        tripped = false;
        // Clear only the buckets we know about (the registered conditions).
        // External callers that have set their OWN bucket must clear it via
        // their own redeploy. This is intentional: bucket flags persist until
        // EXPLICITLY cleared so reads of `bucketTripped[x]` after rearm reflect
        // *post-rearm state*, not stale incident memory.
        for (uint256 i = 0; i < _conditions.length; i++) {
            bucketTripped[_conditionBucket[i]] = false;
        }
        rearmReadyAt = 0;
        rearmReason = bytes32(0);
        emit Rearmed(msg.sender);
    }

    /* ================================== views ================================== */

    /// @notice Master kill — opted-in contracts read this before mutating state.
    function isTripped() external view returns (bool) {
        return tripped;
    }

    /// @notice Per-vertical kill. Bridge-side reads "bridge"; DEX reads "dex".
    function isBucketTripped(bytes32 bucket) external view returns (bool) {
        return tripped || bucketTripped[bucket];
    }

    function conditionsLength() external view returns (uint256) {
        return _conditions.length;
    }

    function conditionAt(uint256 i) external view returns (address condition, bytes32 bucket) {
        if (i >= _conditions.length) revert UnknownCondition();
        return (_conditions[i], _conditionBucket[i]);
    }
}
