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

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

/**
 * @title AereNavOracle — Merkle-rooted asset reserve attestations
 * @notice Publishes proof-of-backing for every bridged stablecoin and RWA
 *         settlement deposit held by AERE protocol contracts. The Foundation
 *         publishes a snapshot Merkle root once per epoch (24h by default);
 *         anyone can later verify a specific (asset, reserveAmount) pair
 *         against the published root via a Merkle proof.
 *
 *         Used by:
 *           - aere.network/reserves   — public dashboard
 *           - USDC.e/USDT.e/USDe.e/EURC.e display pages
 *           - AereSettlementHub for the collateral wave 1 assets
 *           - third-party integrators verifying reserve before custody
 *
 * @dev    Optimistic challenge: every newly proposed root sits for
 *         CHALLENGE_WINDOW (24h default) before becoming "attested".
 *         During that window anyone can submit `challenge(epoch, reason)`
 *         which flags the epoch for Foundation review. Challenged epochs
 *         cannot be promoted to attested until the Foundation dismisses
 *         the challenge with a public response (similar to AereRetroPGF
 *         pattern).
 *
 *         Leaf format (single-hash to match standard Merkle libs):
 *           keccak256(abi.encode(asset, chainId, reserveAmount, decimals))
 *
 *         Asset reserve sources (off-chain):
 *           - bridged stablecoins:  AereHypERC20Collateral.lockedSupply on
 *             Ethereum mainnet for the corresponding token.
 *           - RWA tokens:           on-chain balanceOf on the contract that
 *             custodies them on AERE chain 2800 (set in the SettlementHub
 *             receiver allowlist).
 *           - native AERE:          fixed 2.8B from genesis minus burned
 *                                   balance at 0x...dEaD.
 *
 *         The Foundation off-chain ingester (Python cron on Hostinger VPS,
 *         Ledger-signed) collects these reserve numbers, builds the Merkle
 *         tree, and posts the root via proposeSnapshot.
 *
 *         IMMUTABILITY:
 *           - CHALLENGE_WINDOW constant.
 *           - Foundation can only propose / dismiss / attest — never
 *             retroactively alter an attested snapshot.
 *           - History is append-only; older snapshots remain queryable.
 */
contract AereNavOracle is Ownable {

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

    uint256 public constant CHALLENGE_WINDOW = 24 hours;

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

    enum State { Proposed, Challenged, Attested }

    struct Snapshot {
        bytes32 root;
        uint64 proposedAt;
        uint64 attestedAt;
        State state;
        string ipfsCid;   // off-chain JSON of the full reserve list, optional
    }

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

    /// @notice monotonically-increasing epoch counter.
    uint256 public latestEpoch;

    /// @notice epoch → open-challenge counter (multiple may exist).
    mapping(uint256 => uint256) public openChallenges;
    mapping(uint256 => Challenge[]) public epochChallenges;

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

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

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

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

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

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

    /// @notice Foundation publishes a new snapshot root. Starts the 24h
    ///         challenge window. Epoch must be monotonically-greater than
    ///         the latest one.
    function proposeSnapshot(uint256 epoch, bytes32 root, 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,
            ipfsCid: ipfsCid
        });
        if (epoch > latestEpoch) latestEpoch = epoch;
        emit SnapshotProposed(epoch, root, ipfsCid);
    }

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

    /// @notice Anyone can challenge a proposed snapshot within the window.
    ///         Returns the challenge id.
    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]--;
        Snapshot storage s = snapshots[epoch];
        if (openChallenges[epoch] == 0) s.state = State.Proposed;
        emit ChallengeDismissed(epoch, challengeId, response);
    }

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

    /// @notice Permissionless attestation after the 24h challenge window
    ///         with no open challenges. Locks the snapshot as attested.
    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);
    }

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

    /// @notice Verify a (asset, chainId, reserveAmount, decimals) tuple
    ///         against the attested root for `epoch`.
    /// @return ok True if the proof is valid AND the snapshot is attested.
    function verifyReserve(
        uint256 epoch,
        address asset,
        uint256 chainId,
        uint256 reserveAmount,
        uint8 decimals,
        bytes32[] calldata proof
    ) external view returns (bool ok) {
        Snapshot storage s = snapshots[epoch];
        if (s.state != State.Attested) return false;
        bytes32 leaf = keccak256(abi.encode(asset, chainId, reserveAmount, decimals));
        return MerkleProof.verify(proof, s.root, leaf);
    }

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

    /// @notice ROUND-3 FIX: bounded loop. Attacker proposing many challenged-but-
    ///         never-attested snapshots could DoS consumer dApps reading this view.
    ///         Cap at 128 epochs (~128 days at daily NAV updates).
    uint256 public constant MAX_EPOCH_SCAN = 128;

    function latestAttestedEpoch() external view returns (uint256 epoch, bytes32 root) {
        uint256 scanned = 0;
        for (uint256 e = latestEpoch; e > 0 && scanned < MAX_EPOCH_SCAN; e--) {
            if (snapshots[e].state == State.Attested) {
                return (e, snapshots[e].root);
            }
            unchecked { scanned++; }
        }
        return (0, bytes32(0));
    }
}
