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

/**
 * @title AereInferNet — EU AI Act Article 12 inference-log commitments
 * @notice EU AI Act Article 12 (in force from August 2 2026) requires
 *         providers and deployers of HIGH-RISK AI systems to maintain
 *         automatic event logs covering the system's operation. The
 *         logs must allow ex-post traceability and incident analysis
 *         by the relevant national competent authority.
 *
 *         Today, "logs" lives in vendor S3 buckets. A subpoena later
 *         can show whatever is convenient. AereInferNet turns the
 *         commitment into a public-good primitive:
 *
 *           1. Each inference provider registers their model + the TEE
 *              attestation that bounds it (e.g. Intel SGX / AMD SEV-SNP
 *              measurement). The registration is immutable per (provider,
 *              modelId) — the model commitment cannot be retroactively
 *              "amended" without a new modelId.
 *
 *           2. Each epoch (~24h), the provider commits a Merkle root of
 *              (requestHash, responseHash, timestampMs, deployerId)
 *              leaves for every inference served in that epoch. The
 *              commitment is one transaction per provider per epoch —
 *              cheap enough that even a multi-million-request/day
 *              provider pays cents in gas.
 *
 *           3. Anyone (regulator, customer, journalist, the deployer
 *              themselves) can later prove inclusion of a specific
 *              inference by submitting a Merkle proof against the
 *              committed root. If the provider denies serving a request
 *              and the proof shows it WAS served, the public record
 *              speaks.
 *
 *           4. Slashing: providers MAY post bond via AereAgentBond,
 *              keyed on agentId = keccak256(modelId, providerAddress).
 *              Regulators / customers can dispute → trigger the slash
 *              path. AereInferNet does NOT enforce this layer — it's
 *              composable. Consumers that need stronger guarantees can
 *              require `bond.isBonded(provider, agentId, minBond)`
 *              before accepting the provider's commitments.
 *
 *         DEPLOYER COMMITMENT:
 *           Article 12 obligates BOTH providers and deployers. A
 *           deployer who wires an inference into their own application
 *           records the same leaves on their side and can publish their
 *           own Merkle root — making provider/deployer collusion
 *           detectable (the two roots' leaves must reconcile).
 *
 *         NO PII:
 *           Both `requestHash` and `responseHash` are content hashes
 *           computed off-chain — the chain stores only commitments,
 *           never raw prompts or responses. Aligned with GDPR Art 5(1)(c).
 *
 *         IMMUTABILITY:
 *           - Model registry is append-only; per (provider, modelId)
 *             we set once and never overwrite. Bug-fix releases bump
 *             modelId.
 *           - Commitments append to per-provider epoch history. A
 *             commitment for an epoch already committed reverts.
 */
contract AereInferNet {

    /* ================================ types ================================ */

    /// @notice Severity tier the model is offered for per EU AI Act Annex III.
    ///         Used by indexers to filter; not enforced on-chain.
    enum RiskTier {
        Minimal,         // 0
        Limited,         // 1
        High,            // 2  — Article 12 logging strictly applies
        Unacceptable     // 3  — should never register; left for completeness
    }

    struct ModelRegistration {
        bytes32 modelDigest;          // SHA256 of the model weights blob
        bytes32 attestationDigest;    // hash of the TEE attestation that pins the
                                      //   running binary to `modelDigest`
        RiskTier risk;
        string  modelName;            // human-readable, e.g. "claude-4.7-sonnet-20251022"
        string  attestationUri;       // ipfs:// or https:// — full TEE quote + signing chain
        uint64  registeredAt;
        bool    exists;
    }

    struct EpochCommitment {
        bytes32 root;                 // Merkle root of (req, resp, ts, deployer) leaves
        uint64  committedAt;
        uint64  inferenceCount;       // number of leaves under this root (informational)
        string  metadataUri;          // optional pointer to provider-published leaves
    }

    /* ================================ storage ================================ */

    /// @notice (provider, modelId) → ModelRegistration. modelId is provider-chosen.
    mapping(address => mapping(bytes32 => ModelRegistration)) internal _models;

    /// @notice (provider, modelId, epochId) → EpochCommitment.
    /// AUDIT FIX (HIGH #25): previously the key was (provider, epochId) which
    /// meant multi-model providers could only commit one root per epoch across
    /// all models, and verifyInference had no way to bind a proof to a specific
    /// model. Now keyed by all three.
    mapping(address => mapping(bytes32 => mapping(uint64 => EpochCommitment))) internal _commits;

    /// @notice (provider, modelId) → count of registered epochs (informational).
    mapping(address => mapping(bytes32 => uint256)) public modelEpochCount;

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

    event ModelRegistered(
        address indexed provider,
        bytes32 indexed modelId,
        bytes32 modelDigest,
        bytes32 attestationDigest,
        RiskTier risk,
        string modelName,
        string attestationUri
    );

    event EpochCommitted(
        address indexed provider,
        bytes32 indexed modelId,
        uint64 indexed epochId,
        bytes32 root,
        uint64 inferenceCount,
        string metadataUri
    );

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

    error ModelExists();
    error ModelUnknown();
    error EpochExists();
    error EpochOutOfRange();
    error ZeroDigest();
    error EmptyName();

    /* ============================== model registry ============================ */

    /// @notice Provider registers a new model. msg.sender is the provider.
    ///         Reverts if the (provider, modelId) is already set; cannot be
    ///         amended — register a new modelId for the next version.
    function registerModel(
        bytes32 modelId,
        bytes32 modelDigest,
        bytes32 attestationDigest,
        RiskTier risk,
        string calldata modelName,
        string calldata attestationUri
    ) external {
        if (modelDigest == bytes32(0) || attestationDigest == bytes32(0)) revert ZeroDigest();
        if (bytes(modelName).length == 0) revert EmptyName();
        ModelRegistration storage m = _models[msg.sender][modelId];
        if (m.exists) revert ModelExists();
        m.modelDigest = modelDigest;
        m.attestationDigest = attestationDigest;
        m.risk = risk;
        m.modelName = modelName;
        m.attestationUri = attestationUri;
        m.registeredAt = uint64(block.timestamp);
        m.exists = true;
        emit ModelRegistered(msg.sender, modelId, modelDigest, attestationDigest, risk, modelName, attestationUri);
    }

    /* ============================ epoch commitments ========================== */

    /// @notice Provider commits the Merkle root for an epoch. The model
    ///         MUST already be registered (reverts otherwise — prevents
    ///         committing inferences for a phantom model).
    ///         epochId is provider-chosen but convention is unix-day:
    ///           epochId = uint64(block.timestamp / 86400)
    function commitEpoch(
        bytes32 modelId,
        uint64 epochId,
        bytes32 root,
        uint64 inferenceCount,
        string calldata metadataUri
    ) external {
        if (root == bytes32(0)) revert ZeroDigest();
        ModelRegistration storage m = _models[msg.sender][modelId];
        if (!m.exists) revert ModelUnknown();
        // AUDIT FIX (HIGH #26): epochId must be the current unix-day (with 1-day
        // grace for the just-ended day). Prevents back-dating to rewrite history
        // and forward-dating to commit to future inferences.
        uint64 currentDay = uint64(block.timestamp / 86400);
        if (epochId > currentDay) revert EpochOutOfRange();
        if (currentDay > epochId && currentDay - epochId > 1) revert EpochOutOfRange();
        EpochCommitment storage e = _commits[msg.sender][modelId][epochId];
        if (e.committedAt != 0) revert EpochExists();
        e.root = root;
        e.committedAt = uint64(block.timestamp);
        e.inferenceCount = inferenceCount;
        e.metadataUri = metadataUri;
        modelEpochCount[msg.sender][modelId] += 1;
        emit EpochCommitted(msg.sender, modelId, epochId, root, inferenceCount, metadataUri);
    }

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

    function modelOf(address provider, bytes32 modelId) external view returns (
        bytes32 modelDigest,
        bytes32 attestationDigest,
        RiskTier risk,
        string memory modelName,
        string memory attestationUri,
        uint64 registeredAt,
        bool exists
    ) {
        ModelRegistration storage m = _models[provider][modelId];
        return (m.modelDigest, m.attestationDigest, m.risk, m.modelName, m.attestationUri, m.registeredAt, m.exists);
    }

    function epochOf(address provider, bytes32 modelId, uint64 epochId) external view returns (
        bytes32 root, uint64 committedAt, uint64 inferenceCount, string memory metadataUri
    ) {
        EpochCommitment storage e = _commits[provider][modelId][epochId];
        return (e.root, e.committedAt, e.inferenceCount, e.metadataUri);
    }

    /// @notice Verify that a specific (requestHash, responseHash, timestampMs,
    ///         deployerId) tuple is committed under the provider's root for
    ///         (modelId, epochId). Returns false if the proof is invalid or the
    ///         (modelId, epoch) was never committed.
    /// AUDIT FIX (HIGH #25): modelId is now part of the lookup key.
    function verifyInference(
        address provider,
        bytes32 modelId,
        uint64 epochId,
        bytes32 requestHash,
        bytes32 responseHash,
        uint64 timestampMs,
        bytes32 deployerId,
        bytes32[] calldata proof
    ) external view returns (bool) {
        EpochCommitment storage e = _commits[provider][modelId][epochId];
        if (e.committedAt == 0) return false;
        bytes32 leaf = keccak256(abi.encode(requestHash, responseHash, timestampMs, deployerId));
        return _verifyProof(proof, e.root, leaf);
    }

    /* =============================== internal =============================== */

    /// @dev OpenZeppelin-style Merkle proof verification (sorted-pairs hashing).
    function _verifyProof(bytes32[] calldata proof, bytes32 root, bytes32 leaf) internal pure returns (bool) {
        bytes32 h = leaf;
        for (uint256 i = 0; i < proof.length; i++) {
            bytes32 p = proof[i];
            h = h <= p ? keccak256(abi.encodePacked(h, p)) : keccak256(abi.encodePacked(p, h));
        }
        return h == root;
    }
}
