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

import { PullPaymentsV3 } from "../bonds/PullPaymentsV3.sol";
import { IDisputeSubjectV3, IDisputePayoutSourceV3 } from "./IDisputeSubjectV3.sol";

/// @title DisputeRegistryV3
/// @notice Subject-bound, threshold-adjudicated dispute lifecycle.
contract DisputeRegistryV3 is PullPaymentsV3 {
    uint256 public constant MAX_ADJUDICATORS = 32;

    enum Outcome {
        None,
        Upheld,
        Rejected
    }

    struct Dispute {
        address subject;
        uint256 subjectId;
        bytes32 subjectHash;
        address challenger;
        address respondent;
        /// @notice Refundable filing bond. It is returned exactly once on
        ///         either upheld or rejected finalization and is never slashed.
        uint256 refundableBondWei;
        bytes32 evidenceCommitment;
        /// @notice Exact settlement attempt captured from the subject at filing.
        bytes32 settlementSnapshot;
        uint64 settlementRequestedAt;
        uint64 challengeDeadline;
        uint64 filedAt;
        uint64 responseDeadline;
        uint64 decisionDeadline;
        uint8 adjudicatorThreshold;
        uint256 adjudicatorConfigVersion;
        uint8 approvals;
        bool responded;
        Outcome outcome;
        address payoutSource;
        uint256 payoutCap;
        bool finalized;
    }

    Dispute[] public disputes;
    mapping(address => bool) public adjudicator;
    address[] public adjudicators;
    mapping(address => uint256) private _adjudicatorIndexPlusOne;
    mapping(uint256 => mapping(address => bool)) public eligibleAdjudicator;
    mapping(uint256 => mapping(address => bool)) public voted;
    mapping(bytes32 => bool) public disputeFiledForSettlement;
    address public owner;
    uint8 public adjudicatorCount;
    /// @notice Governance-configured approvals required to uphold newly filed
    ///         disputes. A zero value intentionally leaves filing disabled.
    uint8 public requiredAdjudicatorThreshold;
    /// @notice Bumps whenever adjudicator membership changes for auditability.
    ///         Open disputes use an explicit filing-time eligibility snapshot
    ///         and are not changed or invalidated by later rotation.
    uint256 public adjudicatorConfigVersion;
    // The legacy settlement-wide getter above remains an aggregate observation
    // for ABI compatibility. Duplicate enforcement is challenger-scoped so one
    // authorized party cannot consume the other party's challenge opportunity.
    mapping(bytes32 => mapping(address => bool)) private _disputeFiledForSettlementByChallenger;

    error NotOwner();
    error NotAdjudicator();
    error NotAuthorizedChallenger();
    error NotRespondent();
    error AlreadyVoted();
    error BadDeadlines();
    error BadThreshold();
    error BadPayoutSource();
    error ResponseDeadlinePassed();
    error ResponsePending();
    error VoteDeadlinePassed();
    error DecisionDeadlineNotReached();
    error AlreadyFinalized();
    error ZeroAddress();
    error InvalidSettlementContext();
    error DuplicateDispute();

    event DisputeFiledV3(
        uint256 indexed disputeId,
        bytes32 indexed subjectHash,
        address indexed challenger,
        address respondent,
        uint256 refundableBond,
        bytes32 settlementSnapshot,
        uint64 settlementRequestedAt,
        uint64 challengeDeadline
    );
    event AdjudicatorSetV3(address indexed a, bool ok);
    event AdjudicatorThresholdUpdatedV3(uint8 requiredAdjudicatorThreshold);
    event AdjudicatorConfigVersionUpdatedV3(uint256 indexed configVersion);
    event RespondentRespondedV3(uint256 indexed disputeId, address indexed respondent);
    event VoteCastV3(uint256 indexed disputeId, address indexed adj, bool inFavor, uint256 approvals);
    event DisputeFinalizedV3(uint256 indexed disputeId, Outcome outcome);

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

    constructor(address _owner) {
        if (_owner == address(0)) revert ZeroAddress();
        owner = _owner;
    }

    function setAdjudicator(address a, bool v) external onlyOwner {
        if (a == address(0)) revert ZeroAddress();
        if (adjudicator[a] != v) {
            if (v) {
                if (adjudicators.length >= MAX_ADJUDICATORS) revert BadThreshold();
                adjudicator[a] = true;
                adjudicators.push(a);
                _adjudicatorIndexPlusOne[a] = adjudicators.length;
                adjudicatorCount += 1;
            } else {
                // Do not permit a membership update that would make the
                // governance-configured threshold unattainable for future
                // filings. Existing disputes retain their filing-time set.
                if (requiredAdjudicatorThreshold > adjudicatorCount - 1) revert BadThreshold();
                adjudicator[a] = false;
                uint256 index = _adjudicatorIndexPlusOne[a] - 1;
                uint256 lastIndex = adjudicators.length - 1;
                if (index != lastIndex) {
                    address moved = adjudicators[lastIndex];
                    adjudicators[index] = moved;
                    _adjudicatorIndexPlusOne[moved] = index + 1;
                }
                adjudicators.pop();
                delete _adjudicatorIndexPlusOne[a];
                adjudicatorCount -= 1;
            }
            unchecked {
                ++adjudicatorConfigVersion;
            }
            emit AdjudicatorConfigVersionUpdatedV3(adjudicatorConfigVersion);
        }
        emit AdjudicatorSetV3(a, v);
    }

    /// @notice Sets the approval threshold used for subsequently filed disputes.
    /// @dev Existing disputes retain their threshold snapshot. Until governance
    ///      configures a non-zero attainable value, filing is fail-closed.
    function setAdjudicatorThreshold(uint8 newThreshold) external onlyOwner {
        if (newThreshold == 0 || newThreshold > adjudicatorCount) revert BadThreshold();
        requiredAdjudicatorThreshold = newThreshold;
        emit AdjudicatorThresholdUpdatedV3(newThreshold);
    }

    function fileDispute(
        address subject,
        uint256 subjectId,
        bytes32 evidenceCommitment,
        uint64 responseDeadline,
        uint64 decisionDeadline,
        address payoutSource,
        uint256 payoutCap
    ) external payable returns (uint256 id) {
        return _fileDispute(
            subject, subjectId, evidenceCommitment, responseDeadline, decisionDeadline, payoutSource, payoutCap
        );
    }

    /// @notice Legacy ABI compatibility entrypoint.
    /// @dev The legacy caller-supplied threshold is intentionally ignored. It
    ///      cannot weaken the threshold configured by governance.
    function fileDispute(
        address subject,
        uint256 subjectId,
        bytes32 evidenceCommitment,
        uint64 responseDeadline,
        uint64 decisionDeadline,
        uint8,
        address payoutSource,
        uint256 payoutCap
    ) external payable returns (uint256 id) {
        return _fileDispute(
            subject, subjectId, evidenceCommitment, responseDeadline, decisionDeadline, payoutSource, payoutCap
        );
    }

    function _fileDispute(
        address subject,
        uint256 subjectId,
        bytes32 evidenceCommitment,
        uint64 responseDeadline,
        uint64 decisionDeadline,
        address payoutSource,
        uint256 payoutCap
    ) private returns (uint256 id) {
        if (subject == address(0) || payoutSource == address(0)) revert ZeroAddress();
        if (responseDeadline <= block.timestamp || decisionDeadline <= responseDeadline) revert BadDeadlines();
        uint8 adjudicatorThreshold = requiredAdjudicatorThreshold;
        if (adjudicatorThreshold == 0 || adjudicatorThreshold > adjudicatorCount) revert BadThreshold();
        IDisputeSubjectV3 boundSubject = IDisputeSubjectV3(subject);
        if (!boundSubject.isAuthorizedChallenger(subjectId, msg.sender)) revert NotAuthorizedChallenger();
        // The subject supplies this only while the exact settlement attempt is
        // challengeable. Capturing it here prevents old/unrelated disputes from
        // later stopping a different settlement attempt.
        (bytes32 settlementSnapshot, uint64 settlementRequestedAt, uint64 challengeDeadline) =
            boundSubject.settlementChallengeContext(subjectId);
        if (
            settlementSnapshot == bytes32(0) || settlementRequestedAt == 0 || settlementRequestedAt > block.timestamp
                || challengeDeadline <= block.timestamp || challengeDeadline <= settlementRequestedAt
        ) {
            revert InvalidSettlementContext();
        }
        // The subject accepts an upheld dispute only before its challenge deadline.
        // A decision deadline at or after that cutoff could create a timely-filed
        // dispute that can never affect the captured settlement attempt.
        if (decisionDeadline >= challengeDeadline) revert BadDeadlines();
        address respondent = boundSubject.authorizedRespondent(subjectId, msg.sender);
        if (
            respondent == address(0) || payoutSource != boundSubject.disputePayoutSource(subjectId)
                || payoutCap > boundSubject.disputePayoutCap(subjectId)
        ) revert BadPayoutSource();
        // Each authorized challenger gets one dispute process for one exact
        // settlement attempt. This prevents duplicate filings by that party
        // without allowing one party's timeout or rejection to consume the
        // other authorized party's challenge opportunity.
        bytes32 filingKey = keccak256(abi.encode(subject, subjectId, settlementSnapshot, settlementRequestedAt));
        if (_disputeFiledForSettlementByChallenger[filingKey][msg.sender]) revert DuplicateDispute();
        _disputeFiledForSettlementByChallenger[filingKey][msg.sender] = true;
        disputeFiledForSettlement[filingKey] = true;

        id = disputes.length;
        disputes.push(
            Dispute({
                subject: subject,
                subjectId: subjectId,
                subjectHash: boundSubject.disputeSubject(subjectId),
                challenger: msg.sender,
                respondent: respondent,
                refundableBondWei: msg.value,
                evidenceCommitment: evidenceCommitment,
                settlementSnapshot: settlementSnapshot,
                settlementRequestedAt: settlementRequestedAt,
                challengeDeadline: challengeDeadline,
                filedAt: uint64(block.timestamp),
                responseDeadline: responseDeadline,
                decisionDeadline: decisionDeadline,
                adjudicatorThreshold: adjudicatorThreshold,
                adjudicatorConfigVersion: adjudicatorConfigVersion,
                approvals: 0,
                responded: false,
                outcome: Outcome.None,
                payoutSource: payoutSource,
                payoutCap: payoutCap,
                finalized: false
            })
        );
        // MAX_ADJUDICATORS bounds this filing-time snapshot. Later additions
        // cannot vote on this dispute, and later removals cannot strip an
        // originally eligible adjudicator of the ability to finish it.
        uint256 eligibleCount = adjudicators.length;
        for (uint256 i; i < eligibleCount; ++i) {
            eligibleAdjudicator[id][adjudicators[i]] = true;
        }
        emit DisputeFiledV3(
            id,
            boundSubject.disputeSubject(subjectId),
            msg.sender,
            respondent,
            msg.value,
            settlementSnapshot,
            settlementRequestedAt,
            challengeDeadline
        );
    }

    function respond(uint256 id) external {
        Dispute storage d = disputes[id];
        if (d.finalized) revert AlreadyFinalized();
        if (msg.sender != d.respondent) revert NotRespondent();
        if (block.timestamp >= d.responseDeadline) revert ResponseDeadlinePassed();
        d.responded = true;
        emit RespondentRespondedV3(id, msg.sender);
    }

    function vote(uint256 id, bool inFavor) external {
        Dispute storage d = disputes[id];
        if (d.finalized) revert AlreadyFinalized();
        if (!eligibleAdjudicator[id][msg.sender]) revert NotAdjudicator();
        if (!d.responded && block.timestamp < d.responseDeadline) revert ResponsePending();
        if (block.timestamp >= d.decisionDeadline) revert VoteDeadlinePassed();
        if (voted[id][msg.sender]) revert AlreadyVoted();
        voted[id][msg.sender] = true;
        if (inFavor) d.approvals += 1;
        emit VoteCastV3(id, msg.sender, inFavor, d.approvals);
        if (d.approvals >= d.adjudicatorThreshold) _finalizeUpheld(id, d);
    }

    /// @notice Permissionless explicit default: a deadline without threshold
    ///         finalizes permanently as Rejected and refunds the filing bond.
    function finalizeByTimeout(uint256 id) external {
        Dispute storage d = disputes[id];
        if (d.finalized) revert AlreadyFinalized();
        if (block.timestamp < d.decisionDeadline) revert DecisionDeadlineNotReached();
        d.outcome = Outcome.Rejected;
        d.finalized = true;
        if (d.refundableBondWei > 0) _credit(address(0), d.challenger, d.refundableBondWei);
        emit DisputeFinalizedV3(id, Outcome.Rejected);
    }

    /// @notice Returns upheld status only for the settlement attempt captured at filing.
    /// @dev A plain subject/challenger match is deliberately insufficient: the
    ///      settlement snapshot and request time make each attempt one-shot.
    function isFinalizedUpheldForSettlement(
        uint256 id,
        address subject,
        uint256 subjectId,
        address challenger,
        bytes32 settlementSnapshot,
        uint64 settlementRequestedAt
    ) external view returns (bool upheld, uint64 filedAt) {
        Dispute storage d = disputes[id];
        upheld = d.finalized && d.outcome == Outcome.Upheld && d.subject == subject && d.subjectId == subjectId
            && d.challenger == challenger && d.settlementSnapshot == settlementSnapshot
            && d.settlementRequestedAt == settlementRequestedAt && d.filedAt >= settlementRequestedAt
            && d.filedAt < d.challengeDeadline;
        filedAt = d.filedAt;
    }

    function disputeCount() external view returns (uint256) {
        return disputes.length;
    }

    function disputeOf(uint256 id) external view returns (Dispute memory) {
        return disputes[id];
    }

    function _finalizeUpheld(uint256 id, Dispute storage d) internal {
        d.outcome = Outcome.Upheld;
        d.finalized = true;
        if (d.refundableBondWei > 0) _credit(address(0), d.challenger, d.refundableBondWei);
        if (d.payoutCap > 0) {
            IDisputePayoutSourceV3(d.payoutSource).releaseDisputePayout(id, d.challenger, d.payoutCap);
        }
        emit DisputeFinalizedV3(id, Outcome.Upheld);
    }
}
