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

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

/**
 * @title AereOracle
 * @notice Multi-feed price oracle with reporter quorum.
 * @dev Reporters submit price updates per symbol (e.g. "AERE/USD", "BTC/USD").
 *      The on-chain price is the median of the latest submission from each
 *      authorized reporter, with staleness protection. Designed for use by
 *      AereSwap, AereStaking liquidations, and consumer dApps.
 */
contract AereOracle is Ownable {
    uint8 public constant DECIMALS = 8;
    uint64 public maxStaleness = 5 minutes;

    address[] public reporters;
    mapping(address => bool) public isReporter;

    struct Submission {
        uint128 price;     // scaled to 8 decimals
        uint64 timestamp;
    }

    // symbol => reporter => submission
    mapping(bytes32 => mapping(address => Submission)) public submissions;
    bytes32[] public symbols;
    mapping(bytes32 => bool) private _knownSymbol;

    event ReporterAdded(address indexed reporter);
    event ReporterRemoved(address indexed reporter);
    event PriceSubmitted(bytes32 indexed symbol, address indexed reporter, uint128 price, uint64 timestamp);
    event SymbolAdded(bytes32 indexed symbol);
    event MaxStalenessUpdated(uint64 newValue);

    function addReporter(address r) external onlyOwner {
        require(r != address(0) && !isReporter[r], "bad");
        isReporter[r] = true;
        reporters.push(r);
        emit ReporterAdded(r);
    }

    function removeReporter(address r) external onlyOwner {
        require(isReporter[r], "not reporter");
        isReporter[r] = false;
        for (uint256 i = 0; i < reporters.length; i++) {
            if (reporters[i] == r) {
                reporters[i] = reporters[reporters.length - 1];
                reporters.pop();
                break;
            }
        }
        emit ReporterRemoved(r);
    }

    function setMaxStaleness(uint64 v) external onlyOwner {
        require(v >= 30 seconds && v <= 1 hours, "range");
        maxStaleness = v;
        emit MaxStalenessUpdated(v);
    }

    function submit(bytes32 symbol, uint128 price) external {
        require(isReporter[msg.sender], "not reporter");
        require(price > 0, "price");
        if (!_knownSymbol[symbol]) { _knownSymbol[symbol] = true; symbols.push(symbol); emit SymbolAdded(symbol); }
        submissions[symbol][msg.sender] = Submission(price, uint64(block.timestamp));
        emit PriceSubmitted(symbol, msg.sender, price, uint64(block.timestamp));
    }

    /// @notice Returns the median fresh price for a symbol and the count of contributors.
    function getPrice(bytes32 symbol) external view returns (uint128 price, uint64 timestamp, uint256 contributors) {
        uint256 n = reporters.length;
        uint128[] memory fresh = new uint128[](n);
        uint64 newest = 0;
        uint256 count = 0;

        for (uint256 i = 0; i < n; i++) {
            Submission memory s = submissions[symbol][reporters[i]];
            if (s.timestamp == 0) continue;
            if (block.timestamp - s.timestamp > maxStaleness) continue;
            fresh[count++] = s.price;
            if (s.timestamp > newest) newest = s.timestamp;
        }
        require(count > 0, "no fresh data");

        // insertion sort (small n in practice)
        for (uint256 i = 1; i < count; i++) {
            uint128 key = fresh[i];
            uint256 j = i;
            while (j > 0 && fresh[j - 1] > key) { fresh[j] = fresh[j - 1]; j--; }
            fresh[j] = key;
        }
        price = (count % 2 == 1) ? fresh[count / 2] : uint128((uint256(fresh[count / 2 - 1]) + uint256(fresh[count / 2])) / 2);
        return (price, newest, count);
    }

    function symbolCount() external view returns (uint256) { return symbols.length; }
    function reporterCount() external view returns (uint256) { return reporters.length; }
}
