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

/// @dev The AereTrustRegistry surface this contract relies on: accreditation lookups and the
///      POST-QUANTUM issuer signature verification (which routes to the live precompile 0x0AE1).
interface IAereTrustRegistryVC {
    function isAccredited(uint256 issuerId, bytes32 credentialType) external view returns (bool);
    function issuerStatus(uint256 issuerId) external view returns (uint8); // AereTrustRegistry.Status
    function verifyIssuerSignature(uint256 issuerId, bytes32 message, bytes calldata signature)
        external
        view
        returns (bool);
}

/// @dev The AereBitstringStatusList surface used for the not-revoked check.
interface IAereStatusListVC {
    function statusOf(uint256 listId, uint256 index) external view returns (bool);
}

/**
 * @title AereVerifiableCredential, a W3C Verifiable Credentials 2.0 style on-chain anchor + verifier
 *
 * @notice Anchors and verifies W3C Verifiable Credentials (VC 2.0 data model) on Aere Network with a
 *         POST-QUANTUM issuer signature path. A credential here binds:
 *           - an ISSUER, an accredited issuer id in the AereTrustRegistry;
 *           - a SUBJECT, identified by a did:aere DID (bound by its hash; the DID string itself is a
 *             public identifier carried off-chain, no PII on chain);
 *           - a CREDENTIAL TYPE (an opaque bytes32 tag, e.g. keccak256("KYCCredential"));
 *           - a VALIDITY window (validFrom / validUntil, W3C validFrom / validUntil);
 *           - a STATUS-LIST ENTRY (a W3C BitstringStatusListEntry: a list id + a status index);
 *           - a CLAIMS HASH, the keccak256 of the off-chain VC JSON claims (the credentialSubject),
 *             so the on-chain anchor commits to the credential content without publishing it.
 *
 *         VERIFICATION (verifyPresented / isValid) checks, in order and fail-closed:
 *           1. the issuer is ACCREDITED for this credential type in the AereTrustRegistry;
 *           2. the issuer's POST-QUANTUM (Falcon-512) signature over the credential digest is valid,
 *              verified in full on-chain via the live precompile 0x0AE1 (through the Trust Registry);
 *           3. the credential is WITHIN its validity window (not-yet-valid and expired both fail);
 *           4. the credential is NOT REVOKED per its Bitstring Status List entry.
 *
 *         DID METHOD. Subjects (and issuers) use the `did:aere` method, consistent with Aere's
 *         AereIdentityRegistry8004 (did:aere:<chainId>:<id>). Full DID Core syntax validation is an
 *         off-chain concern; anchorCredentialWithDid enforces the `did:aere:` prefix as a minimal
 *         on-chain guard. [VERIFY] against the finalized did:aere method registration.
 *
 *         ANCHORING. anchorCredential verifies the FULL path (accreditation + PQC signature + validity)
 *         fail-closed and records the credential once. Authorship is the POST-QUANTUM signature alone,
 *         so anchoring may be relayed by any msg.sender (only the issuer's Falcon key can produce a
 *         valid signature over the contract-bound digest). A recorded anchor is then re-checked LIVE
 *         by isValid, which fails closed the instant the issuer is suspended / revoked or the
 *         credential is revoked on its status list, without needing the signature again.
 *
 * @dev    HONEST SCOPE. Application-layer compliance tooling. Holds no funds; changes nothing about
 *         Aere consensus (classical secp256k1 ECDSA QBFT). What is post-quantum is the AUTHENTICITY of
 *         the issuer's attestation, verified by the live precompile. Whether any real institution
 *         issues credentials here is a partnership matter external to this code and is NOT asserted.
 *
 * @dev    [VERIFY] against the finalized W3C VC 2.0 serialization: this contract anchors a COMMITMENT
 *         to a VC (its digest + claims hash), not the JSON-LD document itself. The mapping of these
 *         fields to the VC JSON members (context, type, issuer, validFrom, credentialStatus) is
 *         defined in docs/AERE-PQ-SCREEN.md; reconcile exact JSON member names before claiming wire
 *         conformance.
 */
contract AereVerifiableCredential {
    /// @notice AereTrustRegistry.Status.Active (an issuer must be Active to issue / stay valid).
    uint8 internal constant ISSUER_STATUS_ACTIVE = 1;

    /// @notice Domain separator binding a credential digest to this contract and version.
    bytes32 public constant VC_DOMAIN = keccak256("AereVerifiableCredential.v2.credential");

    /// @notice The DID method these credentials use for subjects and issuers.
    string public constant DID_METHOD = "did:aere";

    /// @notice The federated accredited-issuer trust registry.
    IAereTrustRegistryVC public immutable TRUST_REGISTRY;

    /// @notice The W3C Bitstring Status List used for revocation / suspension.
    IAereStatusListVC public immutable STATUS_LIST;

    /// @notice A W3C VC 2.0 credential binding. `subjectDidHash` is keccak256(bytes(subjectDid)); the
    ///         DID string is carried off-chain to keep the hot path free of variable-length data and
    ///         to keep identifiers off the chain. `claimsHash` is keccak256 of the off-chain VC claims.
    struct Credential {
        uint256 issuerId; // accredited issuer id in the AereTrustRegistry
        bytes32 subjectDidHash; // keccak256(bytes(subjectDid)), subjectDid is a did:aere DID
        bytes32 credentialType; // opaque type tag, e.g. keccak256("KYCCredential")
        uint64 validFrom; // W3C validFrom (unix seconds, inclusive)
        uint64 validUntil; // W3C validUntil (unix seconds, inclusive); 0 = no expiry
        uint256 statusListId; // W3C BitstringStatusListEntry: the list
        uint256 statusIndex; // W3C BitstringStatusListEntry: the index within the list
        bytes32 claimsHash; // keccak256 of the off-chain credentialSubject claims (no PII on chain)
    }

    struct Anchor {
        bool exists;
        uint256 issuerId;
        bytes32 credentialType;
        bytes32 subjectDidHash;
        uint64 validFrom;
        uint64 validUntil;
        uint256 statusListId;
        uint256 statusIndex;
        uint64 anchoredAt;
    }

    mapping(bytes32 => Anchor) private _anchors; // credentialId => anchor
    bytes32[] private _anchorIds; // append-only list of anchored credential ids

    event CredentialAnchored(
        bytes32 indexed credentialId,
        uint256 indexed issuerId,
        bytes32 indexed credentialType,
        bytes32 subjectDidHash,
        uint256 statusListId,
        uint256 statusIndex
    );
    event CredentialAnchoredWithDid(bytes32 indexed credentialId, string subjectDid);

    error ZeroAddress();
    error NotAccredited(uint256 issuerId, bytes32 credentialType);
    error SignatureInvalid();
    error NotYetValid(uint64 validFrom);
    error Expired(uint64 validUntil);
    error AlreadyAnchored(bytes32 credentialId);
    error UnknownCredential(bytes32 credentialId);
    error DidMismatch();
    error NotAereDid();

    constructor(address trustRegistry_, address statusList_) {
        if (trustRegistry_ == address(0) || statusList_ == address(0)) revert ZeroAddress();
        TRUST_REGISTRY = IAereTrustRegistryVC(trustRegistry_);
        STATUS_LIST = IAereStatusListVC(statusList_);
    }

    // ==========================================================================
    //                          Digest (what the issuer signs)
    // ==========================================================================

    /**
     * @notice The canonical 32-byte digest an issuer's Falcon-512 key signs to attest a credential.
     *         Binds chainId + this contract + every credential field, so a signature cannot be
     *         replayed against another contract, chain, subject, type, validity, or status entry.
     *         This value is also the credentialId used to anchor and look the credential up.
     */
    function credentialDigest(Credential calldata c) public view returns (bytes32) {
        return keccak256(
            abi.encode(
                VC_DOMAIN,
                block.chainid,
                address(this),
                c.issuerId,
                c.subjectDidHash,
                c.credentialType,
                c.validFrom,
                c.validUntil,
                c.statusListId,
                c.statusIndex,
                c.claimsHash
            )
        );
    }

    /// @notice Alias of {credentialDigest}: the id a credential is anchored and referenced under.
    function credentialId(Credential calldata c) external view returns (bytes32) {
        return credentialDigest(c);
    }

    /// @notice Helper: the subjectDidHash for a DID string (keccak256 of its UTF-8 bytes).
    function hashDid(string calldata did) external pure returns (bytes32) {
        return keccak256(bytes(did));
    }

    // ==========================================================================
    //                          Verification (views)
    // ==========================================================================

    /// @notice True iff the issuer is accredited for the type AND its Falcon-512 signature over the
    ///         credential digest verifies on-chain via 0x0AE1. Does NOT check validity or revocation.
    ///         Fail-closed (returns false, never reverts).
    function verifyCredentialSignature(Credential calldata c, bytes calldata issuerSig) public view returns (bool) {
        if (!TRUST_REGISTRY.isAccredited(c.issuerId, c.credentialType)) return false;
        return TRUST_REGISTRY.verifyIssuerSignature(c.issuerId, credentialDigest(c), issuerSig);
    }

    /// @notice True iff `c` is within its validity window at the current block time.
    function isWithinValidity(Credential calldata c) public view returns (bool) {
        if (block.timestamp < c.validFrom) return false;
        if (c.validUntil != 0 && block.timestamp > c.validUntil) return false;
        return true;
    }

    /// @notice True iff `c` is marked revoked/suspended on its Bitstring Status List entry.
    function isRevoked(Credential calldata c) public view returns (bool) {
        return STATUS_LIST.statusOf(c.statusListId, c.statusIndex);
    }

    /**
     * @notice Full W3C verification of a PRESENTED credential in one call, WITHOUT requiring it to be
     *         anchored: accreditation + post-quantum signature + within-validity + not-revoked. Returns
     *         true only if all four hold. Fail-closed (never reverts), so any verifier can call it via
     *         eth_call to check a credential a holder presents off-chain.
     */
    function verifyPresented(Credential calldata c, bytes calldata issuerSig) external view returns (bool) {
        if (!verifyCredentialSignature(c, issuerSig)) return false;
        if (!isWithinValidity(c)) return false;
        if (isRevoked(c)) return false;
        return true;
    }

    // ==========================================================================
    //                                Anchor
    // ==========================================================================

    /**
     * @notice Anchor a credential on-chain after verifying the FULL path fail-closed: the issuer must
     *         be accredited for the type, the credential must be within its validity window, and the
     *         issuer's Falcon-512 signature over the credential digest must verify via 0x0AE1. Records
     *         the credential once and emits CredentialAnchored. Reverts (records nothing) on any
     *         failure. Relayable by anyone: authorship is the post-quantum signature.
     * @return id the credentialId (== credentialDigest) the credential was anchored under.
     */
    function anchorCredential(Credential calldata c, bytes calldata issuerSig) public returns (bytes32 id) {
        id = credentialDigest(c);
        if (_anchors[id].exists) revert AlreadyAnchored(id);

        if (!TRUST_REGISTRY.isAccredited(c.issuerId, c.credentialType)) {
            revert NotAccredited(c.issuerId, c.credentialType);
        }
        if (block.timestamp < c.validFrom) revert NotYetValid(c.validFrom);
        if (c.validUntil != 0 && block.timestamp > c.validUntil) revert Expired(c.validUntil);

        // Full on-chain post-quantum verification of the issuer's attestation (fail-closed).
        if (!TRUST_REGISTRY.verifyIssuerSignature(c.issuerId, id, issuerSig)) revert SignatureInvalid();

        _anchors[id] = Anchor({
            exists: true,
            issuerId: c.issuerId,
            credentialType: c.credentialType,
            subjectDidHash: c.subjectDidHash,
            validFrom: c.validFrom,
            validUntil: c.validUntil,
            statusListId: c.statusListId,
            statusIndex: c.statusIndex,
            anchoredAt: uint64(block.timestamp)
        });
        _anchorIds.push(id);

        emit CredentialAnchored(id, c.issuerId, c.credentialType, c.subjectDidHash, c.statusListId, c.statusIndex);
    }

    /**
     * @notice As {anchorCredential}, but additionally binds and emits the subject's did:aere DID
     *         string (enforcing keccak256(subjectDid) == c.subjectDidHash and the `did:aere:` prefix),
     *         so resolvers can recover the subject DID from events without any PII on chain.
     */
    function anchorCredentialWithDid(Credential calldata c, string calldata subjectDid, bytes calldata issuerSig)
        external
        returns (bytes32 id)
    {
        if (keccak256(bytes(subjectDid)) != c.subjectDidHash) revert DidMismatch();
        if (!_isAereDid(subjectDid)) revert NotAereDid();
        id = anchorCredential(c, issuerSig);
        emit CredentialAnchoredWithDid(id, subjectDid);
    }

    // ==========================================================================
    //                        Anchored-credential reads
    // ==========================================================================

    /**
     * @notice Live validity of an ANCHORED credential: the issuer must STILL be accredited for the
     *         type, the credential must be within its validity window, and it must not be revoked on
     *         its status list. Fails closed the instant the issuer is suspended / revoked (which flips
     *         isAccredited to false) or the credential's status bit is set. The post-quantum signature
     *         was verified at anchor time and cannot silently become valid again after revocation,
     *         because issuer suspension / key revocation both fail this check.
     */
    function isValid(bytes32 id) external view returns (bool) {
        Anchor storage a = _anchors[id];
        if (!a.exists) return false;
        if (!TRUST_REGISTRY.isAccredited(a.issuerId, a.credentialType)) return false;
        if (block.timestamp < a.validFrom) return false;
        if (a.validUntil != 0 && block.timestamp > a.validUntil) return false;
        if (STATUS_LIST.statusOf(a.statusListId, a.statusIndex)) return false;
        return true;
    }

    /// @notice True iff a credential id has been anchored (regardless of current validity).
    function isAnchored(bytes32 id) external view returns (bool) {
        return _anchors[id].exists;
    }

    /// @notice Read an anchored credential record. Reverts on an unknown id.
    function getAnchor(bytes32 id) external view returns (Anchor memory) {
        Anchor storage a = _anchors[id];
        if (!a.exists) revert UnknownCredential(id);
        return a;
    }

    /// @notice Total number of anchored credentials.
    function anchorCount() external view returns (uint256) {
        return _anchorIds.length;
    }

    /// @notice The credential id at an index in the append-only anchor list.
    function anchorIdAt(uint256 index) external view returns (bytes32) {
        return _anchorIds[index];
    }

    // ==========================================================================
    //                                Internal
    // ==========================================================================

    /// @dev Minimal on-chain guard that a DID string begins with "did:aere:". Full DID Core syntax
    ///      validation is an off-chain concern. [VERIFY] against the finalized did:aere method.
    function _isAereDid(string calldata did) internal pure returns (bool) {
        bytes calldata b = bytes(did);
        bytes memory prefix = bytes("did:aere:");
        if (b.length < prefix.length) return false;
        for (uint256 i = 0; i < prefix.length; i++) {
            if (b[i] != prefix[i]) return false;
        }
        return true;
    }
}
