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

interface IAereAgentBond {
    function bondOf(address operator, bytes32 agentId) external view returns (
        uint256 amount, uint64 requestedAt, uint256 lifetimeSlashed, bool exists
    );
    function operatorLifetimeSlashed(address operator) external view returns (uint256);
}

/**
 * @title AereAIReputation — composable on-chain reputation for agents
 * @notice Reputation = the sum of signed observations from registered
 *         attestors. An attestor is any party the protocol delegates the
 *         right to score from: the agent's customers (post AERE402 settle),
 *         a third-party model auditor (Cantina/Anthropic), an EAS schema
 *         operator, etc.
 *
 *         The score is a uint256 — convention: 0 (no history) to 10_000
 *         (perfect, no slashes, many positive attestations). Apps decide
 *         their own cutoffs (e.g. AERE402 require >= 3000 before routing).
 *
 *         Score formula (kept simple for Phase 1):
 *
 *             score = positiveCount * 10
 *                   - 10 * lifetimeSlashed / SLASH_UNIT
 *                   - 20 * disputeCount
 *
 *         clamped to [0, 10_000]. Apps that need a more sophisticated
 *         curve (decay, recency-weighting, attestor weighting) read the
 *         raw signal counts and compute their own.
 *
 *         No PII. The agentId is a hash; the attestor identifier is a
 *         registered address; observations include only a hash of the
 *         off-chain evidence + an optional public URI.
 *
 *         IMMUTABILITY:
 *           - BOND immutable at deploy (the slashable-stake contract whose
 *             lifetimeSlashed feeds the formula).
 *           - SLASH_UNIT constant.
 *           - Registered attestors set at deploy; no admin add post-deploy.
 *             Phase 2 will use a DAO-voted attestor list; Phase 1 starts
 *             with Foundation + a few hand-picked auditors.
 */
contract AereAIReputation {

    /* ================================ config ================================ */

    IAereAgentBond public immutable BOND;
    uint256 public constant MAX_SCORE = 10_000;
    uint256 public constant SLASH_UNIT = 1e18; // 1 AERE slashed ⇒ -10 score points

    mapping(address => bool) public isAttestor;
    address[] public attestorList;

    /* ================================= state ================================= */

    struct AgentStats {
        uint128 positiveCount;
        uint128 disputeCount;
    }

    /// @notice (operator, agentId) → counters fed by attestors.
    mapping(address => mapping(bytes32 => AgentStats)) internal _stats;

    /// AUDIT FIX (HIGH #20): dedup attestations by evidenceHash per attestor
    /// to prevent unbounded replay.
    mapping(address => mapping(bytes32 => bool)) public usedEvidence;

    /* ================================= events ================================= */

    event Attested(
        address indexed operator,
        bytes32 indexed agentId,
        address indexed attestor,
        int8    delta,        // +1 / -1 / 0
        bytes32 evidenceHash,
        string  evidenceUri
    );

    /* ================================= errors ================================= */

    error NotAttestor();
    error InvalidDelta();
    error EmptyEvidenceHash();
    error EvidenceReplay();
    error SelfAttest();

    /* =============================== modifiers ============================== */

    modifier onlyAttestor() {
        if (!isAttestor[msg.sender]) revert NotAttestor();
        _;
    }

    /* ============================== constructor ============================= */

    constructor(IAereAgentBond bond, address[] memory attestors) {
        require(address(bond) != address(0), "zero-bond");
        BOND = bond;
        for (uint256 i = 0; i < attestors.length; i++) {
            require(attestors[i] != address(0), "zero-attestor");
            require(!isAttestor[attestors[i]], "dup-attestor");
            isAttestor[attestors[i]] = true;
            attestorList.push(attestors[i]);
        }
    }

    /* ================================= write ================================= */

    /// @notice Attestor records an observation. delta ∈ {-1, 0, +1}:
    ///           +1  positive interaction (settlement succeeded as agent
    ///               described; model output matched committed attestation).
    ///            0  recorded-but-neutral (used for audit-trail without
    ///               score change).
    ///           -1  formal dispute (counted as malus; SEPARATE from a
    ///               slash, which is the bond contract's territory).
    function attest(
        address operator,
        bytes32 agentId,
        int8 delta,
        bytes32 evidenceHash,
        string calldata evidenceUri
    ) external onlyAttestor {
        if (delta < -1 || delta > 1) revert InvalidDelta();
        if (evidenceHash == bytes32(0)) revert EmptyEvidenceHash();
        // AUDIT FIX (HIGH #22): block self-attest (attestor cannot rate
        // themselves as operator).
        if (msg.sender == operator) revert SelfAttest();
        // AUDIT FIX (HIGH #20): dedup by (attestor, evidenceHash) so an
        // attestor cannot replay the same evidence 1000× for free score.
        if (usedEvidence[msg.sender][evidenceHash]) revert EvidenceReplay();
        usedEvidence[msg.sender][evidenceHash] = true;
        AgentStats storage s = _stats[operator][agentId];
        if (delta == 1)        s.positiveCount += 1;
        else if (delta == -1)  s.disputeCount  += 1;
        emit Attested(operator, agentId, msg.sender, delta, evidenceHash, evidenceUri);
    }

    /* ================================= views ================================= */

    function statsOf(address operator, bytes32 agentId) external view returns (
        uint256 positiveCount, uint256 disputeCount, uint256 lifetimeSlashed
    ) {
        AgentStats storage s = _stats[operator][agentId];
        ( , , uint256 slashed, ) = BOND.bondOf(operator, agentId);
        return (s.positiveCount, s.disputeCount, slashed);
    }

    /// @notice Score formula: positiveCount * 10 - 10 * slashed/SLASH_UNIT
    ///         - 20 * disputeCount. Clamped to [0, 10_000].
    function scoreOf(address operator, bytes32 agentId) external view returns (uint256) {
        AgentStats storage s = _stats[operator][agentId];
        ( , , uint256 slashed, ) = BOND.bondOf(operator, agentId);
        // AUDIT FIX (HIGH #21): roll in operator-level lifetime slash so a
        // fresh agentId rotation cannot wipe the slash penalty. Apply at
        // half-weight against the per-agent score so a single bad agent
        // doesn't permanently destroy a separate clean agent's score —
        // but the penalty doesn't disappear.
        uint256 opSlashed = BOND.operatorLifetimeSlashed(operator);
        uint256 effectiveSlashed = slashed + (opSlashed > slashed ? (opSlashed - slashed) / 2 : 0);

        int256 raw = int256(uint256(s.positiveCount)) * 10
                   - int256(effectiveSlashed / SLASH_UNIT) * 10
                   - int256(uint256(s.disputeCount)) * 20;

        if (raw <= 0) return 0;
        uint256 r = uint256(raw);
        return r > MAX_SCORE ? MAX_SCORE : r;
    }

    function attestors() external view returns (address[] memory) {
        return attestorList;
    }
}
