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

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

/**
 * @title AereOracleAdapter — multi-source price oracle for AereLendingMarket
 * @notice Per-asset price feeds. Each asset has a primary feed (Chainlink-
 *         style aggregator OR a Foundation-attested NAV pulled from the
 *         AereNavOracle for RWAs that don't have a live market price). A
 *         staleness window protects against frozen feeds. A deviation cap
 *         vs the last good price protects against single-tick manipulation.
 *
 *         FEEDS:
 *           type 0 = ChainlinkLike (latestRoundData() returning (uint80, int256, uint256, uint256, uint80))
 *           type 1 = AereNavOracle (queryable rate stamped on a per-asset basis)
 *           type 2 = ConstantOne (for assets pegged 1:1 to USD by mandate, e.g. BUIDL pre-launch)
 *
 *         All prices normalised to 18 decimals.
 *
 *         GOVERNANCE:
 *           - Foundation (Ownable) can add/update feeds, set staleness +
 *             deviation guards. NO admin price overrides — only the upstream
 *             feed source can move the price. This guarantees the
 *             Foundation cannot front-run liquidations.
 *           - Foundation CAN circuit-break (set asset to paused), which
 *             causes the lending market to halt new borrows but NOT
 *             liquidations (so bad debt can still be cleared).
 */
contract AereLendingOracle is Ownable {

    enum FeedType { ChainlinkLike, NavOracle, ConstantOne }

    struct Feed {
        bool configured;
        bool paused;
        FeedType feedType;
        address source;            // for ChainlinkLike: aggregator addr; for NavOracle: AereNavOracle addr
        bytes32 navAssetKey;       // for NavOracle feeds only
        uint32 stalenessSeconds;   // max permitted age of feed reading
        uint16 maxDeviationBps;    // max bps move vs lastGoodPrice; 0 = no check
        uint8 sourceDecimals;       // feed-native decimals (typically 8 for Chainlink)
        uint256 lastGoodPrice;      // 18-decimals normalised
        uint64 lastGoodTimestamp;
    }

    mapping(address => Feed) public feeds;

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

    event FeedConfigured(address indexed asset, FeedType feedType, address source, uint32 stalenessSeconds, uint16 maxDeviationBps);
    event FeedPaused(address indexed asset, bool paused);
    event PriceUpdated(address indexed asset, uint256 price, uint64 timestamp);

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

    error FeedNotConfigured();
    error FeedPausedErr();
    error FeedStale();
    error FeedDeviationExceeded();
    error InvalidFeedResponse();
    error ZeroAddress();
    error ZeroPrice();

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

    /// @notice Returns the latest acceptable price for `asset` normalised to 18 decimals.
    /// @dev    Reverts if the feed is paused / stale / deviation-violated.
    function getPrice(address asset) external view returns (uint256 price) {
        Feed memory f = feeds[asset];
        if (!f.configured) revert FeedNotConfigured();
        if (f.paused) revert FeedPausedErr();
        price = _readNormalised(f);
        if (price == 0) revert ZeroPrice();
        // Deviation check (only if we have a lastGoodPrice and a cap).
        if (f.lastGoodPrice != 0 && f.maxDeviationBps != 0) {
            uint256 ref = f.lastGoodPrice;
            uint256 diff = price > ref ? price - ref : ref - price;
            uint256 maxDiff = (ref * f.maxDeviationBps) / 10_000;
            if (diff > maxDiff) revert FeedDeviationExceeded();
        }
    }

    /// @notice Price for the LIQUIDATION path ONLY. Honors the lending market's
    ///         "LIQUIDATIONS NEVER PAUSE" invariant: it returns the best available
    ///         price WITHOUT the pause, deviation-cap, or staleness reverts that
    ///         gate new borrows, so a paused feed, a crash larger than the
    ///         deviation cap, a stale feed, or even a reverting upstream aggregator
    ///         can NEVER freeze the liquidations that clear bad debt. It prefers a
    ///         real fresh reading (you want the true crash price), falls back to
    ///         lastGoodPrice, and reverts only when no usable price exists at all.
    ///         SECURITY-FIX (adversarial-review HIGH 2026-07-12): getPrice()'s
    ///         pause/deviation reverts previously froze liquidate(), contradicting
    ///         the documented invariant and concentrating bad debt on suppliers.
    function getPriceForLiquidation(address asset) external view returns (uint256 price) {
        Feed memory f = feeds[asset];
        if (!f.configured) revert FeedNotConfigured();
        // Staleness-tolerant fresh read via an external self-call, so a reverting
        // upstream aggregator degrades to the lastGoodPrice fallback rather than
        // freezing the liquidation.
        try this.readLiquidationUpstream(asset) returns (uint256 p) {
            price = p;
        } catch {
            price = 0;
        }
        if (price == 0) price = f.lastGoodPrice; // fall back to the last known reference
        if (price == 0) revert ZeroPrice();
    }

    /// @dev Upstream read for the liquidation path: staleness-TOLERANT (a stale
    ///      price beats a frozen liquidation) and returns 0 instead of reverting on
    ///      a malformed/zero/future-dated response so the caller can fall back to
    ///      lastGoodPrice. External so getPriceForLiquidation can try/catch it
    ///      against a reverting aggregator. Applies NO pause and NO deviation cap.
    function readLiquidationUpstream(address asset) external view returns (uint256) {
        Feed memory f = feeds[asset];
        if (f.feedType == FeedType.ConstantOne) return 1e18;
        if (f.feedType == FeedType.ChainlinkLike) {
            (, int256 answer,, uint256 updatedAt,) = IChainlinkLike(f.source).latestRoundData();
            if (answer <= 0 || updatedAt == 0 || updatedAt > block.timestamp) return 0;
            return _normalize(uint256(answer), f.sourceDecimals); // staleness tolerated
        }
        (uint256 nav, uint64 ts) = IAereNavOracleAdapter(f.source).latestForAsset(f.navAssetKey);
        if (nav == 0 || ts == 0 || uint256(ts) > block.timestamp) return 0;
        return _normalize(nav, f.sourceDecimals);
    }

    /// @notice Push the current upstream reading into `lastGoodPrice` state.
    /// @dev    OWNER-ONLY to prevent attackers from locking in a manipulated
    ///         reference price during a one-tick exploit window. The owner
    ///         is expected to poke after planned legitimate moves (e.g.
    ///         monthly RWA NAV updates) so deviation caps stay tight.
    ///         A permissionless `pokeBounded` lets anyone advance the
    ///         reference IFF the new price is within `maxDeviationBps` of
    ///         the existing `lastGoodPrice` — this lets the reference
    ///         track real markets without giving anyone a one-tick override.
    function poke(address asset) external onlyOwner {
        Feed storage f = feeds[asset];
        if (!f.configured) revert FeedNotConfigured();
        uint256 price = _readNormalised(f);
        if (price == 0) revert ZeroPrice();
        f.lastGoodPrice = price;
        f.lastGoodTimestamp = uint64(block.timestamp);
        emit PriceUpdated(asset, price, uint64(block.timestamp));
    }

    /// @notice Permissionless poke restricted to within-band moves.
    ///         If lastGoodPrice is zero (never poked), reverts — caller
    ///         must wait for the owner's first poke.
    function pokeBounded(address asset) external {
        Feed storage f = feeds[asset];
        if (!f.configured) revert FeedNotConfigured();
        if (f.lastGoodPrice == 0) revert FeedNotConfigured();
        uint256 price = _readNormalised(f);
        if (price == 0) revert ZeroPrice();
        if (f.maxDeviationBps != 0) {
            uint256 ref = f.lastGoodPrice;
            uint256 diff = price > ref ? price - ref : ref - price;
            uint256 maxDiff = (ref * f.maxDeviationBps) / 10_000;
            if (diff > maxDiff) revert FeedDeviationExceeded();
        }
        f.lastGoodPrice = price;
        f.lastGoodTimestamp = uint64(block.timestamp);
        emit PriceUpdated(asset, price, uint64(block.timestamp));
    }

    /* -------------------------------- admin --------------------------------- */

    function configureFeed(
        address asset,
        FeedType feedType,
        address source,
        bytes32 navAssetKey,
        uint32 stalenessSeconds,
        uint16 maxDeviationBps,
        uint8 sourceDecimals
    ) external onlyOwner {
        if (asset == address(0)) revert ZeroAddress();
        if (feedType != FeedType.ConstantOne && source == address(0)) revert ZeroAddress();
        Feed storage f = feeds[asset];
        f.configured = true;
        f.feedType = feedType;
        f.source = source;
        f.navAssetKey = navAssetKey;
        f.stalenessSeconds = stalenessSeconds;
        f.maxDeviationBps = maxDeviationBps;
        f.sourceDecimals = sourceDecimals;

        // Auto-poke: lock in the current upstream reading as the deviation
        // reference. Prevents the documented "never-poked feed skips
        // deviation silently" exploit on freshly configured feeds.
        uint256 initial = _readNormalised(f);
        if (initial == 0) revert ZeroPrice();
        f.lastGoodPrice = initial;
        f.lastGoodTimestamp = uint64(block.timestamp);

        emit FeedConfigured(asset, feedType, source, stalenessSeconds, maxDeviationBps);
        emit PriceUpdated(asset, initial, uint64(block.timestamp));
    }

    function setFeedPaused(address asset, bool paused) external onlyOwner {
        Feed storage f = feeds[asset];
        if (!f.configured) revert FeedNotConfigured();
        f.paused = paused;
        emit FeedPaused(asset, paused);
    }

    /* ----------------------------- internal --------------------------------- */

    function _readNormalised(Feed memory f) internal view returns (uint256) {
        if (f.feedType == FeedType.ConstantOne) {
            return 1e18;
        }
        if (f.feedType == FeedType.ChainlinkLike) {
            (, int256 answer,, uint256 updatedAt,) = IChainlinkLike(f.source).latestRoundData();
            if (answer <= 0 || updatedAt == 0) revert InvalidFeedResponse();
            // ROUND-3 FIX: future-dated timestamp would underflow the
            // subtraction and panic-revert (DoS the lending market). Treat
            // future-dated feeds as invalid responses instead.
            if (updatedAt > block.timestamp) revert InvalidFeedResponse();
            if (block.timestamp - updatedAt > f.stalenessSeconds) revert FeedStale();
            return _normalize(uint256(answer), f.sourceDecimals);
        }
        // NavOracle
        (uint256 nav, uint64 ts) = IAereNavOracleAdapter(f.source).latestForAsset(f.navAssetKey);
        if (ts == 0 || nav == 0) revert InvalidFeedResponse();
        if (uint256(ts) > block.timestamp) revert InvalidFeedResponse();
        if (block.timestamp - ts > f.stalenessSeconds) revert FeedStale();
        return _normalize(nav, f.sourceDecimals);
    }

    function _normalize(uint256 raw, uint8 dec) internal pure returns (uint256) {
        if (dec == 18) return raw;
        if (dec < 18) return raw * (10 ** (18 - dec));
        return raw / (10 ** (dec - 18));
    }
}

interface IChainlinkLike {
    function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80);
}

interface IAereNavOracleAdapter {
    function latestForAsset(bytes32 assetKey) external view returns (uint256 nav, uint64 timestamp);
}
