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

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

/**
 * @title AereSlashingInsuranceV2 — delegator slashing-loss coverage vault (native AERE)
 * @notice Bug-fix redeploy of AereSlashingInsurance. Same discretion-fund model:
 *         reimburses a portion of the loss a DELEGATOR suffers when a validator
 *         they delegated to is slashed. Custodies NATIVE AERE only. It is NOT a
 *         token: it mints nothing, has no balances of its own units, and issues
 *         no transferable claim on itself.
 *
 *         ─────────────────────────── WHY V2 ───────────────────────────
 *         V1 (0xAd0109A880444519DDbdbFDF3561C624787591bC) let cancelCoverage run
 *         at ANY time while a claim existed, including AFTER the timelock matured.
 *         A griefer could therefore front-run every executeCoverage of a MATURED,
 *         legitimate claim with cancelCoverage, deleting it before it could pay.
 *         Repeated on each re-proposal, this permanently blocked the payout.
 *
 *         V2 FIX: cancelCoverage is only allowed while block.timestamp < readyAt.
 *         Once a claim MATURES (block.timestamp >= readyAt) it can no longer be
 *         cancelled — only executed. So the moment a claim survives its full
 *         CLAIM_DELAY window, the payout is locked in and a griefer can no longer
 *         delete it. The community-veto property is preserved: during the delay
 *         window cancel remains permissionless, so anyone can still stop a
 *         fraudulent proposal from a compromised Foundation key BEFORE it matures.
 *
 *         DESIGN NOTE / RESIDUAL RISK. Cancel is intentionally kept PERMISSIONLESS
 *         during the pre-maturity window. That is the contract's headline safety
 *         property (a compromised Foundation key alone cannot pay itself, because
 *         the community can cancel during the delay). The unavoidable trade-off is
 *         that a griefer can still cancel a legitimate claim DURING its window,
 *         forcing the Foundation to re-propose (cheap for the Foundation, gas-cost
 *         to the griefer each time). What V2 eliminates is the PERMANENT block: a
 *         matured claim can no longer be cancelled, so a claim that clears one
 *         full window always becomes executable and stays executable. Restricting
 *         cancel to the Foundation/proposer would remove the community veto and was
 *         deliberately NOT done.
 *
 *         Everything else is identical to V1: Foundation-only proposeCoverage,
 *         payout frozen at proposal (min(slashed*bps, cap)), per-event cap, global
 *         cooldown, and NO admin withdrawal of principal.
 */
contract AereSlashingInsuranceV2 is ReentrancyGuard {

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

    address public immutable FOUNDATION;
    uint16  public immutable COVERAGE_BPS;      // fraction of a slash covered, <= 10000
    uint256 public immutable PER_EVENT_CAP;     // max native AERE paid per coverage
    uint64  public immutable COOLDOWN_SECONDS;  // min seconds between executed coverages
    uint64  public immutable CLAIM_DELAY;       // timelock between propose and execute
    uint16  public constant BPS_DENOM = 10000;

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

    struct Claim {
        bool    exists;
        address delegator;      // frozen recipient
        uint256 slashedAmount;  // reported loss (informational)
        uint256 payout;         // frozen native AERE payout
        uint64  readyAt;        // executable at/after this timestamp
        bytes32 evidenceHash;   // off-chain evidence pointer (slashing tx / report)
    }

    mapping(uint256 => Claim) public claims;
    uint256 public nextClaimId;

    uint256 public totalDonated;
    uint256 public totalCovered;
    uint64  public lastCoverageAt;

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

    event Donated(address indexed donor, uint256 amount, uint256 newBalance);
    event CoverageProposed(
        uint256 indexed claimId, address indexed delegator, uint256 slashedAmount,
        uint256 payout, uint64 readyAt, bytes32 evidenceHash
    );
    event CoverageCancelled(uint256 indexed claimId, address indexed canceller, string reason);
    event CoveragePaid(uint256 indexed claimId, address indexed delegator, uint256 payout);

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

    error NotFoundation();
    error ZeroAddress();
    error ZeroAmount();
    error BadBps();
    error NoClaim();
    error TimelockNotElapsed(uint64 readyAt);
    error ClaimMatured(uint64 readyAt);     // V2: cancel attempted at/after readyAt
    error CooldownActive(uint256 readyAt);
    error InsufficientBalance(uint256 have, uint256 want);
    error PayFailed();

    /* ------------------------------- modifiers ------------------------------ */

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

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

    constructor(
        address foundation,
        uint16  coverageBps,
        uint256 perEventCap,
        uint64  cooldownSeconds,
        uint64  claimDelay
    ) {
        if (foundation == address(0)) revert ZeroAddress();
        if (coverageBps == 0 || coverageBps > BPS_DENOM) revert BadBps();
        if (perEventCap == 0) revert ZeroAmount();
        FOUNDATION = foundation;
        COVERAGE_BPS = coverageBps;
        PER_EVENT_CAP = perEventCap;
        COOLDOWN_SECONDS = cooldownSeconds;
        CLAIM_DELAY = claimDelay;
    }

    /* -------------------------------- funding ------------------------------- */

    receive() external payable {
        totalDonated += msg.value;
        emit Donated(msg.sender, msg.value, address(this).balance);
    }

    function donate() external payable {
        if (msg.value == 0) revert ZeroAmount();
        totalDonated += msg.value;
        emit Donated(msg.sender, msg.value, address(this).balance);
    }

    /* ------------------------------- coverage ------------------------------- */

    /// @notice Foundation proposes coverage for a slashed delegator. Payout is
    ///         frozen now and only becomes executable after CLAIM_DELAY, during
    ///         which anyone may cancel a suspicious claim.
    function proposeCoverage(address delegator, uint256 slashedAmount, bytes32 evidenceHash)
        external onlyFoundation returns (uint256 claimId)
    {
        if (delegator == address(0)) revert ZeroAddress();
        if (slashedAmount == 0) revert ZeroAmount();
        uint256 payout = (slashedAmount * COVERAGE_BPS) / BPS_DENOM;
        if (payout > PER_EVENT_CAP) payout = PER_EVENT_CAP;
        if (payout == 0) revert ZeroAmount();
        uint64 readyAt = uint64(block.timestamp) + CLAIM_DELAY;
        claimId = nextClaimId++;
        claims[claimId] = Claim({
            exists: true,
            delegator: delegator,
            slashedAmount: slashedAmount,
            payout: payout,
            readyAt: readyAt,
            evidenceHash: evidenceHash
        });
        emit CoverageProposed(claimId, delegator, slashedAmount, payout, readyAt, evidenceHash);
    }

    /// @notice Permissionless: cancel a pending claim DURING its delay window only.
    ///         V2 FIX: reverts (ClaimMatured) once block.timestamp >= readyAt, so a
    ///         matured claim can never be cancelled — only executed. This closes the
    ///         griefing hole where a matured, legitimate payout could be front-run
    ///         and deleted indefinitely. Before maturity, cancel stays permissionless
    ///         so the community can still veto a fraudulent proposal (compromised
    ///         Foundation key). Foundation can re-propose a legitimate claim.
    function cancelCoverage(uint256 claimId, string calldata reason) external {
        Claim storage c = claims[claimId];
        if (!c.exists) revert NoClaim();
        if (block.timestamp >= uint256(c.readyAt)) revert ClaimMatured(c.readyAt);
        delete claims[claimId];
        emit CoverageCancelled(claimId, msg.sender, reason);
    }

    /// @notice Execute a matured claim: pay the frozen payout to the frozen
    ///         delegator. Callable by anyone once the timelock has elapsed (the
    ///         recipient and amount are fixed, so a third-party trigger cannot
    ///         redirect funds). Enforces the global cooldown between payouts.
    function executeCoverage(uint256 claimId) external nonReentrant {
        Claim memory c = claims[claimId];
        if (!c.exists) revert NoClaim();
        if (block.timestamp < uint256(c.readyAt)) revert TimelockNotElapsed(c.readyAt);
        if (lastCoverageAt != 0 && block.timestamp < uint256(lastCoverageAt) + COOLDOWN_SECONDS) {
            revert CooldownActive(uint256(lastCoverageAt) + COOLDOWN_SECONDS);
        }
        uint256 bal = address(this).balance;
        if (bal < c.payout) revert InsufficientBalance(bal, c.payout);

        // effects before interaction
        delete claims[claimId];
        totalCovered += c.payout;
        lastCoverageAt = uint64(block.timestamp);

        (bool ok, ) = payable(c.delegator).call{value: c.payout}("");
        if (!ok) revert PayFailed();
        emit CoveragePaid(claimId, c.delegator, c.payout);
    }

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

    /// @notice Preview the frozen payout for a given reported slash amount.
    function coverageFor(uint256 slashedAmount) external view returns (uint256 payout) {
        payout = (slashedAmount * COVERAGE_BPS) / BPS_DENOM;
        if (payout > PER_EVENT_CAP) payout = PER_EVENT_CAP;
    }

    /// @notice Native AERE currently available to pay coverage.
    function available() external view returns (uint256) {
        return address(this).balance;
    }

    /// @notice True if the global cooldown permits an execution right now.
    function cooldownReady() external view returns (bool) {
        if (lastCoverageAt == 0) return true;
        return block.timestamp >= uint256(lastCoverageAt) + COOLDOWN_SECONDS;
    }
}
