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

import "../contracts/PrivacyGuarantees.sol";
import "./ERC8004IdentityRegistryV3.sol";

/**
 * @title ERC-8004 Reputation Registry V3 for VaultFire
 * @notice Decentralized feedback and rating system for AI agents
 * @dev V3 — Applies audit fixes: split submitFeedback, rate limiting, Bayesian prior,
 *      string caps, allow zero feedbackURIHash, owner role.
 *
 * **Mission Alignment:**
 * - Privacy over surveillance: No personal data collection
 * - Morals over metrics: Quality partnerships, not just volume
 * - Human verification: Humans have final say on partnership quality
 * - Freedom over control: Reputation is portable across platforms
 *
 * **V3 Changes (2026-02 Partner-Ready Audit):**
 * - HIGH-RR-01: Split submitFeedback into public (unverified) and onlyAuthorized versions
 * - HIGH-RR-02: Per-(reviewer, agent) rate limiting — 1 hour minimum cooldown
 * - MEDIUM-RR-01: Bayesian prior — seed averageRating=5000 on first feedback
 * - LOW-RR-01: Cap category (64 bytes) and feedbackURI (512 bytes) string lengths
 * - LOW-RR-03: Allow zero feedbackURIHash in submitFeedbackHashed
 * - Added: owner role with transferOwnership, setAuthorizedCaller
 *
 * @custom:security Inherits PrivacyGuarantees
 * @custom:ethics Human-verified feedback only, no surveillance
 */
contract ERC8004ReputationRegistryV3 is PrivacyGuarantees {

    // --------------------
    // Privacy hardening
    // --------------------
    // Legacy feedback uses on-chain strings (category, feedbackURI). These are
    // easy to misuse (PII risk) on immutable ledgers.
    //
    // Prefer submitFeedbackHashed() which stores only keccak256 hashes.

    ERC8004IdentityRegistryV3 public immutable identityRegistry;

    // --------------------
    // Ownership
    // --------------------

    address public owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        _;
    }

    /// @custom:audit-v3 HIGH-RR-01: Authorized caller registry for verified feedback
    mapping(address => bool) public authorizedCallers;

    event AuthorizedCallerSet(address indexed caller, bool authorized);

    modifier onlyAuthorized() {
        require(authorizedCallers[msg.sender], "Not authorized");
        _;
    }

    /// @custom:audit-v3 HIGH-RR-02: Rate limiting — last feedback time per (reviewer, agent)
    mapping(address => mapping(address => uint256)) public lastFeedbackTime;

    uint256 public constant FEEDBACK_COOLDOWN = 1 hours;

    struct Feedback {
        address reviewer;          // Human or AI providing feedback
        address agentAddress;      // Agent being reviewed
        uint256 timestamp;
        uint256 rating;            // 0-10000 (basis points)
        string category;           // e.g., "partnership_quality", "technical_skill", "ethics"
        string feedbackURI;        // Off-chain detailed feedback (optional)
        bool verified;             // True if from verified VaultFire partnership
        uint256 bondId;            // VaultFire bond ID (0 if not from bond)
    }

    struct FeedbackHashed {
        address reviewer;
        address agentAddress;
        uint256 timestamp;
        uint256 rating;            // 0-10000
        bytes32 categoryHash;      // keccak256(category)
        bytes32 feedbackURIHash;   // keccak256(feedbackURI), may be bytes32(0)
        bool verified;
        uint256 bondId;
    }

    struct AgentReputation {
        uint256 totalFeedbacks;
        uint256 averageRating;     // 0-10000 (basis points)
        uint256 verifiedFeedbacks; // Count of verified partnership feedbacks
        uint256 lastUpdated;
    }

    // Agent address => reputation summary
    mapping(address => AgentReputation) public reputations;

    // Global feedback ID => Feedback
    mapping(uint256 => Feedback) public feedbacks;
    // Global hashed feedback ID => FeedbackHashed
    mapping(uint256 => FeedbackHashed) public feedbacksHashed;

    uint256 public nextFeedbackId = 1;
    uint256 public nextFeedbackIdHashed = 1;

    // Agent address => feedback IDs
    mapping(address => uint256[]) public agentFeedbacks;
    mapping(address => uint256[]) public agentFeedbacksHashed;

    // Reviewer address => feedback IDs (for transparency)
    mapping(address => uint256[]) public reviewerFeedbacks;
    mapping(address => uint256[]) public reviewerFeedbacksHashed;

    event FeedbackSubmitted(
        uint256 indexed feedbackId,
        address indexed reviewer,
        address indexed agentAddress,
        uint256 rating,
        string category,
        bool verified,
        uint256 bondId,
        uint256 timestamp
    );

    event FeedbackSubmittedHashed(
        uint256 indexed feedbackId,
        address indexed reviewer,
        address indexed agentAddress,
        uint256 rating,
        bytes32 categoryHash,
        bytes32 feedbackURIHash,
        bool verified,
        uint256 bondId,
        uint256 timestamp
    );

    event ReputationUpdated(
        address indexed agentAddress,
        uint256 averageRating,
        uint256 totalFeedbacks,
        uint256 timestamp
    );

    constructor(address _identityRegistry) {
        require(_identityRegistry != address(0), "Invalid identity registry");
        identityRegistry = ERC8004IdentityRegistryV3(_identityRegistry);
        owner = msg.sender;
    }

    // =========================================================================
    //  Owner management
    // =========================================================================

    function transferOwnership(address newOwner) external onlyOwner {
        require(newOwner != address(0), "Invalid new owner");
        emit OwnershipTransferred(owner, newOwner);
        owner = newOwner;
    }

    /// @custom:audit-v3 HIGH-RR-01: Owner manages authorized callers
    function setAuthorizedCaller(address caller, bool authorized) external onlyOwner {
        require(caller != address(0), "Invalid caller address");
        authorizedCallers[caller] = authorized;
        emit AuthorizedCallerSet(caller, authorized);
    }

    // =========================================================================
    //  Feedback submission
    // =========================================================================

    /**
     * @notice Submit unverified public feedback for an AI agent
     * @param agentAddress Address of agent being reviewed
     * @param rating Rating score (0-10000, where 10000 = perfect)
     * @param category Feedback category (e.g., "partnership_quality")
     * @param feedbackURI Optional URI to detailed off-chain feedback
     *
     * @dev HIGH-RR-01: Always sets verified=false, bondId=0.
     *      Cannot be used to claim a verified partnership or bond relationship.
     */
    /// @custom:audit-v3 HIGH-RR-01: Public path — always unverified, bondId=0
    /// @custom:audit-v3 HIGH-RR-02: Rate limiting enforced
    /// @custom:audit-v3 LOW-RR-01: String length caps enforced
    function submitFeedback(
        address agentAddress,
        uint256 rating,
        string calldata category,
        string calldata feedbackURI
    ) external {
        require(identityRegistry.isAgentActive(agentAddress), "Agent not registered");
        require(rating <= 10000, "Rating must be 0-10000");
        require(bytes(category).length > 0, "Category required");

        /// @custom:audit-v3 LOW-RR-01: Cap string lengths
        require(bytes(category).length <= 64, "Category too long (max 64 bytes)");
        require(bytes(feedbackURI).length <= 512, "FeedbackURI too long (max 512 bytes)");

        /// @custom:audit-v3 HIGH-RR-02: Rate limiting — minimum 1-hour cooldown per (reviewer, agent)
        require(
            block.timestamp >= lastFeedbackTime[msg.sender][agentAddress] + FEEDBACK_COOLDOWN,
            "Rate limit: wait 1 hour between feedbacks"
        );
        lastFeedbackTime[msg.sender][agentAddress] = block.timestamp;

        uint256 feedbackId = nextFeedbackId++;

        feedbacks[feedbackId] = Feedback({
            reviewer: msg.sender,
            agentAddress: agentAddress,
            timestamp: block.timestamp,
            rating: rating,
            category: category,
            feedbackURI: feedbackURI,
            verified: false,   // always false for public path
            bondId: 0          // always 0 for public path
        });

        agentFeedbacks[agentAddress].push(feedbackId);
        reviewerFeedbacks[msg.sender].push(feedbackId);

        _updateReputation(agentAddress, rating, false);

        emit FeedbackSubmitted(
            feedbackId,
            msg.sender,
            agentAddress,
            rating,
            category,
            false,
            0,
            block.timestamp
        );
    }

    /**
     * @notice Submit verified feedback for an AI agent (restricted to authorized callers)
     * @param agentAddress Address of agent being reviewed
     * @param rating Rating score (0-10000, where 10000 = perfect)
     * @param category Feedback category (e.g., "partnership_quality")
     * @param feedbackURI Optional URI to detailed off-chain feedback
     * @param bondId VaultFire bond ID (0 if not from bond)
     *
     * @dev HIGH-RR-01: Only authorized callers (e.g., VaultfireERC8004AdapterV3) may
     *      set verified=true. This prevents arbitrary callers from inflating verified counts.
     */
    /// @custom:audit-v3 HIGH-RR-01: Authorized path — sets verified=true, accepts bondId
    /// @custom:audit-v3 HIGH-RR-02: Rate limiting enforced
    /// @custom:audit-v3 LOW-RR-01: String length caps enforced
    function submitVerifiedFeedback(
        address agentAddress,
        uint256 rating,
        string calldata category,
        string calldata feedbackURI,
        uint256 bondId
    ) external onlyAuthorized {
        require(identityRegistry.isAgentActive(agentAddress), "Agent not registered");
        require(rating <= 10000, "Rating must be 0-10000");
        require(bytes(category).length > 0, "Category required");

        /// @custom:audit-v3 LOW-RR-01: Cap string lengths
        require(bytes(category).length <= 64, "Category too long (max 64 bytes)");
        require(bytes(feedbackURI).length <= 512, "FeedbackURI too long (max 512 bytes)");

        /// @custom:audit-v3 HIGH-RR-02: Rate limiting per (caller, agent)
        require(
            block.timestamp >= lastFeedbackTime[msg.sender][agentAddress] + FEEDBACK_COOLDOWN,
            "Rate limit: wait 1 hour between feedbacks"
        );
        lastFeedbackTime[msg.sender][agentAddress] = block.timestamp;

        uint256 feedbackId = nextFeedbackId++;

        feedbacks[feedbackId] = Feedback({
            reviewer: msg.sender,
            agentAddress: agentAddress,
            timestamp: block.timestamp,
            rating: rating,
            category: category,
            feedbackURI: feedbackURI,
            verified: true,
            bondId: bondId
        });

        agentFeedbacks[agentAddress].push(feedbackId);
        reviewerFeedbacks[msg.sender].push(feedbackId);

        _updateReputation(agentAddress, rating, true);

        emit FeedbackSubmitted(
            feedbackId,
            msg.sender,
            agentAddress,
            rating,
            category,
            true,
            bondId,
            block.timestamp
        );
    }

    /**
     * @notice Submit feedback without putting freeform strings on-chain.
     *
     * @dev LOW-RR-03: feedbackURIHash may be bytes32(0) (no URI required).
     */
    /// @custom:audit-v3 LOW-RR-03: feedbackURIHash may be zero (no URI required)
    /// @custom:audit-v3 HIGH-RR-02: Rate limiting enforced
    function submitFeedbackHashed(
        address agentAddress,
        uint256 rating,
        bytes32 categoryHash,
        bytes32 feedbackURIHash
    ) external {
        require(identityRegistry.isAgentActive(agentAddress), "Agent not registered");
        require(rating <= 10000, "Rating must be 0-10000");
        require(categoryHash != bytes32(0), "Category hash required");
        // feedbackURIHash may be zero — LOW-RR-03 fix: no longer required

        /// @custom:audit-v3 HIGH-RR-02: Rate limiting
        require(
            block.timestamp >= lastFeedbackTime[msg.sender][agentAddress] + FEEDBACK_COOLDOWN,
            "Rate limit: wait 1 hour between feedbacks"
        );
        lastFeedbackTime[msg.sender][agentAddress] = block.timestamp;

        uint256 feedbackId = nextFeedbackIdHashed++;

        feedbacksHashed[feedbackId] = FeedbackHashed({
            reviewer: msg.sender,
            agentAddress: agentAddress,
            timestamp: block.timestamp,
            rating: rating,
            categoryHash: categoryHash,
            feedbackURIHash: feedbackURIHash,
            verified: false,
            bondId: 0
        });

        agentFeedbacksHashed[agentAddress].push(feedbackId);
        reviewerFeedbacksHashed[msg.sender].push(feedbackId);

        _updateReputation(agentAddress, rating, false);

        emit FeedbackSubmittedHashed(
            feedbackId,
            msg.sender,
            agentAddress,
            rating,
            categoryHash,
            feedbackURIHash,
            false,
            0,
            block.timestamp
        );
    }

    /**
     * @notice Submit hashed verified feedback (restricted to authorized callers)
     * @dev HIGH-RR-01: Authorized variant of submitFeedbackHashed, sets verified=true.
     */
    /// @custom:audit-v3 HIGH-RR-01: Authorized hashed path — sets verified=true
    /// @custom:audit-v3 HIGH-RR-02: Rate limiting enforced
    function submitVerifiedFeedbackHashed(
        address agentAddress,
        uint256 rating,
        bytes32 categoryHash,
        bytes32 feedbackURIHash,
        uint256 bondId
    ) external onlyAuthorized {
        require(identityRegistry.isAgentActive(agentAddress), "Agent not registered");
        require(rating <= 10000, "Rating must be 0-10000");
        require(categoryHash != bytes32(0), "Category hash required");

        /// @custom:audit-v3 HIGH-RR-02: Rate limiting
        require(
            block.timestamp >= lastFeedbackTime[msg.sender][agentAddress] + FEEDBACK_COOLDOWN,
            "Rate limit: wait 1 hour between feedbacks"
        );
        lastFeedbackTime[msg.sender][agentAddress] = block.timestamp;

        uint256 feedbackId = nextFeedbackIdHashed++;

        feedbacksHashed[feedbackId] = FeedbackHashed({
            reviewer: msg.sender,
            agentAddress: agentAddress,
            timestamp: block.timestamp,
            rating: rating,
            categoryHash: categoryHash,
            feedbackURIHash: feedbackURIHash,
            verified: true,
            bondId: bondId
        });

        agentFeedbacksHashed[agentAddress].push(feedbackId);
        reviewerFeedbacksHashed[msg.sender].push(feedbackId);

        _updateReputation(agentAddress, rating, true);

        emit FeedbackSubmittedHashed(
            feedbackId,
            msg.sender,
            agentAddress,
            rating,
            categoryHash,
            feedbackURIHash,
            true,
            bondId,
            block.timestamp
        );
    }

    // =========================================================================
    //  Internal reputation logic
    // =========================================================================

    /**
     * @notice Internal: Update agent's reputation score
     * @param agentAddress Agent whose reputation to update
     * @param newRating New rating to incorporate
     * @param verified Whether this is verified feedback
     */
    /// @custom:audit-fix MEDIUM-001 — Overflow-safe running average (2026-02-23)
    /// @custom:audit-v3 MEDIUM-RR-01: Bayesian prior — seed averageRating=5000 on first feedback
    uint256 private constant MAX_FEEDBACKS_FOR_TRUE_AVG = 1000;

    function _updateReputation(
        address agentAddress,
        uint256 newRating,
        bool verified
    ) internal {
        AgentReputation storage rep = reputations[agentAddress];

        /// @custom:audit-v3 MEDIUM-RR-01: Bayesian prior — neutral baseline for first feedback
        if (rep.totalFeedbacks == 0) {
            // Seed with neutral prior of 5000 before incorporating the real first rating
            // Equivalent to weighting the first real rating against a neutral prior.
            // averageRating = (5000 + newRating) / 2
            rep.averageRating = (5000 + newRating) / 2;
            rep.totalFeedbacks = 1;
        } else if (rep.totalFeedbacks < MAX_FEEDBACKS_FOR_TRUE_AVG) {
            // True running average: safe since totalFeedbacks < 1000 and rating <= 10000
            uint256 totalRating = rep.averageRating * rep.totalFeedbacks;
            totalRating += newRating;
            rep.totalFeedbacks += 1;
            rep.averageRating = totalRating / rep.totalFeedbacks;
        } else {
            // EMA: new_avg = old_avg * 90% + new_rating * 10% (no overflow risk)
            rep.averageRating = (rep.averageRating * 9 + newRating) / 10;
            rep.totalFeedbacks += 1;
        }

        if (verified) {
            rep.verifiedFeedbacks += 1;
        }

        rep.lastUpdated = block.timestamp;

        emit ReputationUpdated(
            agentAddress,
            rep.averageRating,
            rep.totalFeedbacks,
            block.timestamp
        );
    }

    // =========================================================================
    //  View functions
    // =========================================================================

    /**
     * @notice Get agent's reputation summary
     * @param agentAddress Address of agent
     * @return averageRating Average rating (0-10000)
     * @return totalFeedbacks Total number of feedbacks
     * @return verifiedFeedbacks Number of verified partnership feedbacks
     * @return lastUpdated Last update timestamp
     */
    function getReputation(address agentAddress)
        external
        view
        returns (
            uint256 averageRating,
            uint256 totalFeedbacks,
            uint256 verifiedFeedbacks,
            uint256 lastUpdated
        )
    {
        AgentReputation memory rep = reputations[agentAddress];
        return (
            rep.averageRating,
            rep.totalFeedbacks,
            rep.verifiedFeedbacks,
            rep.lastUpdated
        );
    }

    /**
     * @notice Get all feedback IDs for an agent
     * @param agentAddress Address of agent
     * @return Array of feedback IDs
     */
    function getAgentFeedbacks(address agentAddress)
        external
        view
        returns (uint256[] memory)
    {
        return agentFeedbacks[agentAddress];
    }

    /**
     * @notice Get feedback details by ID
     * @param feedbackId Feedback ID
     * @return reviewer Address of reviewer
     * @return agentAddress Agent being reviewed
     * @return rating Rating score
     * @return category Feedback category
     * @return verified Whether verified from partnership
     * @return timestamp When feedback was submitted
     */
    function getFeedback(uint256 feedbackId)
        external
        view
        returns (
            address reviewer,
            address agentAddress,
            uint256 rating,
            string memory category,
            bool verified,
            uint256 timestamp
        )
    {
        Feedback memory f = feedbacks[feedbackId];
        return (
            f.reviewer,
            f.agentAddress,
            f.rating,
            f.category,
            f.verified,
            f.timestamp
        );
    }

    /**
     * @notice Get verified feedback percentage for an agent
     * @param agentAddress Address of agent
     * @return percentage Percentage of verified feedbacks (0-10000)
     */
    function getVerifiedFeedbackPercentage(address agentAddress)
        external
        view
        returns (uint256 percentage)
    {
        AgentReputation memory rep = reputations[agentAddress];
        if (rep.totalFeedbacks == 0) return 0;
        return (rep.verifiedFeedbacks * 10000) / rep.totalFeedbacks;
    }

    /**
     * @notice Get all feedbacks submitted by a reviewer
     * @param reviewer Address of reviewer
     * @return Array of feedback IDs
     */
    function getReviewerFeedbacks(address reviewer)
        external
        view
        returns (uint256[] memory)
    {
        return reviewerFeedbacks[reviewer];
    }
}
