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

/**
 * @title FlourishingMetricsOracleV3
 * @notice Multi-oracle consensus system for flourishing metrics.
 * @dev Security audit recommendation: eliminates single-oracle dependency
 *      by requiring multiple independent oracles to submit data and reaching
 *      consensus via median-value aggregation.
 *
 * @custom:security-enhancement From Professional Security Audit 2026
 * @custom:purpose Harden flourishing metrics against oracle manipulation
 * @custom:audit-fix V3 applies all HIGH/MEDIUM/LOW audit findings plus enhancements.
 *
 * Design:
 * - Registered oracles submit metric values during open submission windows.
 * - A minimum quorum of 3 oracles is required for consensus.
 * - Consensus value is the median of all submissions (resistant to outliers).
 * - Submissions outside ±20% of the median are flagged and filtered.
 * - The owner can register/remove oracles but cannot submit data.
 * - Only one active (non-finalized) round is permitted per metricId at a time.
 */
contract FlourishingMetricsOracleV3 {

    // ============ Errors ============

    error OnlyOwner();
    error OnlyOracle();
    error ZeroAddress();
    error OracleAlreadyRegistered();
    error OracleNotRegistered();
    error MaxOraclesReached();
    error InsufficientOracles();
    error RoundNotOpen();
    error RoundAlreadyFinalized();
    error AlreadySubmitted();
    error NoActiveRound();
    error RoundStillOpen();
    error InsufficientSubmissions();
    // audit-v3: MEDIUM-3: active round gate
    error ActiveRoundNotFinalized(uint256 existingRoundId);
    // audit-v3: LOW-2: reject zero-value submissions
    error ZeroValueSubmission();

    // ============ Events ============

    event OracleAdded(address indexed oracle);
    event OracleRemoved(address indexed oracle);
    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    event RoundStarted(
        uint256 indexed roundId,
        bytes32 indexed metricId,
        uint256 deadline
    );

    event MetricSubmitted(
        uint256 indexed roundId,
        address indexed oracle,
        uint256 value
    );

    event ConsensusReached(
        uint256 indexed roundId,
        bytes32 indexed metricId,
        uint256 consensusValue,
        uint256 submissionCount
    );

    event RoundExpired(uint256 indexed roundId);

    // audit-v3: HIGH-1: emitted when a submission is outside ±MAX_DEVIATION_BPS of the initial median
    event DeviationFlagged(
        uint256 indexed roundId,
        address indexed oracle,
        uint256 value,
        uint256 median
    );

    // ============ Enums ============

    // audit-v3: Enhancement: RoundStatus enum replaces ambiguous `finalized` bool.
    /// Open      — accepting submissions, deadline not yet passed.
    /// Finalized — consensus reached (or quorum failed) and value recorded.
    /// Expired   — deadline passed with insufficient submissions; no consensus value.
    enum RoundStatus { Open, Finalized, Expired }

    // ============ Structs ============

    struct Submission {
        address oracle;
        uint256 value;
        uint256 timestamp;
    }

    // audit-v3: Enhancement: `submissionCount` removed — use _submissions[roundId].length.
    // audit-v3: Enhancement: `finalized` bool replaced by `status` (RoundStatus enum).
    struct Round {
        uint256 roundId;
        bytes32 metricId;
        uint256 startTime;
        uint256 deadline;
        uint256 consensusValue;
        RoundStatus status;
    }

    // ============ Constants ============

    /// @notice Minimum number of oracle submissions required for consensus.
    uint256 public constant MINIMUM_QUORUM = 3;

    /// @notice Maximum number of registered oracles (Sybil prevention).
    uint256 public constant MAXIMUM_ORACLES = 50;

    /// @notice Default submission window (24 hours).
    uint256 public constant SUBMISSION_WINDOW = 24 hours;

    /// @notice Maximum deviation from median in basis points (20% = 2000 bps).
    uint256 public constant MAX_DEVIATION_BPS = 2000;

    // ============ State Variables ============

    address public owner;

    /// @notice Registered oracle addresses.
    mapping(address => bool) public isOracle;
    address[] public oracleList;
    uint256 public oracleCount;

    /// @notice Round data.
    mapping(uint256 => Round) public rounds;
    uint256 public nextRoundId;

    /// @notice Submissions per round: roundId => index => Submission.
    mapping(uint256 => Submission[]) internal _submissions;

    /// @notice Track whether an oracle has submitted for a round.
    mapping(uint256 => mapping(address => bool)) public hasSubmitted;

    /// @notice Latest consensus value per metric.
    mapping(bytes32 => uint256) public latestValue;

    /// @notice Latest round ID per metric.
    mapping(bytes32 => uint256) public latestRoundForMetric;

    // audit-v3: MEDIUM-1: timestamp of last finalization per metric for staleness checks.
    mapping(bytes32 => uint256) public latestTimestamp;

    // audit-v3: MEDIUM-3: tracks the currently active (open) round per metricId.
    mapping(bytes32 => uint256) public activeRoundForMetric;

    // ============ Modifiers ============

    modifier onlyOwner() {
        if (msg.sender != owner) revert OnlyOwner();
        _;
    }

    modifier onlyOracle() {
        if (!isOracle[msg.sender]) revert OnlyOracle();
        _;
    }

    // ============ Constructor ============

    constructor() {
        owner = msg.sender;
        nextRoundId = 1;
        emit OwnershipTransferred(address(0), msg.sender);
    }

    // ============ Oracle Management ============

    /// @notice Register a new oracle.
    /// @param oracle Address of the oracle to register.
    function addOracle(address oracle) external onlyOwner {
        if (oracle == address(0)) revert ZeroAddress();
        if (isOracle[oracle]) revert OracleAlreadyRegistered();
        if (oracleCount >= MAXIMUM_ORACLES) revert MaxOraclesReached();

        isOracle[oracle] = true;
        oracleList.push(oracle);
        oracleCount++;

        emit OracleAdded(oracle);
    }

    /// @notice Remove an oracle.
    /// @param oracle Address of the oracle to remove.
    function removeOracle(address oracle) external onlyOwner {
        if (!isOracle[oracle]) revert OracleNotRegistered();

        isOracle[oracle] = false;
        oracleCount--;

        // Remove from list (swap-and-pop)
        uint256 length = oracleList.length;
        for (uint256 i = 0; i < length;) {
            if (oracleList[i] == oracle) {
                oracleList[i] = oracleList[length - 1];
                oracleList.pop();
                break;
            }
            unchecked { ++i; }
        }

        emit OracleRemoved(oracle);
    }

    // ============ Round Management ============

    /// @notice Start a new consensus round for a metric.
    /// @dev Reverts if an unfinalized round already exists for this metricId.
    /// @param metricId Identifier for the metric being measured.
    /// @return roundId The created round ID.
    // audit-v3: MEDIUM-3: enforce single active round per metricId.
    function startRound(bytes32 metricId) external onlyOwner returns (uint256) {
        if (oracleCount < MINIMUM_QUORUM) revert InsufficientOracles();

        // MEDIUM-3: block if there is already an open round for this metricId.
        uint256 existingRoundId = activeRoundForMetric[metricId];
        if (existingRoundId != 0) {
            // roundId 0 is never used (nextRoundId starts at 1), so non-zero means set.
            Round storage existing = rounds[existingRoundId];
            if (existing.status == RoundStatus.Open) {
                revert ActiveRoundNotFinalized(existingRoundId);
            }
        }

        uint256 roundId = nextRoundId++;

        rounds[roundId] = Round({
            roundId: roundId,
            metricId: metricId,
            startTime: block.timestamp,
            deadline: block.timestamp + SUBMISSION_WINDOW,
            consensusValue: 0,
            status: RoundStatus.Open
        });

        // MEDIUM-3: register this as the active round for the metric.
        activeRoundForMetric[metricId] = roundId;

        emit RoundStarted(roundId, metricId, block.timestamp + SUBMISSION_WINDOW);
        return roundId;
    }

    /// @notice Submit a metric value for an active round.
    /// @param roundId The round to submit for.
    /// @param value The metric value.
    // audit-v3: LOW-2: reject zero-value submissions.
    function submitMetric(uint256 roundId, uint256 value) external onlyOracle {
        // LOW-2: zero values are nonsensical for flourishing metrics and can skew medians.
        if (value == 0) revert ZeroValueSubmission();

        Round storage round = rounds[roundId];
        if (round.startTime == 0) revert NoActiveRound();
        if (block.timestamp > round.deadline) revert RoundNotOpen();
        if (round.status != RoundStatus.Open) revert RoundAlreadyFinalized();
        if (hasSubmitted[roundId][msg.sender]) revert AlreadySubmitted();

        _submissions[roundId].push(Submission({
            oracle: msg.sender,
            value: value,
            timestamp: block.timestamp
        }));

        hasSubmitted[roundId][msg.sender] = true;
        // Enhancement: submissionCount removed; use _submissions[roundId].length.

        emit MetricSubmitted(roundId, msg.sender, value);
    }

    /// @notice Finalize a round and compute consensus (median value).
    /// @param roundId The round to finalize.
    // audit-v3: LOW-1: restricted to onlyOracle — oracles act as keepers.
    // audit-v3: HIGH-1: applies MAX_DEVIATION_BPS filtering after initial median.
    // audit-v3: HIGH-2: only updates latestValue if roundId is newer than current.
    // audit-v3: MEDIUM-1: records latestTimestamp on finalization.
    function finalizeRound(uint256 roundId) external onlyOracle {
        Round storage round = rounds[roundId];
        if (round.startTime == 0) revert NoActiveRound();
        if (round.status != RoundStatus.Open) revert RoundAlreadyFinalized();
        if (block.timestamp <= round.deadline) revert RoundStillOpen();

        Submission[] storage subs = _submissions[roundId];
        uint256 subCount = subs.length;

        if (subCount < MINIMUM_QUORUM) {
            // Enhancement: use Expired status for quorum-failure outcome.
            round.status = RoundStatus.Expired;
            emit RoundExpired(roundId);
            return;
        }

        // HIGH-1: Compute initial median over all submissions.
        uint256 initialMedian = _computeMedianFromValues(_extractValues(subs, subCount));

        // HIGH-1: Re-compute filtered median using only values within ±20% of initialMedian.
        (uint256 filteredMedian, uint256 filteredCount) =
            _computeFilteredMedian(roundId, subs, subCount, initialMedian);

        uint256 consensusMedian;
        if (filteredCount >= MINIMUM_QUORUM) {
            // Filtered set meets quorum — use its median.
            consensusMedian = filteredMedian;
        } else {
            // Filtered set too small — fall back to original median.
            consensusMedian = initialMedian;
        }

        round.consensusValue = consensusMedian;
        round.status = RoundStatus.Finalized;

        // HIGH-2: Only overwrite latestValue/latestRoundForMetric if this round is newer.
        if (roundId > latestRoundForMetric[round.metricId]) {
            latestValue[round.metricId] = consensusMedian;
            latestRoundForMetric[round.metricId] = roundId;
            // MEDIUM-1: record the finalization timestamp for staleness checks.
            latestTimestamp[round.metricId] = block.timestamp;
        }

        emit ConsensusReached(roundId, round.metricId, consensusMedian, subCount);
    }

    // ============ View Functions ============

    /// @notice Get the latest consensus value for a metric, with staleness metadata.
    /// @param metricId The metric identifier.
    /// @return value       The latest consensus value.
    /// @return roundId     The round where consensus was reached.
    /// @return updatedAt   Timestamp of the last finalization for this metric.
    /// @return isValid     True if a consensus value has ever been recorded.
    // audit-v3: MEDIUM-1: extended return signature with updatedAt and isValid.
    function getLatestValue(bytes32 metricId) external view returns (
        uint256 value,
        uint256 roundId,
        uint256 updatedAt,
        bool isValid
    ) {
        roundId   = latestRoundForMetric[metricId];
        value     = latestValue[metricId];
        updatedAt = latestTimestamp[metricId];
        isValid   = (roundId != 0);
    }

    /// @notice Get round details.
    /// @param roundId The round to query.
    /// @return round The round data.
    function getRound(uint256 roundId) external view returns (Round memory) {
        return rounds[roundId];
    }

    /// @notice Get submissions for a round.
    /// @param roundId The round to query.
    /// @return submissions Array of submissions.
    function getSubmissions(uint256 roundId) external view returns (Submission[] memory) {
        return _submissions[roundId];
    }

    /// @notice Get all registered oracle addresses.
    /// @return oracles Array of oracle addresses.
    function getOracles() external view returns (address[] memory) {
        return oracleList;
    }

    /// @notice Get the number of submissions for a round.
    /// @dev Enhancement: replaces removed submissionCount field on Round struct.
    /// @param roundId The round to query.
    /// @return count Number of submissions.
    function getSubmissionCount(uint256 roundId) external view returns (uint256 count) {
        count = _submissions[roundId].length;
    }

    // ============ Owner Functions ============

    /// @notice Transfer ownership.
    /// @param newOwner The new owner address.
    function transferOwnership(address newOwner) external onlyOwner {
        if (newOwner == address(0)) revert ZeroAddress();
        address oldOwner = owner;
        owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }

    // ============ Internal Functions ============

    /// @dev Extract raw values from a Submission storage array into a memory array.
    function _extractValues(
        Submission[] storage subs,
        uint256 length
    ) internal view returns (uint256[] memory values) {
        values = new uint256[](length);
        for (uint256 i = 0; i < length;) {
            values[i] = subs[i].value;
            unchecked { ++i; }
        }
    }

    /// @dev Sort a memory array in-place using insertion sort (gas-efficient for small arrays).
    function _insertionSort(uint256[] memory values) internal pure {
        uint256 length = values.length;
        for (uint256 i = 1; i < length;) {
            uint256 key = values[i];
            uint256 j = i;
            while (j > 0 && values[j - 1] > key) {
                values[j] = values[j - 1];
                j--;
            }
            values[j] = key;
            unchecked { ++i; }
        }
    }

    /// @dev Return median of a sorted or unsorted memory array.
    ///      Sorts the array in-place before computing.
    ///      Uses overflow-safe formula for even-length arrays.
    function _computeMedianFromValues(uint256[] memory values) internal pure returns (uint256) {
        uint256 length = values.length;
        _insertionSort(values);

        if (length % 2 == 0) {
            uint256 a = values[length / 2 - 1];
            uint256 b = values[length / 2];
            // Overflow-safe midpoint
            return a / 2 + b / 2 + (a % 2 + b % 2) / 2;
        } else {
            return values[length / 2];
        }
    }

    /// @dev HIGH-1: Compute a median restricted to values within ±MAX_DEVIATION_BPS of a
    ///      reference median.  Emits DeviationFlagged for each outlier.
    /// @param roundId        Round ID (for event emission).
    /// @param subs           Submissions storage reference.
    /// @param subCount       Total number of submissions.
    /// @param referenceMedian The first-pass median used as the deviation anchor.
    /// @return filteredMedian Median of the in-band submissions.
    /// @return filteredCount  Number of in-band submissions.
    // audit-v3: HIGH-1: deviation filter implementation.
    function _computeFilteredMedian(
        uint256 roundId,
        Submission[] storage subs,
        uint256 subCount,
        uint256 referenceMedian
    ) internal returns (uint256 filteredMedian, uint256 filteredCount) {
        // Calculate band boundaries: ±20% of the reference median.
        // lower = referenceMedian * (10000 - MAX_DEVIATION_BPS) / 10000
        // upper = referenceMedian * (10000 + MAX_DEVIATION_BPS) / 10000
        uint256 lower = referenceMedian * (10_000 - MAX_DEVIATION_BPS) / 10_000;
        uint256 upper = referenceMedian * (10_000 + MAX_DEVIATION_BPS) / 10_000;

        // First pass: count in-band values.
        filteredCount = 0;
        for (uint256 i = 0; i < subCount;) {
            uint256 v = subs[i].value;
            if (v >= lower && v <= upper) {
                unchecked { ++filteredCount; }
            } else {
                // HIGH-1: flag each outlier.
                emit DeviationFlagged(roundId, subs[i].oracle, v, referenceMedian);
            }
            unchecked { ++i; }
        }

        if (filteredCount == 0) {
            return (0, 0);
        }

        // Second pass: collect in-band values.
        uint256[] memory filtered = new uint256[](filteredCount);
        uint256 idx = 0;
        for (uint256 i = 0; i < subCount;) {
            uint256 v = subs[i].value;
            if (v >= lower && v <= upper) {
                filtered[idx] = v;
                unchecked { ++idx; }
            }
            unchecked { ++i; }
        }

        filteredMedian = _computeMedianFromValues(filtered);
    }
}
