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

import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

/**
 * @title AereAIProof — anchors signed (model, input, output) triples for
 *                       AI inference / generation provenance
 * @notice AI providers register a `modelId` with a signing key. For each
 *         inference / generation request they expose, the provider commits
 *         an EIP-712 signed statement:
 *           {modelId, inputHash, outputHash, timestamp, requesterHash}
 *         where requesterHash is keccak256(abi.encode(requester, nonce)).
 *
 *         Use cases:
 *           - Agentic transactions (AERE402 caller proves the LLM output
 *             it acted on came from a specific signed model)
 *           - Tamper-evident audit logs for regulated AI deployments
 *           - Cross-tenant accountability without revealing raw I/O
 *
 *         IMMUTABILITY:
 *           - Provider's signer key is rotatable (via setSigner) but the
 *             modelId itself is permanent once registered
 *           - Anchored attestations are append-only — never editable
 *
 *         PRIVACY:
 *           - inputHash + outputHash + requesterHash are all hashes
 *           - The on-chain record never sees raw prompts or completions
 *
 * @dev    EIP-712 domain pinned at deploy. Signatures are verified by
 *         ECDSA.recover against the provider's currently-registered signer.
 */
contract AereAIProof {
    using ECDSA for bytes32;

    /* --------------------------------- types --------------------------------- */

    struct Model {
        bool registered;
        address signer;
        address operator;          // can rotate signer
        string description;        // human-readable identifier
    }

    struct Attestation {
        bytes32 modelId;
        bytes32 inputHash;
        bytes32 outputHash;
        bytes32 requesterHash;
        uint256 timestamp;
        uint256 nonce;             // per-modelId monotonic
    }

    /* ------------------------------- immutable ------------------------------- */

    bytes32 public immutable DOMAIN_SEPARATOR;
    bytes32 public constant ATTESTATION_TYPEHASH = keccak256(
        "Attestation(bytes32 modelId,bytes32 inputHash,bytes32 outputHash,bytes32 requesterHash,uint256 timestamp,uint256 nonce)"
    );

    /* --------------------------------- state -------------------------------- */

    mapping(bytes32 => Model) public models;
    mapping(bytes32 => mapping(uint256 => bool)) public usedNonce;  // modelId → nonce → consumed
    mapping(bytes32 => uint256) public attestationCount;            // modelId → total anchored
    /// @notice M3 fix: nonces below this watermark are inadmissible — set
    ///         to the highest used nonce + 1 on every signer rotation so
    ///         the new signer cannot re-use nonces issued under the old
    ///         signer's context, polluting provenance history.
    mapping(bytes32 => uint256) public minValidNonce;
    mapping(bytes32 => uint256) public highestNonceSeen;

    /* --------------------------------- events ------------------------------- */

    event ModelRegistered(bytes32 indexed modelId, address indexed operator, address indexed signer, string description);
    event SignerRotated(bytes32 indexed modelId, address indexed newSigner);
    event Anchored(
        bytes32 indexed modelId,
        bytes32 indexed inputHash,
        bytes32 indexed outputHash,
        bytes32 requesterHash,
        uint256 timestamp,
        uint256 nonce
    );

    /* --------------------------------- errors ------------------------------- */

    error ModelExists();
    error UnknownModel();
    error NotOperator();
    error BadSignature();
    error NonceConsumed();
    error NonceBelowWatermark(uint256 got, uint256 minValid);
    error AttestationInFuture();
    error ZeroAddress();

    /* ----------------------------- constructor ------------------------------ */

    constructor() {
        DOMAIN_SEPARATOR = keccak256(abi.encode(
            keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
            keccak256("AereAIProof"),
            keccak256("1"),
            block.chainid,
            address(this)
        ));
    }

    /* ------------------------------ registration ---------------------------- */

    function registerModel(bytes32 modelId, address signer, string calldata description) external {
        if (modelId == bytes32(0)) revert ZeroAddress();
        if (signer == address(0)) revert ZeroAddress();
        if (models[modelId].registered) revert ModelExists();
        models[modelId] = Model({
            registered: true,
            signer: signer,
            operator: msg.sender,
            description: description
        });
        emit ModelRegistered(modelId, msg.sender, signer, description);
    }

    function setSigner(bytes32 modelId, address newSigner) external {
        Model storage m = models[modelId];
        if (!m.registered) revert UnknownModel();
        if (msg.sender != m.operator) revert NotOperator();
        if (newSigner == address(0)) revert ZeroAddress();
        m.signer = newSigner;
        // M3 fix: invalidate any nonces below this point so new signer
        // cannot reuse the old signer's nonce space.
        minValidNonce[modelId] = highestNonceSeen[modelId] + 1;
        emit SignerRotated(modelId, newSigner);
    }

    /* ------------------------------- anchor --------------------------------- */

    /// @notice Submit a provider-signed attestation. Anyone can submit
    ///         (the provider, the requester, an indexer). The signature
    ///         must come from the model's CURRENT signer.
    function anchor(
        bytes32 modelId,
        bytes32 inputHash,
        bytes32 outputHash,
        bytes32 requesterHash,
        uint256 timestamp,
        uint256 nonce,
        bytes calldata signature
    ) external {
        Model memory m = models[modelId];
        if (!m.registered) revert UnknownModel();
        if (timestamp > block.timestamp) revert AttestationInFuture();
        if (usedNonce[modelId][nonce]) revert NonceConsumed();
        // M3 fix: post-rotation, old nonce space is gone.
        if (nonce < minValidNonce[modelId]) revert NonceBelowWatermark(nonce, minValidNonce[modelId]);

        bytes32 structHash = keccak256(abi.encode(
            ATTESTATION_TYPEHASH,
            modelId, inputHash, outputHash, requesterHash, timestamp, nonce
        ));
        bytes32 digest = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash));
        address recovered = digest.recover(signature);
        if (recovered == address(0) || recovered != m.signer) revert BadSignature();

        usedNonce[modelId][nonce] = true;
        if (nonce > highestNonceSeen[modelId]) highestNonceSeen[modelId] = nonce;
        attestationCount[modelId] += 1;

        emit Anchored(modelId, inputHash, outputHash, requesterHash, timestamp, nonce);
    }
}
