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

import "@openzeppelin/contracts/access/Ownable.sol";

/**
 * @title AereFoundationAggregator — Chainlink-compatible single-asset feed
 * @notice One deployment per (asset, denomination) pair. The Foundation
 *         pushes price updates (in 8-decimal Chainlink convention) backed
 *         by an off-chain attestation pipeline (e.g. fund-administrator
 *         signed NAV reports for BUIDL/USDY/OUSG, exchange median for
 *         BTC LST de-peg detection, etc.).
 *
 *         Exposes the EXACT Chainlink AggregatorV3 surface that
 *         AereLendingOracle expects:
 *           function latestRoundData() external view returns
 *               (uint80 roundId, int256 answer, uint256 startedAt,
 *                uint256 updatedAt, uint80 answeredInRound);
 *
 *         SAFETY:
 *           - `description()` is set at deploy and immutable (e.g. "BUIDL.e/USD").
 *           - `decimals()` is fixed at 8 to match the Chainlink ecosystem.
 *           - `maxJumpBps`: cap on per-update price move. Set at deploy.
 *             Foundation cannot push a single update that exceeds this cap.
 *             Forces multi-update walks for any large legitimate move
 *             (gives external observers a chance to challenge / freeze).
 *           - `minHeartbeat`: minimum seconds between updates. Prevents
 *             Foundation from spamming small updates to walk the price
 *             rapidly within a single block.
 *           - PAUSE: Foundation can pause; while paused, `latestRoundData`
 *             returns the LAST GOOD price + stale timestamp so the
 *             AereLendingOracle staleness check trips and lending falls
 *             back to "no fresh price" → safest behaviour (blocks new
 *             borrows, allows liquidations on the pre-pause price).
 *
 *         WORKFLOW:
 *           1. Foundation gets off-chain NAV (BUIDL daily, USDY hourly, etc.).
 *           2. Multisig signs `updateAnswer(price)` via Ledger.
 *           3. Contract validates: not paused, ≥ minHeartbeat since last,
 *              within maxJumpBps of current price.
 *           4. Round is incremented, answer + updatedAt stored.
 *           5. AereLendingOracle reads via configured feed.
 */
contract AereFoundationAggregator is Ownable {

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

    string public description;
    uint8 public constant decimals = 8;
    uint16 public immutable maxJumpBps;     // e.g. 500 = 5%
    uint32 public immutable minHeartbeat;   // e.g. 300 = 5 min

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

    uint80  public latestRoundId;
    int256  public latestAnswer;
    uint256 public latestUpdatedAt;
    bool    public paused;

    /// @notice roundId → {answer, updatedAt}
    mapping(uint80 => RoundData) internal _rounds;
    struct RoundData { int256 answer; uint256 updatedAt; }

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

    event AnswerUpdated(int256 indexed answer, uint256 indexed roundId, uint256 updatedAt);
    event PausedSet(bool paused);

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

    error InvalidPrice();
    error HeartbeatTooSoon(uint256 next);
    error JumpExceeded(uint256 deviationBps, uint256 cap);
    error AggregatorPaused();
    error UnknownRound();

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

    constructor(
        string memory description_,
        uint16 maxJumpBps_,
        uint32 minHeartbeat_,
        int256 initialAnswer
    ) {
        require(initialAnswer > 0, "initial price zero");
        require(maxJumpBps_ > 0 && maxJumpBps_ <= 5_000, "jump cap");
        description = description_;
        maxJumpBps = maxJumpBps_;
        minHeartbeat = minHeartbeat_;
        latestRoundId = 1;
        latestAnswer = initialAnswer;
        latestUpdatedAt = block.timestamp;
        _rounds[1] = RoundData(initialAnswer, block.timestamp);
        emit AnswerUpdated(initialAnswer, 1, block.timestamp);
    }

    /* ----------------------------- price update ----------------------------- */

    function updateAnswer(int256 newAnswer) external onlyOwner {
        if (paused) revert AggregatorPaused();
        if (newAnswer <= 0) revert InvalidPrice();
        if (block.timestamp < latestUpdatedAt + minHeartbeat) {
            revert HeartbeatTooSoon(latestUpdatedAt + minHeartbeat);
        }
        // Jump check.
        int256 prev = latestAnswer;
        uint256 absDiff;
        if (newAnswer > prev) absDiff = uint256(newAnswer - prev);
        else                  absDiff = uint256(prev - newAnswer);
        uint256 cap = (uint256(prev) * maxJumpBps) / 10_000;
        if (absDiff > cap) revert JumpExceeded((absDiff * 10_000) / uint256(prev), maxJumpBps);

        unchecked { latestRoundId += 1; }
        latestAnswer = newAnswer;
        latestUpdatedAt = block.timestamp;
        _rounds[latestRoundId] = RoundData(newAnswer, block.timestamp);
        emit AnswerUpdated(newAnswer, latestRoundId, block.timestamp);
    }

    function setPaused(bool _paused) external onlyOwner {
        paused = _paused;
        emit PausedSet(_paused);
    }

    /* ------------------------------ Chainlink view -------------------------- */

    function latestRoundData() external view returns (
        uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound
    ) {
        // When paused: still return last good values; the AereLendingOracle's
        // staleness window will eventually expire and tip downstream into
        // safe-mode automatically. We do NOT lie about updatedAt — paused
        // means the upstream attestor stopped vouching, so the on-chain
        // timestamp legitimately freezes.
        return (latestRoundId, latestAnswer, latestUpdatedAt, latestUpdatedAt, latestRoundId);
    }

    function getRoundData(uint80 roundId) external view returns (
        uint80 _roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound
    ) {
        RoundData memory r = _rounds[roundId];
        if (r.updatedAt == 0) revert UnknownRound();
        return (roundId, r.answer, r.updatedAt, r.updatedAt, roundId);
    }
}
