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

/// @dev Matches Succinct's canonical SP1 verifier / SP1VerifierGateway ABI: verifyProof RETURNS
///      NOTHING and REVERTS on an invalid proof. Used here as the (future) NON-REVOCATION proof
///      verifier. See AereZKScreen.sol for the same pattern already used in Aere's compliance stack.
interface INonRevocationVerifier {
    function verifyProof(bytes32 programVKey, bytes calldata publicValues, bytes calldata proof) external view;
}

/**
 * @title AereBitstringStatusList, a W3C Bitstring Status List for credential revocation / suspension
 *
 * @notice An on-chain implementation of the W3C Bitstring Status List v1.0 mechanism. A status list
 *         is a compact bitset; every credential is assigned a status INDEX into a list, and the bit
 *         at that index encodes the credential's status (default 0 = active; 1 = revoked or
 *         suspended, per the list's purpose). Revoking a credential flips one bit. Because a single
 *         list packs many credentials, a verifier learns only "the bit at index i", which gives herd
 *         privacy: it cannot tell WHICH credential a status update concerns from the list alone.
 *
 *         PURPOSES. A list is created for exactly one W3C statusPurpose:
 *           - Revocation: a status change is PERMANENT. Once a bit is set it can never be cleared
 *             (monotonic), matching revocation's terminal semantics.
 *           - Suspension: a status change is REVERSIBLE. A bit may be set and later cleared.
 *
 *         ZERO-KNOWLEDGE NON-REVOCATION (see the zk section below). The plain on-chain read
 *         statusOf(listId, index) is public and reveals the queried index. For privacy-preserving
 *         verification, a holder can instead prove in zero knowledge that "the status bit of the
 *         credential I hold is 0" WITHOUT revealing which index it is, binding the proof to the list's
 *         current committed state. This file defines that verifier interface and the exact
 *         public-input shape; the SP1 circuit that produces such a proof is [MEASURE] (to be
 *         implemented against Aere's existing SP1 zk stack, the same stack behind AereZKScreen).
 *
 * @dev    HONEST SCOPE. Application-layer compliance tooling. Holds no funds; changes nothing about
 *         Aere consensus (classical secp256k1 ECDSA QBFT). No PII on chain: a status list is an
 *         anonymous bitset keyed by numeric index only.
 */
contract AereBitstringStatusList {
    /// @notice W3C statusPurpose for a list.
    enum Purpose {
        Revocation,
        Suspension
    }

    struct List {
        address controller; // the party (typically the issuer) that owns this list's bits
        Purpose purpose;
        uint256 capacity; // number of status entries (bits) in the list
        uint64 createdAt;
        uint64 epoch; // increments on every bit mutation (monotonic version counter)
        uint64 lastMutatedAt;
        bytes32 publishedRoot; // controller-attested commitment to the CURRENT bitstring [MEASURE]
        uint64 rootEpoch; // the epoch publishedRoot was published at (freshness anchor)
    }

    // listId is the index into _lists.
    List[] private _lists;
    // listId => wordIndex => 256-bit packed status word (bit j of word w is entry w*256 + j).
    mapping(uint256 => mapping(uint256 => uint256)) private _words;

    // ---- zero-knowledge non-revocation program (network-level config) ----

    /// @notice The admin allowed to publish the non-revocation program vkey (the deployer). This is
    ///         intentionally NOT the per-list controller: the circuit is a network artifact.
    address public immutable ADMIN;

    /// @notice The SP1 verifier (SP1VerifierGateway) that checks a non-revocation proof. May be the
    ///         zero address at deploy, in which case the zk non-revocation path is fail-closed off
    ///         until configured. Immutable once set at construction.
    INonRevocationVerifier public immutable NON_REVOCATION_VERIFIER;

    /// @notice The SP1 program verification key for the non-revocation circuit. Zero until the
    ///         circuit is built and its vkey published. [MEASURE]: the circuit itself is pending.
    bytes32 public nonRevocationVKey;

    event ListCreated(uint256 indexed listId, address indexed controller, Purpose purpose, uint256 capacity);
    event StatusSet(uint256 indexed listId, uint256 indexed index, bool value, uint64 epoch);
    event StatusRootPublished(uint256 indexed listId, bytes32 root, uint64 epoch);
    event NonRevocationVKeySet(bytes32 oldVKey, bytes32 newVKey);

    error NotAdmin();
    error NotController(uint256 listId);
    error UnknownList(uint256 listId);
    error ZeroCapacity();
    error IndexOutOfRange(uint256 listId, uint256 index);
    error RevocationIsTerminal(uint256 listId, uint256 index); // cannot clear a revocation bit
    error NoStatusChange(uint256 listId, uint256 index); // the bit already holds `value`
    error NonRevocationNotConfigured();
    error VerifierHasNoCode();

    constructor(address nonRevocationVerifier_) {
        ADMIN = msg.sender;
        // A non-zero but CODE-LESS verifier would make the try/verifyProof in verifyNonRevocation
        // succeed silently (verifyProof returns no data), so a revoked credential would prove "not
        // revoked" = fail-open. Guard it exactly as the sibling AereFinalityCertificateVerifier does.
        // The zero address is still allowed: it keeps the zk non-revocation path fail-closed OFF until
        // a real gateway is configured (nonRevocationConfigured() stays false).
        if (nonRevocationVerifier_ != address(0) && nonRevocationVerifier_.code.length == 0) {
            revert VerifierHasNoCode();
        }
        NON_REVOCATION_VERIFIER = INonRevocationVerifier(nonRevocationVerifier_); // zero allowed
    }

    // ==========================================================================
    //                                Lists
    // ==========================================================================

    /// @notice Create a status list for a single W3C statusPurpose. msg.sender becomes its controller
    ///         (typically the credential issuer). Returns the new listId.
    /// @param purpose  Revocation (terminal bits) or Suspension (reversible bits).
    /// @param capacity the number of status entries (bits) the list holds. [VERIFY] W3C recommends a
    ///                 large minimum (e.g. 131072) for herd privacy; that is a deployment policy
    ///                 choice and is NOT enforced here.
    function createList(Purpose purpose, uint256 capacity) external returns (uint256 listId) {
        if (capacity == 0) revert ZeroCapacity();
        listId = _lists.length;
        _lists.push(
            List({
                controller: msg.sender,
                purpose: purpose,
                capacity: capacity,
                createdAt: uint64(block.timestamp),
                epoch: 0,
                lastMutatedAt: uint64(block.timestamp),
                publishedRoot: bytes32(0),
                rootEpoch: 0
            })
        );
        emit ListCreated(listId, msg.sender, purpose, capacity);
    }

    /// @notice Set the status bit of a credential index. Controller-only.
    ///         Revocation lists: `value` must be true and the bit must currently be 0 (monotonic,
    ///         terminal). Suspension lists: `value` may be true or false (reversible). Reverts if the
    ///         bit already equals `value` (no silent no-op).
    function setStatus(uint256 listId, uint256 index, bool value) public {
        List storage l = _requireList(listId);
        if (msg.sender != l.controller) revert NotController(listId);
        if (index >= l.capacity) revert IndexOutOfRange(listId, index);

        bool current = _getBit(listId, index);
        if (current == value) revert NoStatusChange(listId, index);
        if (l.purpose == Purpose.Revocation && !value) revert RevocationIsTerminal(listId, index);

        _setBit(listId, index, value);
        unchecked {
            l.epoch += 1;
        }
        l.lastMutatedAt = uint64(block.timestamp);
        emit StatusSet(listId, index, value, l.epoch);
    }

    /// @notice Convenience: revoke (set bit to 1) the credential at `index` on a Revocation list.
    function revoke(uint256 listId, uint256 index) external {
        setStatus(listId, index, true);
    }

    /// @notice Publish a controller-attested commitment to the CURRENT bitstring, anchored to the
    ///         current epoch. A zk non-revocation proof (below) binds to this root, so a verifier
    ///         accepts a proof only against an up-to-date root.
    /// @dev    [MEASURE] The root is controller-attested in this version: the controller is trusted
    ///         to publish a commitment matching the live bits it just wrote. A future version derives
    ///         the root on-chain (or inside the SP1 VM) from getWord so it is trustless. The raw words
    ///         are exposed by getWord for independent recomputation regardless.
    function publishStatusRoot(uint256 listId, bytes32 root) external {
        List storage l = _requireList(listId);
        if (msg.sender != l.controller) revert NotController(listId);
        l.publishedRoot = root;
        l.rootEpoch = l.epoch;
        emit StatusRootPublished(listId, root, l.epoch);
    }

    // ==========================================================================
    //                                Reads
    // ==========================================================================

    /// @notice The status bit of a credential index (true = revoked/suspended per the list purpose).
    function statusOf(uint256 listId, uint256 index) external view returns (bool) {
        List storage l = _requireList(listId);
        if (index >= l.capacity) revert IndexOutOfRange(listId, index);
        return _getBit(listId, index);
    }

    /// @notice The raw 256-bit packed word at `wordIndex` (entries [wordIndex*256, wordIndex*256+255]).
    ///         Exposed so anyone can recompute a commitment over the full bitstring off-chain.
    function getWord(uint256 listId, uint256 wordIndex) external view returns (uint256) {
        _requireList(listId);
        return _words[listId][wordIndex];
    }

    /// @notice The number of 256-bit words backing a list's capacity.
    function wordCount(uint256 listId) external view returns (uint256) {
        List storage l = _requireList(listId);
        return (l.capacity + 255) / 256;
    }

    /// @notice Number of lists ever created (listId runs 0..listCount-1).
    function listCount() external view returns (uint256) {
        return _lists.length;
    }

    /// @notice Read a list's metadata. Reverts on an unknown listId.
    function getList(uint256 listId)
        external
        view
        returns (
            address controller,
            Purpose purpose,
            uint256 capacity,
            uint64 createdAt,
            uint64 epoch,
            uint64 lastMutatedAt,
            bytes32 publishedRoot,
            uint64 rootEpoch
        )
    {
        List storage l = _requireList(listId);
        return (l.controller, l.purpose, l.capacity, l.createdAt, l.epoch, l.lastMutatedAt, l.publishedRoot, l.rootEpoch);
    }

    // ==========================================================================
    //                     Zero-knowledge non-revocation
    // ==========================================================================
    //
    // GOAL. Let a credential HOLDER prove "the status bit of my credential is 0" (i.e. my credential
    // is NOT revoked) WITHOUT revealing which status index it is, and bind that proof to the list's
    // current on-chain state so a stale proof is rejected.
    //
    // PUBLIC-INPUT SHAPE (the SP1 program's committed public values), ABI-encoded exactly as:
    //   abi.encode(
    //     uint256 chainId,             // must equal block.chainid
    //     address statusListContract,  // must equal address(this)
    //     uint256 statusListId,        // the list the credential's status lives in
    //     bytes32 statusRoot,          // commitment to the list bitstring the proof was made against
    //     bytes32 credentialCommitment,// hiding commitment to the holder's credential (binds a real
    //                                  //   anchored credential without revealing its index)
    //     uint8   purpose              // must equal uint8(list.purpose)
    //   )
    //
    // IN-CIRCUIT WITNESS (private, [MEASURE] to be implemented):
    //   - the status index i of the holder's credential,
    //   - an opening of `statusRoot` at i proving bitstring[i] == 0,
    //   - an opening of `credentialCommitment` to the credential (issuer, type, index i),
    //   so the statement proven is: "there is an index i, committed by credentialCommitment, whose
    //   bit under statusRoot is 0", revealing neither i nor the credential.
    //
    // ON-CHAIN BINDING. verifyNonRevocation enforces chainId / contract / listId / purpose, and
    // requires statusRoot to equal the list's publishedRoot AT THE CURRENT EPOCH (rootEpoch == epoch),
    // so a proof is only accepted against the freshest committed state. It then delegates soundness of
    // the bit-is-0 statement to the SP1 verifier. FAIL-CLOSED if the verifier or vkey is unconfigured.

    /// @notice The canonical ABI type string of the non-revocation public inputs (for off-chain tooling).
    string public constant NON_REVOCATION_PUBLIC_VALUES_ABI =
        "(uint256 chainId,address statusListContract,uint256 statusListId,bytes32 statusRoot,bytes32 credentialCommitment,uint8 purpose)";

    /// @notice Publish the SP1 non-revocation program vkey (admin-only). Settable because the circuit
    ///         is [MEASURE]: once built and its vkey known, the admin sets it; setting it to a fresh
    ///         value is how the program is upgraded. Emits the transition for auditability.
    function setNonRevocationVKey(bytes32 vkey) external {
        if (msg.sender != ADMIN) revert NotAdmin();
        bytes32 old = nonRevocationVKey;
        nonRevocationVKey = vkey;
        emit NonRevocationVKeySet(old, vkey);
    }

    /// @notice True once a non-revocation verifier AND a program vkey are both configured.
    function nonRevocationConfigured() public view returns (bool) {
        return address(NON_REVOCATION_VERIFIER) != address(0) && nonRevocationVKey != bytes32(0);
    }

    /**
     * @notice Verify a zero-knowledge NON-REVOCATION proof for `listId` against its current committed
     *         state. Returns true iff (a) the public inputs bind this chain, this contract, this list
     *         and its purpose, (b) the proof's statusRoot equals the list's publishedRoot at the
     *         current epoch (freshness), and (c) the SP1 verifier accepts the proof. FAIL-CLOSED:
     *         returns false (never reverts) on any binding mismatch or invalid proof, and reverts
     *         with NonRevocationNotConfigured only when the program is not yet configured, so callers
     *         cannot mistake "unconfigured" for "not revoked".
     *
     * @dev    [MEASURE] The SP1 circuit that produces the proof is not yet implemented; when it is,
     *         its vkey is published via setNonRevocationVKey and the verifier accepts real proofs. The
     *         on-chain binding here is complete and final; only the circuit is pending.
     */
    function verifyNonRevocation(uint256 listId, bytes calldata publicValues, bytes calldata proof)
        external
        view
        returns (bool)
    {
        List storage l = _requireList(listId);
        if (!nonRevocationConfigured()) revert NonRevocationNotConfigured();

        // Exact length check (6 * 32 = 192) prevents a trailing-bytes decode spoof.
        if (publicValues.length != 192) return false;
        (
            uint256 chainId,
            address statusListContract,
            uint256 statusListId,
            bytes32 statusRoot,
            , // credentialCommitment: bound inside the circuit, not re-checked on chain
            uint8 purpose
        ) = abi.decode(publicValues, (uint256, address, uint256, bytes32, bytes32, uint8));

        if (chainId != block.chainid) return false;
        if (statusListContract != address(this)) return false;
        if (statusListId != listId) return false;
        if (purpose != uint8(l.purpose)) return false;
        // Freshness: the proof must be against the current published root (root must be up to date).
        if (l.publishedRoot == bytes32(0)) return false;
        if (l.rootEpoch != l.epoch) return false;
        if (statusRoot != l.publishedRoot) return false;

        // Delegate the "bit is 0 for a hidden index" statement to the SP1 verifier (reverts if invalid).
        try NON_REVOCATION_VERIFIER.verifyProof(nonRevocationVKey, publicValues, proof) {
            return true;
        } catch {
            return false;
        }
    }

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

    function _requireList(uint256 listId) internal view returns (List storage l) {
        if (listId >= _lists.length) revert UnknownList(listId);
        l = _lists[listId];
    }

    function _getBit(uint256 listId, uint256 index) internal view returns (bool) {
        uint256 word = _words[listId][index >> 8];
        return ((word >> (index & 0xff)) & 1) == 1;
    }

    function _setBit(uint256 listId, uint256 index, bool value) internal {
        uint256 wordIndex = index >> 8;
        uint256 mask = uint256(1) << (index & 0xff);
        uint256 word = _words[listId][wordIndex];
        if (value) {
            word |= mask;
        } else {
            word &= ~mask;
        }
        _words[listId][wordIndex] = word;
    }
}
