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

/// @title ThresholdConsensusV3
/// @notice Small on-chain threshold policy for settlement/evidence conclusions.
/// @dev A conclusion is keyed by (schema, subject, nonce). All votes for that
///      key must agree on expiry, immutable settlement snapshot, and outcome.
contract ThresholdConsensusV3 {
    bytes32 public constant GOVERNANCE_POLICY_MODULE_ID = keccak256("Vaultfire.BaseV3.ThresholdConsensus");
    /// @notice A single attester can never finalize a V3 conclusion.
    /// @dev Production manifests use 3-of-5; two is retained only as the
    ///      smallest fail-closed policy for isolated test and non-pilot use.
    uint8 public constant MINIMUM_THRESHOLD = 2;

    struct Claim {
        bytes32 schema;
        bytes32 subject;
        uint96 nonce;
        uint64 expiresAt;
        bytes32 settlementSnapshot;
        bytes32 outcome;
    }

    struct Record {
        uint64 expiresAt;
        uint8 approvals;
        bool finalized;
        /// @notice Attester/threshold configuration under which approvals were cast.
        uint256 configVersion;
        bytes32 settlementSnapshot;
        bytes32 outcome;
    }

    address public owner;
    uint8 public threshold;
    uint8 public attesterCount;
    mapping(address => bool) public attester;
    mapping(bytes32 => Record) internal _records;
    /// @notice Bumps whenever an attester is added/removed or the threshold changes.
    /// @dev Partial conclusions from an older configuration must never be completed
    ///      with approvals from the replacement configuration.
    uint256 public attesterConfigVersion;
    mapping(bytes32 => mapping(address => uint256)) private _voteConfigVersion;

    error NotOwner();
    error NotAttester();
    error ZeroAddress();
    error BadThreshold();
    error ExpiredClaim();
    error DuplicateVote();
    error ConflictingClaim();
    error AlreadyFinalized();

    event AttesterSetV3(address indexed attester, bool authorized);
    event ThresholdSetV3(uint8 threshold);
    event AttesterConfigVersionUpdatedV3(uint256 indexed configVersion);
    event ClaimAttestedV3(bytes32 indexed claimKey, address indexed attester, uint8 approvals);
    event ClaimFinalizedV3(bytes32 indexed claimKey, bytes32 indexed outcome, bytes32 settlementSnapshot);

    constructor(address _owner, uint8 _threshold) {
        if (_owner == address(0)) revert ZeroAddress();
        owner = _owner;
        if (_threshold < MINIMUM_THRESHOLD) revert BadThreshold();
        threshold = _threshold;
        attesterConfigVersion = 1;
    }

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

    function setAttester(address account, bool authorized) external onlyOwner {
        if (account == address(0)) revert ZeroAddress();
        if (attester[account] != authorized) {
            attester[account] = authorized;
            attesterCount = authorized ? attesterCount + 1 : attesterCount - 1;
            _bumpAttesterConfigVersion();
        }
        emit AttesterSetV3(account, authorized);
    }

    function setThreshold(uint8 newThreshold) external onlyOwner {
        if (newThreshold < MINIMUM_THRESHOLD || newThreshold > attesterCount) revert BadThreshold();
        if (newThreshold != threshold) {
            threshold = newThreshold;
            _bumpAttesterConfigVersion();
        }
        emit ThresholdSetV3(newThreshold);
    }

    function claimKey(bytes32 schema, bytes32 subject, uint96 nonce) public pure returns (bytes32) {
        return keccak256(abi.encode(schema, subject, nonce));
    }

    function attest(Claim calldata claim) external {
        if (!attester[msg.sender]) revert NotAttester();
        if (threshold == 0 || threshold > attesterCount) revert BadThreshold();
        if (claim.expiresAt <= block.timestamp) revert ExpiredClaim();
        if (
            claim.schema == bytes32(0) || claim.subject == bytes32(0) || claim.settlementSnapshot == bytes32(0)
                || claim.outcome == bytes32(0)
        ) revert ConflictingClaim();

        bytes32 key = claimKey(claim.schema, claim.subject, claim.nonce);
        Record storage r = _records[key];
        if (r.finalized) revert AlreadyFinalized();
        if (r.configVersion != attesterConfigVersion) {
            r.expiresAt = claim.expiresAt;
            r.settlementSnapshot = claim.settlementSnapshot;
            r.outcome = claim.outcome;
            r.approvals = 0;
            r.configVersion = attesterConfigVersion;
        } else if (
            r.expiresAt != claim.expiresAt || r.settlementSnapshot != claim.settlementSnapshot
                || r.outcome != claim.outcome
        ) {
            revert ConflictingClaim();
        }
        if (_voteConfigVersion[key][msg.sender] == r.configVersion) revert DuplicateVote();

        _voteConfigVersion[key][msg.sender] = r.configVersion;
        r.approvals += 1;
        emit ClaimAttestedV3(key, msg.sender, r.approvals);
        if (r.approvals >= threshold) {
            r.finalized = true;
            emit ClaimFinalizedV3(key, r.outcome, r.settlementSnapshot);
        }
    }

    function isFinalized(bytes32 schema, bytes32 subject, uint96 nonce, bytes32 settlementSnapshot, bytes32 outcome)
        external
        view
        returns (bool)
    {
        Record storage r = _records[claimKey(schema, subject, nonce)];
        return r.finalized && block.timestamp < r.expiresAt && r.settlementSnapshot == settlementSnapshot
            && r.outcome == outcome;
    }

    /// @notice Whether `account` voted under the record's stored configuration.
    /// @dev Preserves the previous public-mapping getter ABI. Historical votes
    ///      remain visible on a finalized record so downstream self-attestation
    ///      guards stay conservative after a later configuration rotation.
    function voted(bytes32 key, address account) external view returns (bool) {
        Record storage r = _records[key];
        return r.configVersion != 0 && _voteConfigVersion[key][account] == r.configVersion;
    }

    function recordOf(bytes32 schema, bytes32 subject, uint96 nonce) external view returns (Record memory) {
        return _records[claimKey(schema, subject, nonce)];
    }

    function _bumpAttesterConfigVersion() private {
        unchecked {
            ++attesterConfigVersion;
        }
        emit AttesterConfigVersionUpdatedV3(attesterConfigVersion);
    }
}
