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

/**
 * @title AereAgentActionReceipt — PQC-anchored, publicly verifiable AI provenance
 *        (AERE chain 2800)
 *
 * @notice An on-chain provenance record for an autonomous/AI agent's output. An
 *         agent commits the HASH of an output (or action) it produced, signed by a
 *         short-lived secp256k1 SESSION key that was itself issued under a
 *         quantum-durable Falcon ROOT via AereAgentDID (proof-of-possession verified
 *         on-chain by AERE's live native PQC precompile). The result is a permanent,
 *         append-only, publicly re-verifiable statement: "agent A produced an output
 *         whose hash is H, authorized by session S under Falcon root A."
 *
 *         WHY THIS EXISTS. A wallet claiming an AI output is a claim, not a property.
 *         Off-chain "the model said X" is unverifiable and forgeable. A receipt here
 *         binds the output hash to a PQC-rooted identity with a signature ANYONE can
 *         re-check in a single read, and cross-wires it to the agent's live economic
 *         accountability: its AereAgentBond stake and its AereAIReputation score /
 *         slashing history. A verifier learns, from one contract, both "is this
 *         provenance genuine?" and "how much is this agent economically on the hook,
 *         and has it been slashed?".
 *
 *         QUANTUM DURABILITY. The receipt's authorship chain terminates at a Falcon
 *         root key. The hot session signature is classical secp256k1 (cheap ecrecover),
 *         but a session only exists because a Falcon PoP authorized it in AereAgentDID,
 *         and a session (hence any new receipt under it) stops validating the instant
 *         its Falcon root is rotated or revoked. The commitment hash itself is content
 *         you choose (e.g. a SHA3/Keccak digest of the output), durable by construction.
 *
 *         HONEST SCOPE. Application-layer provenance + authorization. This does NOT
 *         change AERE consensus (blocks are still Besu QBFT, classical ECDSA). Falcon
 *         verification lives in AereAgentDID / AerePQCKeyRegistry via the live native
 *         precompile, not here. This contract holds no funds and never mutates the DID,
 *         bond, or reputation contracts — it only reads them. Emitting is permissionless
 *         (any relayer may submit): the SESSION SIGNATURE is the sole authority, so the
 *         relayer paying gas is irrelevant to authorship.
 *
 *         CROSS-WIRE CONVENTION. AereAgentDID keys an agent by its Falcon rootKeyId
 *         (a uint256). AereAgentBond / AereAIReputation key by (operator, bytes32
 *         agentId). This contract reads the economic layer at the canonical key
 *         (operator = the DID controller of the root key, agentId = bytes32(rootKeyId)).
 *         An operator that wants its DID-rooted agent's stake / reputation surfaced
 *         alongside its receipts posts its bond under agentId == bytes32(rootKeyId)
 *         (see {bondKeyOf}). If it does not, the cross-reads simply return zero — the
 *         provenance signature is unaffected and remains fully verifiable.
 */

/// @dev Read-only surface of AereAgentDID used by this contract.
interface IAereAgentDID {
    function getSession(uint256 sessionId)
        external
        view
        returns (
            uint256 rootKeyId,
            address sessionAddr,
            bytes32 scopeHash,
            uint256 spendCap,
            uint256 spent,
            uint64 issuedBlock,
            uint64 expiry,
            uint64 actionNonce,
            bool revoked
        );

    function isSessionValid(uint256 sessionId) external view returns (bool);

    function getAgent(uint256 rootKeyId)
        external
        view
        returns (address controller, uint64 createdBlock, uint64 sessionNonce, uint256 sessionCountForAgent);

    function agentExists(uint256 rootKeyId) external view returns (bool);
}

/// @dev Read-only surface of AereAgentBond used for economic cross-reads.
interface IAereAgentBondView {
    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);
}

/// @dev Read-only surface of AereAIReputation used for reputation cross-reads.
interface IAereAIReputationView {
    function scoreOf(address operator, bytes32 agentId) external view returns (uint256);

    function statsOf(address operator, bytes32 agentId)
        external
        view
        returns (uint256 positiveCount, uint256 disputeCount, uint256 lifetimeSlashed);
}

contract AereAgentActionReceipt {
    // ----- domain separator (distinct from AereAgentDID's ACTION_DOMAIN so a DID
    //       action signature can never be replayed as a provenance receipt) -----
    bytes32 public constant RECEIPT_DOMAIN = keccak256("AereAgentActionReceipt.v1.receipt");

    // ----- immutable wiring -----
    IAereAgentDID public immutable did; // Falcon-rooted DID: session verification source of truth
    address public immutable agentBond; // AereAgentBond (slashable stake) or 0
    address public immutable aiReputation; // AereAIReputation (composable score) or 0

    // ----- provenance records (global incrementing id; append-only) -----
    struct Receipt {
        uint256 agentId; // the Falcon rootKeyId (AereAgentDID agent) that authored this
        uint256 sessionId; // the DID session that signed it
        address signer; // the session secp256k1 address recovered at emit time
        bytes32 outputHash; // the committed hash of the agent's output/action
        bytes32 scope; // the session scope tag this output was produced under
        uint64 nonce; // per-session receipt nonce used in the signed digest
        uint64 emittedBlock;
        uint64 emittedAt; // unix timestamp
        bytes sig; // the 65-byte session signature (stored so anyone can re-verify)
    }

    Receipt[] private _receipts; // receiptId is the index (0-based)
    mapping(uint256 => uint256[]) private _agentReceipts; // rootKeyId => receiptIds
    mapping(uint256 => uint256[]) private _sessionReceipts; // sessionId => receiptIds
    mapping(uint256 => uint64) private _receiptNonce; // sessionId => next receipt nonce

    // ----- events -----
    event ReceiptEmitted(
        uint256 indexed receiptId,
        uint256 indexed agentId,
        uint256 indexed sessionId,
        address signer,
        bytes32 outputHash,
        bytes32 scope,
        uint64 nonce
    );

    // ----- errors -----
    error ZeroDID();
    error EmptyOutputHash();
    error RevokedOrInvalidSession(uint256 sessionId);
    error ScopeMismatch(uint256 sessionId);
    error BadSessionSignature(uint256 sessionId);
    error UnknownReceipt(uint256 receiptId);

    constructor(address did_, address agentBond_, address aiReputation_) {
        if (did_ == address(0)) revert ZeroDID();
        did = IAereAgentDID(did_);
        agentBond = agentBond_;
        aiReputation = aiReputation_;
    }

    // ==========================================================================
    //                                Emit
    // ==========================================================================

    /**
     * @notice Record a provenance receipt for an agent output. Permissionless relay:
     *         the SESSION SIGNATURE is the authority, not msg.sender.
     *
     *         Reverts (recording nothing) if the output hash is zero, the session is
     *         not currently valid (revoked / expired / its Falcon root no longer ACTIVE
     *         / unknown session), the scope does not match the session's scope, or the
     *         signature is not by the session key over the exact commitment (which binds
     *         the output hash — a tampered output hash recovers a different address and
     *         is rejected).
     *
     * @param sessionId  the AereAgentDID session that signs this output.
     * @param scope      the scope tag for this output (must equal the session's scopeHash).
     * @param outputHash a 32-byte commitment to the agent's output/action (non-zero).
     * @param sig        65-byte secp256k1 signature (r||s||v) by the session address over
     *                   receiptDigest(agentId, sessionId, outputHash, scope, nonce).
     * @return receiptId the id of the newly recorded receipt.
     */
    function emitReceipt(uint256 sessionId, bytes32 scope, bytes32 outputHash, bytes calldata sig)
        external
        returns (uint256 receiptId)
    {
        if (outputHash == bytes32(0)) revert EmptyOutputHash();

        // A live session gates everything: revoked / expired / root-not-active / unknown
        // all collapse to "invalid" here (isSessionValid returns false, never reverts).
        if (!did.isSessionValid(sessionId)) revert RevokedOrInvalidSession(sessionId);

        (uint256 rootKeyId, address sessionAddr, bytes32 scopeHash,,,,,,) = did.getSession(sessionId);
        if (scope != scopeHash) revert ScopeMismatch(sessionId);

        uint64 nonce = _receiptNonce[sessionId];
        bytes32 digest = _receiptDigest(rootKeyId, sessionId, outputHash, scope, nonce);
        address recovered = _recover(digest, sig);
        // A tampered outputHash (or wrong signer) recovers a different / null address.
        if (recovered == address(0) || recovered != sessionAddr) revert BadSessionSignature(sessionId);

        // Effects.
        _receiptNonce[sessionId] = nonce + 1;
        receiptId = _receipts.length;
        _receipts.push(
            Receipt({
                agentId: rootKeyId,
                sessionId: sessionId,
                signer: sessionAddr,
                outputHash: outputHash,
                scope: scope,
                nonce: nonce,
                emittedBlock: uint64(block.number),
                emittedAt: uint64(block.timestamp),
                sig: sig
            })
        );
        _agentReceipts[rootKeyId].push(receiptId);
        _sessionReceipts[sessionId].push(receiptId);

        emit ReceiptEmitted(receiptId, rootKeyId, sessionId, sessionAddr, outputHash, scope, nonce);
    }

    // ==========================================================================
    //                              Verification
    // ==========================================================================

    /**
     * @notice Independently re-verify a stored receipt in one read. Re-derives the
     *         signed digest from the stored fields, recovers the signer from the stored
     *         signature, and reports whether it matches the recorded session key, plus
     *         whether that session is STILL live now (a receipt stays genuine forever;
     *         `sessionLiveNow` tells a consumer whether the agent is currently active or
     *         has since been revoked / expired / rotated away).
     *
     * @return sigValid       the stored signature genuinely authorizes the stored output.
     * @return sessionLiveNow the authoring session is valid at the current block.
     * @return agentId        the Falcon rootKeyId that authored the receipt.
     * @return signer         the session address the signature recovers to.
     * @return outputHash     the committed output hash.
     */
    function verifyReceipt(uint256 receiptId)
        external
        view
        returns (bool sigValid, bool sessionLiveNow, uint256 agentId, address signer, bytes32 outputHash)
    {
        if (receiptId >= _receipts.length) revert UnknownReceipt(receiptId);
        Receipt storage r = _receipts[receiptId];
        bytes32 digest = _receiptDigest(r.agentId, r.sessionId, r.outputHash, r.scope, r.nonce);
        address recovered = _recover(digest, r.sig);
        sigValid = (recovered != address(0) && recovered == r.signer);
        sessionLiveNow = did.isSessionValid(r.sessionId);
        return (sigValid, sessionLiveNow, r.agentId, r.signer, r.outputHash);
    }

    /**
     * @notice Verify that a receipt attests EXACTLY `outputHash` and that its stored
     *         signature is genuine. This is the tamper check at read time: pass the hash
     *         of the output you hold; a byte-different output yields a different hash and
     *         returns false. Returns false (never reverts) for an out-of-range receiptId.
     */
    function verifyReceiptOutput(uint256 receiptId, bytes32 outputHash) external view returns (bool) {
        if (receiptId >= _receipts.length) return false;
        Receipt storage r = _receipts[receiptId];
        if (r.outputHash != outputHash) return false;
        bytes32 digest = _receiptDigest(r.agentId, r.sessionId, r.outputHash, r.scope, r.nonce);
        address recovered = _recover(digest, r.sig);
        return (recovered != address(0) && recovered == r.signer);
    }

    /**
     * @notice The full provenance picture in one call: the receipt's identity + validity
     *         AND the agent's live economic accountability, read from AereAgentBond and
     *         AereAIReputation at the canonical cross-wire key (operator = DID controller,
     *         agentId = bytes32(rootKeyId)). All economic reads are defensive: if a
     *         cross-link is unset or reverts, its fields come back zero and the
     *         provenance fields (sigValid / signer / outputHash) are still returned.
     *
     * @return agentId               the Falcon rootKeyId author.
     * @return operator              the DID controller (bond/reputation key operator).
     * @return signer                the authoring session key.
     * @return outputHash            the committed output hash.
     * @return scope                 the scope the output was produced under.
     * @return emittedAt             unix timestamp the receipt was recorded.
     * @return sigValid              the stored signature genuinely authorizes the output.
     * @return sessionLiveNow        the authoring session is valid now.
     * @return reputationScore       AereAIReputation.scoreOf(operator, bytes32(agentId)).
     * @return bondAmount            current AereAgentBond stake at the canonical key.
     * @return bondLifetimeSlashed   cumulative slash on that (operator, agentId) bond.
     * @return operatorTotalSlashed  operator-level cumulative slash across all its agents.
     */
    function provenanceOf(uint256 receiptId)
        external
        view
        returns (
            uint256 agentId,
            address operator,
            address signer,
            bytes32 outputHash,
            bytes32 scope,
            uint64 emittedAt,
            bool sigValid,
            bool sessionLiveNow,
            uint256 reputationScore,
            uint256 bondAmount,
            uint256 bondLifetimeSlashed,
            uint256 operatorTotalSlashed
        )
    {
        if (receiptId >= _receipts.length) revert UnknownReceipt(receiptId);
        Receipt storage r = _receipts[receiptId];

        agentId = r.agentId;
        signer = r.signer;
        outputHash = r.outputHash;
        scope = r.scope;
        emittedAt = r.emittedAt;

        bytes32 digest = _receiptDigest(r.agentId, r.sessionId, r.outputHash, r.scope, r.nonce);
        address recovered = _recover(digest, r.sig);
        sigValid = (recovered != address(0) && recovered == r.signer);
        sessionLiveNow = did.isSessionValid(r.sessionId);

        operator = _operatorOf(r.agentId);
        bytes32 bondKey = bytes32(r.agentId);

        (reputationScore) = _scoreOf(operator, bondKey);
        (bondAmount, bondLifetimeSlashed) = _bondOf(operator, bondKey);
        operatorTotalSlashed = _operatorLifetimeSlashed(operator);
    }

    // ==========================================================================
    //                            Digest / cross-wire views
    // ==========================================================================

    /// @notice The digest a session key must sign to record a receipt at `nonce`.
    function receiptDigest(uint256 agentId, uint256 sessionId, bytes32 outputHash, bytes32 scope, uint64 nonce)
        external
        view
        returns (bytes32)
    {
        return _receiptDigest(agentId, sessionId, outputHash, scope, nonce);
    }

    function _receiptDigest(uint256 agentId, uint256 sessionId, bytes32 outputHash, bytes32 scope, uint64 nonce)
        internal
        view
        returns (bytes32)
    {
        return keccak256(
            abi.encode(RECEIPT_DOMAIN, block.chainid, address(this), agentId, sessionId, outputHash, scope, nonce)
        );
    }

    /// @notice The next receipt nonce for a session (the value the next emit will sign).
    function receiptNonceOf(uint256 sessionId) external view returns (uint64) {
        return _receiptNonce[sessionId];
    }

    /// @notice The canonical AereAgentBond / AereAIReputation agentId for a DID rootKeyId.
    ///         Operators post their bond under this key to have stake surfaced by
    ///         {provenanceOf}. Pure — no state, just the encoding convention.
    function bondKeyOf(uint256 rootKeyId) external pure returns (bytes32) {
        return bytes32(rootKeyId);
    }

    /// @notice The operator (bond/reputation key) this contract reads for an agent:
    ///         the DID controller of the root key. Returns address(0) if unknown.
    function operatorOf(uint256 rootKeyId) external view returns (address) {
        return _operatorOf(rootKeyId);
    }

    // ==========================================================================
    //                                  Views
    // ==========================================================================

    /// @notice Total receipts ever recorded (receiptIds run 0..receiptCount-1).
    function receiptCount() external view returns (uint256) {
        return _receipts.length;
    }

    /// @notice Read a receipt. Reverts on an out-of-range receiptId.
    function getReceipt(uint256 receiptId)
        external
        view
        returns (
            uint256 agentId,
            uint256 sessionId,
            address signer,
            bytes32 outputHash,
            bytes32 scope,
            uint64 nonce,
            uint64 emittedBlock,
            uint64 emittedAt,
            bytes memory sig
        )
    {
        if (receiptId >= _receipts.length) revert UnknownReceipt(receiptId);
        Receipt storage r = _receipts[receiptId];
        return (r.agentId, r.sessionId, r.signer, r.outputHash, r.scope, r.nonce, r.emittedBlock, r.emittedAt, r.sig);
    }

    /// @notice All receiptIds authored by an agent (append-only, chronological).
    function receiptsOf(uint256 rootKeyId) external view returns (uint256[] memory) {
        return _agentReceipts[rootKeyId];
    }

    /// @notice All receiptIds recorded under a session (append-only, chronological).
    function receiptsOfSession(uint256 sessionId) external view returns (uint256[] memory) {
        return _sessionReceipts[sessionId];
    }

    // ==========================================================================
    //                        Defensive economic cross-reads
    // ==========================================================================

    function _operatorOf(uint256 rootKeyId) internal view returns (address) {
        try did.getAgent(rootKeyId) returns (address controller, uint64, uint64, uint256) {
            return controller;
        } catch {
            return address(0);
        }
    }

    function _scoreOf(address operator, bytes32 bondKey) internal view returns (uint256) {
        if (aiReputation == address(0) || operator == address(0)) return 0;
        try IAereAIReputationView(aiReputation).scoreOf(operator, bondKey) returns (uint256 s) {
            return s;
        } catch {
            return 0;
        }
    }

    function _bondOf(address operator, bytes32 bondKey) internal view returns (uint256 amount, uint256 slashed) {
        if (agentBond == address(0) || operator == address(0)) return (0, 0);
        try IAereAgentBondView(agentBond).bondOf(operator, bondKey) returns (
            uint256 a, uint64, uint256 sl, bool
        ) {
            return (a, sl);
        } catch {
            return (0, 0);
        }
    }

    function _operatorLifetimeSlashed(address operator) internal view returns (uint256) {
        if (agentBond == address(0) || operator == address(0)) return 0;
        try IAereAgentBondView(agentBond).operatorLifetimeSlashed(operator) returns (uint256 s) {
            return s;
        } catch {
            return 0;
        }
    }

    // ==========================================================================
    //                              ECDSA recovery
    // ==========================================================================

    /// @dev Minimal ecrecover with malleability guard (low-s, v in {27,28}). Returns
    ///      address(0) on any malformed input so callers treat it as an invalid signature.
    ///      Mirrors AereAgentDID._recover so a session key signs both surfaces identically.
    function _recover(bytes32 digest, bytes memory sig) internal pure returns (address) {
        if (sig.length != 65) return address(0);
        bytes32 r;
        bytes32 s;
        uint8 v;
        assembly {
            r := mload(add(sig, 0x20))
            s := mload(add(sig, 0x40))
            v := byte(0, mload(add(sig, 0x60)))
        }
        // Reject high-s (EIP-2) to prevent signature malleability.
        if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) {
            return address(0);
        }
        if (v != 27 && v != 28) return address(0);
        return ecrecover(digest, v, r, s);
    }
}
