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

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";

/**
 * @title AereSanctionsRegistry — read-only OFAC SDN sanctions registry
 * @notice Foundation publishes a Merkle root of OFAC Specially Designated
 *         Nationals (SDN) addresses each epoch (24h cron). Any consumer
 *         contract can verify whether a specific address is sanctioned at
 *         a given epoch by submitting a Merkle inclusion proof.
 *
 *         Source of truth: the official OFAC SDN list at
 *         https://www.treasury.gov/ofac/downloads/sdnlist.txt
 *         An off-chain ingester running on a Hostinger VPS (#2) reads the
 *         official list nightly, extracts crypto addresses where present,
 *         builds the Merkle tree, and publishes the root via the
 *         Foundation 2-of-3 multisig (Ledger-signed).
 *
 *         Designed as a PUBLIC GOOD. Anyone — institutional integrator,
 *         dApp, EOA — calls `isSanctioned(...)` to gate or screen a
 *         counterparty. There is no fee. No discretionary interception.
 *
 * @dev    Lifecycle: Proposed → (Challenged?) → Attested (mirror of
 *         AereNavOracle pattern). Optimistic 24h window.
 *
 *         Leaf format: keccak256(abi.encode(address evmAddress, uint16 listId))
 *         where listId distinguishes the source list (1 = OFAC SDN,
 *         2 = OFAC SSI, 3 = EU consolidated, etc.). Phase 1 ships with
 *         OFAC SDN only.
 *
 *         IMMUTABILITY:
 *           - CHALLENGE_WINDOW constant
 *           - History append-only; attested epochs cannot be rewritten
 *           - Owner can only propose / dismiss / attest
 *           - There is NO "deSanction" path on chain — that would suggest
 *             AERE Foundation is making a sanctions decision, which it is
 *             NOT. The Foundation only mirrors the canonical OFAC list.
 */
contract AereSanctionsRegistry is Ownable {

    uint256 public constant CHALLENGE_WINDOW = 24 hours;

    /// @notice OFAC SDN list id, used as the prefix in the leaf hash.
    uint16 public constant LIST_OFAC_SDN = 1;
    uint16 public constant LIST_OFAC_SSI = 2;
    uint16 public constant LIST_EU_CONSOLIDATED = 3;
    uint16 public constant LIST_UK_HMT = 4;
    uint16 public constant LIST_UN_CONSOLIDATED = 5;

    enum State { Proposed, Challenged, Attested }

    struct Snapshot {
        bytes32 root;
        uint64 proposedAt;
        uint64 attestedAt;
        State state;
        string sourceUrl;          // canonical OFAC URL the root was built from
        string ipfsCid;            // optional: pin the full list
    }

    /// @notice epoch → Snapshot
    mapping(uint256 => Snapshot) public snapshots;

    /// @notice epoch → open-challenge count
    mapping(uint256 => uint256) public openChallenges;
    mapping(uint256 => Challenge[]) public epochChallenges;

    struct Challenge {
        address challenger;
        uint64 timestamp;
        string reason;
        bool dismissed;
    }

    /// @notice monotonic latest epoch counter.
    uint256 public latestEpoch;

    event SnapshotProposed(uint256 indexed epoch, bytes32 root, string sourceUrl, string ipfsCid);
    event SnapshotChallenged(uint256 indexed epoch, uint256 challengeId, address challenger, string reason);
    event ChallengeDismissed(uint256 indexed epoch, uint256 challengeId, string response);
    event SnapshotAttested(uint256 indexed epoch, bytes32 root);

    error WrongState(State got, State want);
    error UnknownEpoch();
    error UnknownChallenge();
    error EpochAlreadyExists();
    error TimelockNotElapsed(uint256 nowTs, uint256 earliest);

    /* -------------------------------- propose -------------------------------- */

    function proposeSnapshot(
        uint256 epoch,
        bytes32 root,
        string calldata sourceUrl,
        string calldata ipfsCid
    ) external onlyOwner {
        if (snapshots[epoch].proposedAt != 0) revert EpochAlreadyExists();
        snapshots[epoch] = Snapshot({
            root: root,
            proposedAt: uint64(block.timestamp),
            attestedAt: 0,
            state: State.Proposed,
            sourceUrl: sourceUrl,
            ipfsCid: ipfsCid
        });
        if (epoch > latestEpoch) latestEpoch = epoch;
        emit SnapshotProposed(epoch, root, sourceUrl, ipfsCid);
    }

    /* ------------------------------- challenge ------------------------------ */

    function challenge(uint256 epoch, string calldata reason) external returns (uint256 challengeId) {
        Snapshot storage s = snapshots[epoch];
        if (s.proposedAt == 0) revert UnknownEpoch();
        if (s.state != State.Proposed && s.state != State.Challenged) revert WrongState(s.state, State.Proposed);
        if (block.timestamp >= uint256(s.proposedAt) + CHALLENGE_WINDOW) {
            revert TimelockNotElapsed(block.timestamp, uint256(s.proposedAt) + CHALLENGE_WINDOW);
        }
        challengeId = epochChallenges[epoch].length;
        epochChallenges[epoch].push(Challenge({
            challenger: msg.sender,
            timestamp: uint64(block.timestamp),
            reason: reason,
            dismissed: false
        }));
        openChallenges[epoch]++;
        s.state = State.Challenged;
        emit SnapshotChallenged(epoch, challengeId, msg.sender, reason);
    }

    function dismissChallenge(uint256 epoch, uint256 challengeId, string calldata response) external onlyOwner {
        Challenge[] storage list = epochChallenges[epoch];
        if (challengeId >= list.length) revert UnknownChallenge();
        Challenge storage c = list[challengeId];
        if (c.dismissed) revert UnknownChallenge();
        c.dismissed = true;
        openChallenges[epoch]--;
        if (openChallenges[epoch] == 0) snapshots[epoch].state = State.Proposed;
        emit ChallengeDismissed(epoch, challengeId, response);
    }

    /* ------------------------------- attestation ---------------------------- */

    function attest(uint256 epoch) external {
        Snapshot storage s = snapshots[epoch];
        if (s.proposedAt == 0) revert UnknownEpoch();
        if (s.state != State.Proposed) revert WrongState(s.state, State.Proposed);
        uint256 earliest = uint256(s.proposedAt) + CHALLENGE_WINDOW;
        if (block.timestamp < earliest) revert TimelockNotElapsed(block.timestamp, earliest);
        if (openChallenges[epoch] != 0) revert WrongState(State.Challenged, State.Proposed);
        s.state = State.Attested;
        s.attestedAt = uint64(block.timestamp);
        emit SnapshotAttested(epoch, s.root);
    }

    /* ------------------------------- verify -------------------------------- */

    /// @notice Verify that `evmAddress` was on `listId` at `epoch`.
    /// @return sanctioned True if the proof is valid AND the snapshot is attested.
    function isSanctioned(
        uint256 epoch,
        address evmAddress,
        uint16 listId,
        bytes32[] calldata proof
    ) external view returns (bool sanctioned) {
        Snapshot storage s = snapshots[epoch];
        if (s.state != State.Attested) return false;
        bytes32 leaf = keccak256(abi.encode(evmAddress, listId));
        return MerkleProof.verify(proof, s.root, leaf);
    }

    /// @notice ROUND-3 FIX: cap iteration depth to prevent griefable DoS.
    ///         If Foundation proposes 1000 challenged-but-never-attested
    ///         snapshots, dApps calling this in-tx would have been bricked.
    ///         Now capped at 64 — practically: the latest 64 epochs.
    uint256 public constant MAX_EPOCH_SCAN = 64;

    /// @notice Same shape as isSanctioned but takes only the latest attested epoch.
    function isSanctionedLatest(
        address evmAddress,
        uint16 listId,
        bytes32[] calldata proof
    ) external view returns (bool sanctioned, uint256 attestedEpoch) {
        uint256 scanned = 0;
        for (uint256 e = latestEpoch; e > 0 && scanned < MAX_EPOCH_SCAN; e--) {
            Snapshot storage s = snapshots[e];
            if (s.state == State.Attested) {
                bytes32 leaf = keccak256(abi.encode(evmAddress, listId));
                return (MerkleProof.verify(proof, s.root, leaf), e);
            }
            unchecked { scanned++; }
        }
        return (false, 0);
    }

    function snapshotStatus(uint256 epoch) external view returns (
        bytes32 root,
        uint64 proposedAt,
        uint64 attestedAt,
        State state,
        uint256 openChallengeCount,
        string memory sourceUrl,
        string memory ipfsCid
    ) {
        Snapshot storage s = snapshots[epoch];
        return (s.root, s.proposedAt, s.attestedAt, s.state, openChallenges[epoch], s.sourceUrl, s.ipfsCid);
    }
}
