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

import "../contracts/PrivacyGuarantees.sol";
import "./ERC8004IdentityRegistryV3.sol";
import "../contracts/BeliefAttestationVerifier.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

/**
 * @title ERC-8004 Validation Registry V3 for VaultFire
 * @notice Cryptoeconomic validation mechanisms for agent claims
 * @dev V3 — Applies audit fixes: per-request escrow, duplicate prevention,
 *      dynamic rewards, deadline/cancel, safe decrements, ZK claim binding.
 *
 * **Mission Alignment:**
 * - Privacy over surveillance: ZK proofs validate without revealing data
 * - Morals over metrics: Economic stakes prevent lying
 * - Freedom over control: Open validation, anyone can verify
 * - Trust through proof: Cryptographic certainty, not promises
 *
 * **V3 Changes (2026-02 Partner-Ready Audit):**
 * - CRITICAL-VR-01: Per-request escrow replaces fixed VALIDATION_REWARD
 * - HIGH-VR-01: Duplicate response prevention via hasValidated mapping
 * - HIGH-VR-03: payable removed from submitValidation / submitValidationZK
 * - MEDIUM-VR-01: Request deadline + cancelRequest for escrow recovery
 * - MEDIUM-VR-02: Safe decrement guard on validatorActiveValidations
 * - MEDIUM-VR-03: ZK publicInputs[0] bound to request.claimHash
 *
 * @custom:security Economic stakes + ZK proofs + multi-validator consensus
 * @custom:ethics Privacy-preserving validation (no data extraction)
 */
/// @custom:audit-fix HIGH-001 — Added ReentrancyGuard; replaced .transfer() with .call{} (2026-02-23)
contract ERC8004ValidationRegistryV3 is PrivacyGuarantees, ReentrancyGuard {

    ERC8004IdentityRegistryV3 public immutable identityRegistry;
    BeliefAttestationVerifier public immutable zkVerifier;

    enum ValidationType {
        STAKER_RERUN,      // Validator re-runs the agent's task
        ZK_PROOF,          // Zero-knowledge proof verification
        TEE_ORACLE,        // Trusted Execution Environment verification
        TRUSTED_JUDGE,     // Human expert judgment
        MULTI_VALIDATOR    // Consensus of multiple validators
    }

    enum ValidationStatus {
        PENDING,
        APPROVED,
        REJECTED,
        DISPUTED,
        CANCELLED
    }

    struct ValidationRequest {
        uint256 requestId;
        address agentAddress;
        address requester;
        string claimURI;           // Off-chain URI describing the claim
        bytes32 claimHash;         // Hash of claim for verification
        ValidationType validationType;
        uint256 stakeAmount;       // Economic stake for validation
        uint256 createdAt;
        uint256 deadline;          // /// @custom:audit-v3 MEDIUM-VR-01: request deadline
        ValidationStatus status;
        uint256 validatorsRequired; // For multi-validator consensus
        uint256 approvalsCount;
        uint256 rejectionsCount;
    }

    struct ValidationResponse {
        uint256 responseId;
        uint256 requestId;
        address validator;
        bool approved;
        string evidenceURI;        // Off-chain URI to validation evidence
        bytes zkProof;             // Zero-knowledge proof (if applicable)
        uint256 timestamp;
        uint256 validatorStake;    // Snapshot of validator's stake at response time
    }

    // Request ID => Validation Request
    mapping(uint256 => ValidationRequest) public validationRequests;
    uint256 public nextRequestId = 1;

    // Request ID => Response IDs
    mapping(uint256 => uint256[]) public requestResponses;

    // Response ID => Validation Response
    mapping(uint256 => ValidationResponse) public validationResponses;
    uint256 public nextResponseId = 1;

    // Validator address => total stake
    mapping(address => uint256) public validatorStakes;

    // Validator address => active validation count
    mapping(address => uint256) public validatorActiveValidations;

    // Agent address => request IDs (for discovery / pagination)
    mapping(address => uint256[]) public agentValidationRequests;

    /// @custom:audit-v3 CRITICAL-VR-01: Per-request escrow mapping
    mapping(uint256 => uint256) public requestEscrow;

    /// @custom:audit-v3 HIGH-VR-01: Duplicate response prevention
    mapping(uint256 => mapping(address => bool)) public hasValidated;

    // Minimum stake required to become validator
    uint256 public constant MIN_VALIDATOR_STAKE = 1 ether;

    /// @custom:audit-v3 MEDIUM-VR-01: Request deadline constant
    uint256 public constant REQUEST_DEADLINE = 7 days;

    // NOTE: VALIDATION_REWARD is kept for reference in slashing logic,
    // but reward distribution now uses per-request escrow (CRITICAL-VR-01).
    uint256 public constant VALIDATION_REWARD = 0.1 ether;

    event ValidationRequested(
        uint256 indexed requestId,
        address indexed agentAddress,
        address indexed requester,
        ValidationType validationType,
        uint256 stakeAmount,
        uint256 timestamp
    );

    event ValidationResponseSubmitted(
        uint256 indexed responseId,
        uint256 indexed requestId,
        address indexed validator,
        bool approved,
        uint256 timestamp
    );

    event ValidationCompleted(
        uint256 indexed requestId,
        ValidationStatus finalStatus,
        uint256 timestamp
    );

    event ValidatorStaked(
        address indexed validator,
        uint256 amount,
        uint256 totalStake,
        uint256 timestamp
    );

    event ValidatorSlashed(
        address indexed validator,
        uint256 amount,
        string reason,
        uint256 timestamp
    );

    event ValidatorStakeWithdrawn(
        address indexed validator,
        uint256 amount,
        uint256 remainingStake,
        uint256 timestamp
    );

    /// @custom:audit-v3 MEDIUM-VR-01: Cancellation event
    event ValidationRequestCancelled(
        uint256 indexed requestId,
        address indexed requester,
        uint256 escrowReturned,
        uint256 timestamp
    );

    constructor(
        address _identityRegistry,
        address _zkVerifier
    ) {
        require(_identityRegistry != address(0), "Invalid identity registry");
        require(_zkVerifier != address(0), "Invalid ZK verifier");
        identityRegistry = ERC8004IdentityRegistryV3(_identityRegistry);
        zkVerifier = BeliefAttestationVerifier(_zkVerifier);
    }

    /**
     * @notice Request validation of an agent claim
     * @param agentAddress Address of agent making the claim
     * @param claimURI Off-chain URI describing the claim
     * @param claimHash Hash of claim data for verification
     * @param validationType Type of validation required
     * @param validatorsRequired Number of validators for consensus (if multi-validator)
     */
    function requestValidation(
        address agentAddress,
        string calldata claimURI,
        bytes32 claimHash,
        ValidationType validationType,
        uint256 validatorsRequired
    ) external payable {
        require(agentAddress != address(0), "Invalid agent address");
        require(identityRegistry.isAgentActive(agentAddress), "Agent not registered");
        require(bytes(claimURI).length > 0, "Claim URI required");
        require(msg.value >= MIN_VALIDATOR_STAKE, "Insufficient stake");

        if (validationType == ValidationType.MULTI_VALIDATOR) {
            require(validatorsRequired >= 3, "Multi-validator needs >=3 validators");
        } else {
            validatorsRequired = 1;
        }

        uint256 requestId = nextRequestId++;

        /// @custom:audit-v3 CRITICAL-VR-01: Store escrow per request
        requestEscrow[requestId] = msg.value;

        /// @custom:audit-v3 MEDIUM-VR-01: Set deadline at creation time
        validationRequests[requestId] = ValidationRequest({
            requestId: requestId,
            agentAddress: agentAddress,
            requester: msg.sender,
            claimURI: claimURI,
            claimHash: claimHash,
            validationType: validationType,
            stakeAmount: msg.value,
            createdAt: block.timestamp,
            deadline: block.timestamp + REQUEST_DEADLINE,
            status: ValidationStatus.PENDING,
            validatorsRequired: validatorsRequired,
            approvalsCount: 0,
            rejectionsCount: 0
        });

        // Track request for the agent (for paginated queries)
        agentValidationRequests[agentAddress].push(requestId);

        emit ValidationRequested(
            requestId,
            agentAddress,
            msg.sender,
            validationType,
            msg.value,
            block.timestamp
        );
    }

    /**
     * @notice Submit validation response
     * @param requestId Validation request ID
     * @param approved Whether the claim is validated
     * @param evidenceURI Off-chain URI to validation evidence
     * @param zkProof Zero-knowledge proof (if ZK validation type)
     *
     * @dev HIGH-VR-03: Removed payable — validators must pre-stake via stakeAsValidator.
     */
    /// @custom:audit-v3 HIGH-VR-03: payable removed; validators pre-stake via stakeAsValidator
    /// @custom:audit-v3 HIGH-VR-01: Duplicate response prevention added
    function submitValidation(
        uint256 requestId,
        bool approved,
        string calldata evidenceURI,
        bytes calldata zkProof
    ) external nonReentrant {
        ValidationRequest storage request = validationRequests[requestId];
        require(request.status == ValidationStatus.PENDING, "Request not pending");
        require(validatorStakes[msg.sender] >= MIN_VALIDATOR_STAKE, "Insufficient validator stake");

        /// @custom:audit-v3 HIGH-VR-01: Prevent duplicate responses per validator per request
        require(!hasValidated[requestId][msg.sender], "Already submitted response");
        hasValidated[requestId][msg.sender] = true;

        // For ZK proof validation:
        // - submitValidation() is backward compatible and stores the proof bytes.
        // - For enforceable verification, use submitValidationZK() which calls the zkVerifier.
        if (request.validationType == ValidationType.ZK_PROOF) {
            require(zkProof.length > 0, "ZK proof required");
        }

        uint256 responseId = nextResponseId++;

        validationResponses[responseId] = ValidationResponse({
            responseId: responseId,
            requestId: requestId,
            validator: msg.sender,
            approved: approved,
            evidenceURI: evidenceURI,
            zkProof: zkProof,
            timestamp: block.timestamp,
            validatorStake: validatorStakes[msg.sender]
        });

        requestResponses[requestId].push(responseId);
        validatorActiveValidations[msg.sender] += 1;

        // Update approval/rejection counts
        if (approved) {
            request.approvalsCount += 1;
        } else {
            request.rejectionsCount += 1;
        }

        emit ValidationResponseSubmitted(
            responseId,
            requestId,
            msg.sender,
            approved,
            block.timestamp
        );

        // Check if validation is complete
        _checkValidationComplete(requestId);
    }

    /**
     * @notice Submit a ZK validation response with on-chain proof verification.
     * @dev Enforces zkVerifier.verifyProof(). If the proof is invalid, this call reverts.
     *
     * @dev HIGH-VR-03: Removed payable — validators must pre-stake via stakeAsValidator.
     * @dev MEDIUM-VR-03: publicInputs[0] must equal uint256(request.claimHash).
     */
    /// @custom:audit-v3 HIGH-VR-03: payable removed
    /// @custom:audit-v3 HIGH-VR-01: Duplicate response prevention added
    /// @custom:audit-v3 MEDIUM-VR-03: publicInputs[0] bound to claimHash
    function submitValidationZK(
        uint256 requestId,
        bool approved,
        string calldata evidenceURI,
        bytes calldata proofBytes,
        uint256[] calldata publicInputs
    ) external nonReentrant {
        ValidationRequest storage request = validationRequests[requestId];
        require(request.status == ValidationStatus.PENDING, "Request not pending");
        require(request.validationType == ValidationType.ZK_PROOF, "Not a ZK validation request");
        require(validatorStakes[msg.sender] >= MIN_VALIDATOR_STAKE, "Insufficient validator stake");
        require(proofBytes.length > 0, "ZK proof required");

        /// @custom:audit-v3 HIGH-VR-01: Prevent duplicate responses
        require(!hasValidated[requestId][msg.sender], "Already submitted response");
        hasValidated[requestId][msg.sender] = true;

        /// @custom:audit-v3 MEDIUM-VR-03: Bind publicInputs to the claim hash
        require(publicInputs.length > 0, "Public inputs required");
        require(publicInputs[0] == uint256(request.claimHash), "publicInputs[0] must equal claimHash");

        bool ok = zkVerifier.verifyProof(proofBytes, publicInputs);
        require(ok, "ZK proof invalid");

        uint256 responseId = nextResponseId++;

        validationResponses[responseId] = ValidationResponse({
            responseId: responseId,
            requestId: requestId,
            validator: msg.sender,
            approved: approved,
            evidenceURI: evidenceURI,
            zkProof: proofBytes,
            timestamp: block.timestamp,
            validatorStake: validatorStakes[msg.sender]
        });

        requestResponses[requestId].push(responseId);
        validatorActiveValidations[msg.sender] += 1;

        if (approved) {
            request.approvalsCount += 1;
        } else {
            request.rejectionsCount += 1;
        }

        emit ValidationResponseSubmitted(responseId, requestId, msg.sender, approved, block.timestamp);

        _checkValidationComplete(requestId);
    }

    /**
     * @notice Cancel a validation request after the deadline and recover escrow.
     * @param requestId Validation request ID
     *
     * @dev MEDIUM-VR-01: Only callable by the original requester, only after deadline.
     */
    /// @custom:audit-v3 MEDIUM-VR-01: Deadline-based cancellation with escrow recovery
    function cancelRequest(uint256 requestId) external nonReentrant {
        ValidationRequest storage request = validationRequests[requestId];
        require(request.requester == msg.sender, "Not the requester");
        require(request.status == ValidationStatus.PENDING, "Request not pending");
        require(block.timestamp > request.deadline, "Deadline not reached");

        uint256 escrow = requestEscrow[requestId];
        request.status = ValidationStatus.CANCELLED;
        requestEscrow[requestId] = 0;

        emit ValidationRequestCancelled(requestId, msg.sender, escrow, block.timestamp);

        if (escrow > 0) {
            (bool success, ) = payable(msg.sender).call{value: escrow}("");
            require(success, "Escrow return failed");
        }
    }

    /**
     * @notice Internal: Check if validation is complete
     * @param requestId Validation request ID
     */
    function _checkValidationComplete(uint256 requestId) internal {
        ValidationRequest storage request = validationRequests[requestId];

        uint256 totalResponses = request.approvalsCount + request.rejectionsCount;

        if (totalResponses >= request.validatorsRequired) {
            // Determine final status based on majority
            if (request.approvalsCount > request.rejectionsCount) {
                request.status = ValidationStatus.APPROVED;
                _distributeRewards(requestId, true);
            } else if (request.rejectionsCount > request.approvalsCount) {
                request.status = ValidationStatus.REJECTED;
                _distributeRewards(requestId, false);
            } else {
                request.status = ValidationStatus.DISPUTED;
            }

            emit ValidationCompleted(
                requestId,
                request.status,
                block.timestamp
            );
        }
    }

    /**
     * @notice Internal: Distribute rewards to validators from per-request escrow.
     * @param requestId Validation request ID
     * @param approved Whether validation was approved (majority side)
     *
     * @dev CRITICAL-VR-01: rewardPerValidator computed from requestEscrow[requestId].
     *      Unspent escrow is returned to the requester.
     * @dev MEDIUM-VR-02: Safe decrement guard on validatorActiveValidations.
     */
    // slither-disable-next-line reentrancy-eth
    /// @custom:audit-v3 CRITICAL-VR-01: Per-request escrow distribution
    /// @custom:audit-v3 MEDIUM-VR-02: Safe decrement of validatorActiveValidations
    function _distributeRewards(uint256 requestId, bool approved) internal {
        ValidationRequest storage request = validationRequests[requestId];
        uint256[] memory responses = requestResponses[requestId];

        // Count majority validators to compute per-validator reward
        uint256 majorityCount = approved ? request.approvalsCount : request.rejectionsCount;

        /// @custom:audit-v3 CRITICAL-VR-01: Compute reward from escrow, not fixed constant
        uint256 escrow = requestEscrow[requestId];
        requestEscrow[requestId] = 0; // Clear escrow state before any transfers (CEI)

        uint256 rewardPerValidator = majorityCount > 0 ? escrow / majorityCount : 0;
        uint256 totalRewarded = 0;

        for (uint256 i = 0; i < responses.length; i++) {
            ValidationResponse storage response = validationResponses[responses[i]];
            address validator = response.validator;

            // Reward validators who voted with the majority
            if (response.approved == approved) {
                /// @custom:audit-v3 MEDIUM-VR-02: Safe decrement guard
                if (validatorActiveValidations[validator] > 0) {
                    validatorActiveValidations[validator] -= 1;
                }
                totalRewarded += rewardPerValidator;
                // CEI: state already updated above before external call
                (bool rewardSuccess, ) = payable(validator).call{value: rewardPerValidator}("");
                if (!rewardSuccess) {
                    // Do not revert — log and continue to avoid DoS on reward distribution
                    // Unspent reward will be returned to requester below
                    totalRewarded -= rewardPerValidator;
                    emit ValidatorSlashed(validator, 0, "Reward transfer failed - skipped", block.timestamp);
                }
            } else {
                // Slash validators who voted against majority
                uint256 slashAmount = validatorStakes[validator] >= VALIDATION_REWARD
                    ? VALIDATION_REWARD
                    : validatorStakes[validator];
                validatorStakes[validator] -= slashAmount;
                /// @custom:audit-v3 MEDIUM-VR-02: Safe decrement guard
                if (validatorActiveValidations[validator] > 0) {
                    validatorActiveValidations[validator] -= 1;
                }
                emit ValidatorSlashed(
                    validator,
                    slashAmount,
                    "Voted against majority",
                    block.timestamp
                );
            }
        }

        /// @custom:audit-v3 CRITICAL-VR-01: Return unspent escrow to requester
        uint256 unspent = escrow - totalRewarded;
        if (unspent > 0) {
            (bool returnSuccess, ) = payable(request.requester).call{value: unspent}("");
            if (!returnSuccess) {
                // Log failure but do not revert to avoid DoS
                emit ValidatorSlashed(address(0), unspent, "Unspent escrow return failed", block.timestamp);
            }
        }
    }

    /**
     * @notice Stake to become a validator
     */
    function stakeAsValidator() external payable nonReentrant {
        require(msg.value >= MIN_VALIDATOR_STAKE, "Insufficient stake");

        validatorStakes[msg.sender] += msg.value;

        emit ValidatorStaked(
            msg.sender,
            msg.value,
            validatorStakes[msg.sender],
            block.timestamp
        );
    }

    /**
     * @notice Withdraw validator stake
     * @param amount Amount to withdraw
     */
    /// @custom:audit-fix HIGH-001 — CEI pattern + .call{} replaces .transfer()
    function withdrawValidatorStake(uint256 amount) external nonReentrant {
        require(amount > 0, "Amount must be greater than zero");
        require(validatorStakes[msg.sender] >= amount, "Insufficient stake");
        require(validatorActiveValidations[msg.sender] == 0, "Active validations pending");

        // CEI: update state BEFORE external call
        validatorStakes[msg.sender] -= amount;
        (bool success, ) = payable(msg.sender).call{value: amount}("");
        require(success, "Stake withdrawal failed");
        emit ValidatorStakeWithdrawn(
            msg.sender,
            amount,
            validatorStakes[msg.sender],
            block.timestamp
        );
    }

    /**
     * @notice Get validation request details
     * @param requestId Request ID
     * @return agentAddress Agent making the claim
     * @return status Current validation status
     * @return validationType Type of validation
     * @return approvalsCount Number of approvals
     * @return rejectionsCount Number of rejections
     * @return deadline Request deadline timestamp
     */
    function getValidationRequest(uint256 requestId)
        external
        view
        returns (
            address agentAddress,
            ValidationStatus status,
            ValidationType validationType,
            uint256 approvalsCount,
            uint256 rejectionsCount,
            uint256 deadline
        )
    {
        ValidationRequest memory request = validationRequests[requestId];
        return (
            request.agentAddress,
            request.status,
            request.validationType,
            request.approvalsCount,
            request.rejectionsCount,
            request.deadline
        );
    }

    /**
     * @notice Get all responses for a validation request (unbounded — kept for backward compatibility)
     * @param requestId Request ID
     * @return Array of response IDs
     */
    function getValidationResponses(uint256 requestId)
        external
        view
        returns (uint256[] memory)
    {
        return requestResponses[requestId];
    }

    // =========================================================================
    //  Paginated query functions to prevent gas-griefing DoS
    // =========================================================================

    /**
     * @notice Get the total number of responses for a validation request.
     * @param requestId Request ID
     * @return count Number of responses
     */
    function getValidationResponsesCount(uint256 requestId)
        external
        view
        returns (uint256 count)
    {
        return requestResponses[requestId].length;
    }

    /**
     * @notice Get a paginated slice of responses for a validation request.
     * @dev Prevents gas-griefing DoS by bounding the iteration to `limit` items.
     * @param requestId Request ID
     * @param offset Starting index (0-based)
     * @param limit Maximum number of items to return
     * @return page Array of response IDs in the requested range
     */
    function getValidationResponsesPaginated(
        uint256 requestId,
        uint256 offset,
        uint256 limit
    ) external view returns (uint256[] memory page) {
        uint256[] storage ids = requestResponses[requestId];
        uint256 len = ids.length;
        if (offset >= len || limit == 0) return new uint256[](0);

        uint256 end = offset + limit;
        if (end > len) end = len;

        page = new uint256[](end - offset);
        for (uint256 i = offset; i < end; i++) {
            page[i - offset] = ids[i];
        }
    }

    /**
     * @notice Get the total number of validation requests for an agent.
     * @param agentAddress Agent address
     * @return count Number of validation requests
     */
    function getAgentValidationRequestsCount(address agentAddress)
        external
        view
        returns (uint256 count)
    {
        return agentValidationRequests[agentAddress].length;
    }

    /**
     * @notice Get a paginated slice of validation request IDs for an agent.
     * @param agentAddress Agent address
     * @param offset Starting index (0-based)
     * @param limit Maximum number of items to return
     * @return page Array of request IDs in the requested range
     */
    function getAgentValidationRequestsPaginated(
        address agentAddress,
        uint256 offset,
        uint256 limit
    ) external view returns (uint256[] memory page) {
        require(agentAddress != address(0), "Invalid agent address");
        uint256[] storage ids = agentValidationRequests[agentAddress];
        uint256 len = ids.length;
        if (offset >= len || limit == 0) return new uint256[](0);

        uint256 end = offset + limit;
        if (end > len) end = len;

        page = new uint256[](end - offset);
        for (uint256 i = offset; i < end; i++) {
            page[i - offset] = ids[i];
        }
    }
}
