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

import "@openzeppelin/contracts/access/Ownable2Step.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

/**
 * @title MultiOracleConsensusV3
 * @notice V3: Sybil resistance, weighted voting, commit-reveal, outlier slashing.
 */
contract MultiOracleConsensusV3 is Ownable2Step, ReentrancyGuard {

    uint256 public constant MIN_ORACLE_STAKE    = 0.1 ether;
    uint256 public constant MIN_PARTICIPATION   = 5000; // 50% in bps
    uint256 public constant COMMIT_WINDOW       = 1 hours;
    uint256 public constant REVEAL_WINDOW       = 1 hours;
    uint256 public constant OUTLIER_THRESHOLD   = 1500; // 15% deviation = outlier
    uint256 public constant SLASH_PERCENTAGE    = 1000; // 10%
    uint256 public constant TRIM_PERCENTAGE     = 1000; // trim top/bottom 10%

    error InsufficientStake();
    error RoundNotFound();
    error CommitWindowClosed();
    error RevealWindowClosed();
    error RevealWindowOpen();
    error AlreadyCommitted();
    error AlreadyRevealed();
    error InvalidReveal();
    error InsufficientParticipation();
    error OracleNotRegistered();
    error RoundAlreadyFinalized();

    struct Oracle {
        uint256 stake;
        bool    registered;
        uint256 slashedTotal;
        uint256 roundsParticipated;
    }

    struct Round {
        uint64  commitDeadline;
        uint64  revealDeadline;
        uint256 consensusValue;
        uint256 participantCount;
        uint256 totalStake;
        bool    finalized;
    }

    struct Commit {
        bytes32 commitment;  // keccak256(abi.encode(value, salt))
        bool    revealed;
        uint256 revealedValue;
    }

    mapping(address => Oracle)                          public oracles;
    mapping(uint256 => Round)                           public rounds;
    mapping(uint256 => mapping(address => Commit))      public commits;
    mapping(uint256 => address[])                       public roundParticipants;
    uint256                                             public roundCount;
    address[]                                           public oracleList;

    event OracleRegistered(address indexed oracle, uint256 stake);
    event OracleSlashed(address indexed oracle, uint256 amount, uint256 roundId);
    event RoundStarted(uint256 indexed roundId, uint64 commitDeadline, uint64 revealDeadline);
    event VoteCommitted(uint256 indexed roundId, address oracle);
    event VoteRevealed(uint256 indexed roundId, address oracle, uint256 value);
    event RoundFinalized(uint256 indexed roundId, uint256 consensusValue);

    constructor() Ownable(msg.sender) {}

    function registerOracle() external payable {
        if (msg.value < MIN_ORACLE_STAKE) revert InsufficientStake();
        require(!oracles[msg.sender].registered, "Oracle already registered");
        oracles[msg.sender] = Oracle(msg.value, true, 0, 0);
        oracleList.push(msg.sender);
        emit OracleRegistered(msg.sender, msg.value);
    }

    function startRound() external onlyOwner returns (uint256 roundId) {
        roundId = ++roundCount;
        rounds[roundId] = Round({
            commitDeadline:   uint64(block.timestamp + COMMIT_WINDOW),
            revealDeadline:   uint64(block.timestamp + COMMIT_WINDOW + REVEAL_WINDOW),
            consensusValue:   0,
            participantCount: 0,
            totalStake:       0,
            finalized:        false
        });
        emit RoundStarted(roundId, rounds[roundId].commitDeadline, rounds[roundId].revealDeadline);
    }

    function commitVote(uint256 roundId, bytes32 commitment) external nonReentrant {
        if (!oracles[msg.sender].registered) revert OracleNotRegistered();
        Round storage r = rounds[roundId];
        if (r.commitDeadline == 0)               revert RoundNotFound();
        if (block.timestamp > r.commitDeadline)  revert CommitWindowClosed();
        if (commits[roundId][msg.sender].commitment != bytes32(0)) revert AlreadyCommitted();

        commits[roundId][msg.sender] = Commit(commitment, false, 0);
        roundParticipants[roundId].push(msg.sender);
        r.participantCount++;
        r.totalStake += oracles[msg.sender].stake;
        emit VoteCommitted(roundId, msg.sender);
    }

    function revealVote(uint256 roundId, uint256 value, bytes32 salt) external nonReentrant {
        Round storage r = rounds[roundId];
        if (r.commitDeadline == 0)                revert RoundNotFound();
        if (block.timestamp <= r.commitDeadline)  revert RevealWindowClosed();
        if (block.timestamp > r.revealDeadline)   revert RevealWindowClosed();

        Commit storage c = commits[roundId][msg.sender];
        if (c.revealed) revert AlreadyRevealed();
        if (c.commitment != keccak256(abi.encode(value, salt))) revert InvalidReveal();

        c.revealed      = true;
        c.revealedValue = value;
        oracles[msg.sender].roundsParticipated++;
        emit VoteRevealed(roundId, msg.sender, value);
    }

    function finalizeRound(uint256 roundId) external nonReentrant {
        Round storage r = rounds[roundId];
        if (r.commitDeadline == 0)    revert RoundNotFound();
        if (r.finalized)              revert RoundAlreadyFinalized();
        if (block.timestamp <= r.revealDeadline) revert RevealWindowOpen();

        address[] storage participants = roundParticipants[roundId];
        uint256 n = participants.length;

        // Check participation
        uint256 participation = n > 0 && oracleList.length > 0
            ? (n * 10000) / oracleList.length
            : 0;
        if (participation < MIN_PARTICIPATION) revert InsufficientParticipation();

        // Collect revealed values
        uint256[] memory values = new uint256[](n);
        uint256 revealed = 0;
        for (uint256 i = 0; i < n; i++) {
            if (commits[roundId][participants[i]].revealed) {
                values[revealed++] = commits[roundId][participants[i]].revealedValue;
            }
        }

        // Sort (bubble sort - ok for small oracle sets)
        for (uint256 i = 0; i < revealed; i++) {
            for (uint256 j = i + 1; j < revealed; j++) {
                if (values[j] < values[i]) {
                    (values[i], values[j]) = (values[j], values[i]);
                }
            }
        }

        // Trim top/bottom 10%
        uint256 trimCount = (revealed * TRIM_PERCENTAGE) / 10000;
        uint256 start = trimCount;
        uint256 end   = revealed > trimCount ? revealed - trimCount : 0;

        uint256 sum = 0;
        uint256 count = 0;
        for (uint256 i = start; i < end; i++) {
            sum   += values[i];
            count++;
        }

        uint256 consensus = count > 0 ? sum / count : 0;
        r.consensusValue = consensus;
        r.finalized      = true;

        // Slash outliers
        for (uint256 i = 0; i < n; i++) {
            address oracle = participants[i];
            Commit storage c = commits[roundId][oracle];
            if (!c.revealed) continue;
            if (consensus == 0) continue;
            uint256 diff = c.revealedValue > consensus
                ? c.revealedValue - consensus
                : consensus - c.revealedValue;
            uint256 deviation = (diff * 10000) / consensus;
            if (deviation > OUTLIER_THRESHOLD) {
                _slashOracle(oracle, roundId);
            }
        }

        emit RoundFinalized(roundId, consensus);
    }

    function _slashOracle(address oracle, uint256 roundId) internal {
        Oracle storage o = oracles[oracle];
        uint256 slashAmount = (o.stake * SLASH_PERCENTAGE) / 10000;
        o.stake        -= slashAmount;
        o.slashedTotal += slashAmount;
        emit OracleSlashed(oracle, slashAmount, roundId);
    }

    function slashOracle(address oracle, uint256 roundId) external onlyOwner {
        _slashOracle(oracle, roundId);
    }

    function getOracleInfo(address oracle) external view returns (Oracle memory) {
        return oracles[oracle];
    }

    function getRound(uint256 roundId) external view returns (Round memory) {
        return rounds[roundId];
    }

    receive() external payable {}
}
