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

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

/**
 * @title AerePyth
 * @notice Pyth-Network-compatible pull-oracle receiver for AERE Network.
 *
 *   Implements Pyth's IPyth interface so applications written against the
 *   canonical Pyth SDK work unchanged on AERE. Once AERE is added to the
 *   public Pyth network (via PR to pyth-network/pyth-crosschain), the same
 *   contract accepts updates signed by Pyth's Guardian set with no migration.
 *
 *   In the bootstrap phase, prices are pushed by AERE Foundation's
 *   publisher set (initially: our existing oracle reporter container,
 *   later: extended to multiple publishers for redundancy). Publishers
 *   sign price updates with ECDSA — same signature scheme as AereMessenger.
 *
 *   Reference: https://docs.pyth.network/price-feeds/contract-addresses/evm
 *              https://github.com/pyth-network/pyth-sdk-solidity
 */

struct PythPrice {
    int64 price;
    uint64 conf;
    int32 expo;
    uint256 publishTime;
}

struct PythPriceFeed {
    bytes32 id;
    PythPrice price;
    PythPrice emaPrice;
}

interface IPyth {
    function getPrice(bytes32 id) external view returns (PythPrice memory price);
    function getEmaPrice(bytes32 id) external view returns (PythPrice memory price);
    function getPriceUnsafe(bytes32 id) external view returns (PythPrice memory price);
    function getEmaPriceUnsafe(bytes32 id) external view returns (PythPrice memory price);
    function getPriceNoOlderThan(bytes32 id, uint256 age) external view returns (PythPrice memory price);
    function getEmaPriceNoOlderThan(bytes32 id, uint256 age) external view returns (PythPrice memory price);
    function updatePriceFeeds(bytes[] calldata updateData) external payable;
    function updatePriceFeedsIfNecessary(bytes[] calldata updateData, bytes32[] calldata priceIds, uint64[] calldata publishTimes) external payable;
    function getUpdateFee(bytes[] calldata updateData) external view returns (uint256 feeAmount);
    function getValidTimePeriod() external view returns (uint256);
}

contract AerePyth is IPyth, Ownable, ReentrancyGuard {
    /// Authorised publisher set. Each signs price updates with ECDSA.
    mapping(address => bool) public isPublisher;
    address[] public publishers;

    /// Threshold of distinct publishers required to accept an update.
    uint8 public threshold = 1;

    /// Stored price feed by id (e.g. keccak256("BTC/USD") or canonical Pyth feed ID).
    mapping(bytes32 => PythPrice) internal _prices;
    mapping(bytes32 => PythPrice) internal _emaPrices;

    /// Per-update fee in wei AERE. Default 0; owner can raise it later
    /// to fund Foundation infrastructure or distribute back to publishers.
    uint256 public updateFeePerUpdate = 0;

    /// Maximum age (seconds) a price can be without being considered stale.
    uint256 public validTimePeriod = 60;

    // ─────────────── Update payload framing (v1) ───────────────
    //
    // Constatarea D-067. The first version of this contract inferred the length
    // of the encoded price feed from the total payload length:
    //     head = data.length - ((data.length - 64) / 65) * 65
    // For a payload of 288 + 65*k bytes that expression collapses to a constant
    // 93 for EVERY k, so `abi.decode(data[:head])` reverted on every valid
    // update that could ever be submitted. The write path was dead arithmetic.
    //
    // The replacement follows the framing discipline of the production Pyth
    // receiver (pyth-network/pyth-crosschain, PythAccumulator.sol): the payload
    // carries its own magic bytes and version, every variable-length segment is
    // announced by an explicit count, and the parser asserts that it consumed
    // the buffer EXACTLY. Nothing is inferred from the total length.
    //
    //   bytes 0..3    magic  "AEPU"
    //   byte  4       version, must be 1
    //   byte  5       sigCount, number of 65-byte signatures that follow the feed
    //   bytes 6..293  abi.encode(PythPriceFeed), fixed 288 bytes (nine static words)
    //   bytes 294..   sigCount * 65 signature bytes
    //   total length must equal 294 + 65*sigCount, no trailing bytes tolerated
    bytes4 internal constant UPDATE_MAGIC = 0x41455055; // "AEPU"
    uint8 internal constant UPDATE_VERSION = 1;
    uint256 internal constant FEED_ENCODED_LEN = 288;
    uint256 internal constant UPDATE_HEADER_LEN = 6;
    uint256 internal constant SIG_LEN = 65;

    event PriceUpdated(bytes32 indexed id, int64 price, uint64 conf, int32 expo, uint256 publishTime);
    event PublisherAdded(address indexed publisher);
    event PublisherRemoved(address indexed publisher);
    event ThresholdChanged(uint8 newThreshold);
    event FeeChanged(uint256 newFee);

    // ───────────────────── Admin ─────────────────────

    function addPublisher(address p) external onlyOwner {
        require(!isPublisher[p], "AerePyth: already");
        isPublisher[p] = true;
        publishers.push(p);
        emit PublisherAdded(p);
    }

    function removePublisher(address p) external onlyOwner {
        require(isPublisher[p], "AerePyth: not found");
        isPublisher[p] = false;
        for (uint256 i = 0; i < publishers.length; i++) {
            if (publishers[i] == p) {
                publishers[i] = publishers[publishers.length - 1];
                publishers.pop();
                break;
            }
        }
        emit PublisherRemoved(p);
    }

    function setThreshold(uint8 _t) external onlyOwner {
        require(_t > 0 && _t <= publishers.length, "AerePyth: bad threshold");
        threshold = _t;
        emit ThresholdChanged(_t);
    }

    function setUpdateFee(uint256 _fee) external onlyOwner {
        updateFeePerUpdate = _fee;
        emit FeeChanged(_fee);
    }

    function setValidTimePeriod(uint256 _seconds) external onlyOwner {
        validTimePeriod = _seconds;
    }

    function withdrawFees(address payable to, uint256 amount) external onlyOwner {
        (bool ok, ) = to.call{ value: amount }("");
        require(ok, "AerePyth: withdraw failed");
    }

    function publisherCount() external view returns (uint256) {
        return publishers.length;
    }

    // ───────────────────── Reads (IPyth) ─────────────────────

    function getPrice(bytes32 id) external view override returns (PythPrice memory price) {
        price = _prices[id];
        require(price.publishTime > 0, "AerePyth: feed unknown");
        require(block.timestamp - price.publishTime <= validTimePeriod, "AerePyth: stale");
    }

    function getPriceUnsafe(bytes32 id) external view override returns (PythPrice memory price) {
        price = _prices[id];
        require(price.publishTime > 0, "AerePyth: feed unknown");
    }

    function getEmaPrice(bytes32 id) external view override returns (PythPrice memory price) {
        price = _emaPrices[id];
        require(price.publishTime > 0, "AerePyth: ema unknown");
        require(block.timestamp - price.publishTime <= validTimePeriod, "AerePyth: stale");
    }

    function getEmaPriceUnsafe(bytes32 id) external view override returns (PythPrice memory price) {
        price = _emaPrices[id];
        require(price.publishTime > 0, "AerePyth: ema unknown");
    }

    function getPriceNoOlderThan(bytes32 id, uint256 age) external view override returns (PythPrice memory price) {
        price = _prices[id];
        require(price.publishTime > 0, "AerePyth: feed unknown");
        require(block.timestamp - price.publishTime <= age, "AerePyth: older than allowed");
    }

    function getEmaPriceNoOlderThan(bytes32 id, uint256 age) external view override returns (PythPrice memory price) {
        price = _emaPrices[id];
        require(price.publishTime > 0, "AerePyth: ema unknown");
        require(block.timestamp - price.publishTime <= age, "AerePyth: older than allowed");
    }

    function getValidTimePeriod() external view override returns (uint256) {
        return validTimePeriod;
    }

    // ───────────────────── Updates ─────────────────────

    /**
     * @notice Pyth-style pull-oracle update.
     * @dev    Each `updateData[i]` is a v1 framed payload: magic "AEPU",
     *         version byte, signature count, the 288-byte abi-encoded
     *         PythPriceFeed, then that many 65-byte ECDSA signatures over
     *         `updateDigest(feed)`. The parser never infers a length.
     */
    function updatePriceFeeds(bytes[] calldata updateData) external payable override nonReentrant {
        uint256 required = updateFeePerUpdate * updateData.length;
        require(msg.value >= required, "AerePyth: insufficient fee");

        for (uint256 i = 0; i < updateData.length; i++) {
            (PythPriceFeed memory feed, bytes calldata sigs) = _parseUpdate(updateData[i]);
            _verifyAndStore(feed, sigs);
        }
    }

    function updatePriceFeedsIfNecessary(
        bytes[] calldata updateData,
        bytes32[] calldata priceIds,
        uint64[] calldata publishTimes
    ) external payable override nonReentrant {
        require(priceIds.length == publishTimes.length, "AerePyth: length mismatch");
        // The fee is charged here too. The first version of this function took
        // no fee at all, so once `updateFeePerUpdate` was raised the same work
        // was free through this door.
        require(msg.value >= updateFeePerUpdate * updateData.length, "AerePyth: insufficient fee");

        bool applied = false;
        // Each payload is parsed exactly once, then matched against the ids the
        // caller says it needs. The previous version decoded every payload once
        // per requested id inside the inner loop.
        for (uint256 j = 0; j < updateData.length; j++) {
            (PythPriceFeed memory feed, bytes calldata sigs) = _parseUpdate(updateData[j]);
            for (uint256 i = 0; i < priceIds.length; i++) {
                if (feed.id == priceIds[i] && _prices[priceIds[i]].publishTime < publishTimes[i]) {
                    _verifyAndStore(feed, sigs);
                    applied = true;
                    break;
                }
            }
        }
        // Same choice the canonical Pyth receiver makes: say so instead of
        // taking the fee and silently doing nothing.
        require(applied, "AerePyth: no fresh update");
    }

    /**
     * @notice The exact 32-byte hash a publisher must personal_sign for `feed`.
     * @dev    Bound to this chain and this contract, so a publisher signature
     *         cannot be replayed into another deployment, or onto another chain
     *         that shares the same publisher key. The publisher signs THIS with
     *         EIP-191 personal_sign; the contract adds the same prefix before
     *         ecrecover.
     */
    function updateSigningHash(PythPriceFeed calldata feed) external view returns (bytes32) {
        return _updatePayloadHash(feed);
    }

    function _updatePayloadHash(PythPriceFeed memory feed) internal view returns (bytes32) {
        return keccak256(abi.encode(UPDATE_MAGIC, UPDATE_VERSION, block.chainid, address(this), feed));
    }

    function _updateDigest(PythPriceFeed memory feed) internal view returns (bytes32) {
        return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", _updatePayloadHash(feed)));
    }

    function _parseUpdate(bytes calldata data)
        internal
        pure
        returns (PythPriceFeed memory feed, bytes calldata sigs)
    {
        require(data.length >= UPDATE_HEADER_LEN + FEED_ENCODED_LEN, "AerePyth: update too short");
        require(bytes4(data[0:4]) == UPDATE_MAGIC, "AerePyth: bad magic");
        require(uint8(data[4]) == UPDATE_VERSION, "AerePyth: bad version");
        uint256 sigCount = uint8(data[5]);
        require(sigCount > 0, "AerePyth: no signatures");
        // Exact consumption. Nothing is inferred from data.length; the length
        // the header ANNOUNCES has to be the length that was sent.
        require(
            data.length == UPDATE_HEADER_LEN + FEED_ENCODED_LEN + sigCount * SIG_LEN,
            "AerePyth: bad payload length"
        );
        feed = abi.decode(data[UPDATE_HEADER_LEN:UPDATE_HEADER_LEN + FEED_ENCODED_LEN], (PythPriceFeed));
        sigs = data[UPDATE_HEADER_LEN + FEED_ENCODED_LEN:];
    }

    function _verifyAndStore(PythPriceFeed memory feed, bytes calldata sigs) internal {
        uint256 sigCount = sigs.length / SIG_LEN;
        require(sigCount >= threshold, "AerePyth: below threshold");

        bytes32 digest = _updateDigest(feed);

        address[] memory seen = new address[](sigCount);
        uint256 validCount = 0;
        for (uint256 i = 0; i < sigCount; i++) {
            bytes32 r; bytes32 s; uint8 v;
            assembly {
                let off := add(sigs.offset, mul(i, 65))
                r := calldataload(off)
                s := calldataload(add(off, 32))
                v := byte(0, calldataload(add(off, 64)))
            }
            address signer = ecrecover(digest, v, r, s);
            require(signer != address(0) && isPublisher[signer], "AerePyth: bad publisher");
            for (uint256 j = 0; j < validCount; j++) {
                require(seen[j] != signer, "AerePyth: duplicate signer");
            }
            seen[validCount] = signer;
            validCount++;
        }

        // Only apply if newer than what we have.
        if (feed.price.publishTime > _prices[feed.id].publishTime) {
            _prices[feed.id] = feed.price;
            _emaPrices[feed.id] = feed.emaPrice;
            emit PriceUpdated(feed.id, feed.price.price, feed.price.conf, feed.price.expo, feed.price.publishTime);
        }
    }

    function getUpdateFee(bytes[] calldata updateData) external view override returns (uint256 feeAmount) {
        return updateFeePerUpdate * updateData.length;
    }

    receive() external payable {}
}
