import * as block from 'multiformats/block';
import * as cbor from '@ipld/dag-cbor';
import { Cid } from '../utils/cid.js';
import { CID } from 'multiformats/cid';
import { executeUnlessAborted } from '../utils/abort.js';
import { FilterUtility } from '../utils/filter.js';
import { IndexLevel } from './index-level.js';
import { Message } from '../core/message.js';
import { Replication } from '../utils/replication.js';
import { sha256 } from 'multiformats/hashes/sha2';
import { SortDirection } from '../types/query-types.js';
import { createLevelDatabase, LevelWrapper } from './level-wrapper.js';
import { DwnError, DwnErrorCode } from '../core/dwn-error.js';
/**
 * Default compound indexes that cover the most common DWN query patterns.
 * These are automatically registered unless overridden via config.
 *
 * Each compound index enables a single-scan query when the filter matches
 * all equality properties and the sort matches the sort property.
 */
const DEFAULT_COMPOUND_INDEXES = [
    {
        // Covers: RecordsQuery with protocol + protocolPath, sorted by messageTimestamp
        // Example: "all chat messages in protocol X, path 'chat/message', sorted by time"
        name: 'protocol-protocolPath-messageTimestamp',
        properties: ['protocol', 'protocolPath'],
        sortProperty: 'messageTimestamp',
    },
    {
        // Covers: RecordsQuery with protocol + protocolPath, sorted by dateCreated
        name: 'protocol-protocolPath-dateCreated',
        properties: ['protocol', 'protocolPath'],
        sortProperty: 'dateCreated',
    },
    {
        // Covers: RecordsQuery with schema, sorted by messageTimestamp
        // Example: "all records with schema X, sorted by time"
        name: 'schema-messageTimestamp',
        properties: ['schema'],
        sortProperty: 'messageTimestamp',
    },
    {
        // Covers: RecordsQuery with schema, sorted by dateCreated
        name: 'schema-dateCreated',
        properties: ['schema'],
        sortProperty: 'dateCreated',
    },
    {
        // Covers: RecordsQuery with protocol + contextId, sorted by messageTimestamp
        // Example: "all messages in a specific protocol context (conversation), sorted by time"
        // NOTE: contextId is often a prefix/range filter. Compound indexes only support equality.
        // This index is useful when contextId is an exact match (e.g., root-level context queries).
        name: 'protocol-contextId-messageTimestamp',
        properties: ['protocol', 'contextId'],
        sortProperty: 'messageTimestamp',
    },
];
/**
 * Sublevel names under the single Level root. Every mutation touching multiple partitions is
 * committed as one atomic batch on the root, so store↔log divergence is impossible by construction.
 */
const BLOCKS_PARTITION = 'blocks';
const INDEX_PARTITION = 'idx';
const LOG_PARTITION = 'log';
const CID_TO_SEQ_PARTITION = 'cid';
const FINGERPRINT_PARTITION = 'fp';
const HEADS_PARTITION = 'heads';
const META_PARTITION = 'meta';
const EPOCH_KEY = 'epoch';
const CURRENT_PARTITIONS = new Set([
    BLOCKS_PARTITION,
    INDEX_PARTITION,
    LOG_PARTITION,
    CID_TO_SEQ_PARTITION,
    FINGERPRINT_PARTITION,
    HEADS_PARTITION,
    META_PARTITION,
]);
/**
 * A {@link MessageStore} and {@link ReplicationFeedReader} implementation that works in both the
 * browser and server-side, backed by a SINGLE LevelDB root: message blocks, query indexes, the
 * per-tenant replication log, the cid→seq index, fingerprint domains, the tenant counters, and
 * the store epoch are all sublevels of one Level instance, so every mutation commits as one fully
 * atomic batch.
 *
 * A per-tenant async write mutex spans seq assignment through batch write, so commit order equals
 * seq order by construction. Seq assignment is gap-free (a failed batch never persists the head),
 * while the readable log stays sparse after compaction and under filters — readers never assume
 * contiguity.
 */
export class MessageStoreLevel {
    config;
    wakePublisher;
    partitionsPromise;
    epochPromise;
    writeLocks = new Map();
    /**
     * @param {MessageStoreLevelConfig} config
     * @param {string} config.location - must be a directory path (relative or absolute) where
     *  LevelDB will store its files, or in browsers, the name of the
     * {@link https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase IDBDatabase} to be opened.
     * @param {CompoundIndexDefinition[]} config.compoundIndexes - compound indexes to register.
     *   Defaults to DEFAULT_COMPOUND_INDEXES which cover the most common DWN query patterns.
     * @param {WakePublisher} config.wakePublisher - optional bus for store-owned wake publication.
     */
    constructor(config = {}) {
        this.config = {
            location: 'MESSAGESTORE',
            createLevelDatabase,
            ...config
        };
        this.wakePublisher = this.config.wakePublisher;
    }
    async open() {
        const partitions = await this.partitions();
        await partitions.root.open();
        await this.assertNoPreSubstrateLayout(partitions);
        await this.getEpoch();
    }
    async close() {
        if (this.partitionsPromise === undefined) {
            return;
        }
        const partitions = await this.partitionsPromise;
        await partitions.root.close();
    }
    /**
     * Lazily creates the single Level root and its partition handles.
     */
    async partitions() {
        this.partitionsPromise ??= this.createPartitions().catch((error) => {
            this.partitionsPromise = undefined;
            throw error;
        });
        return this.partitionsPromise;
    }
    async createPartitions() {
        const location = this.config.location;
        const db = await this.config.createLevelDatabase(location, { keyEncoding: 'utf8', valueEncoding: 'utf8' });
        const root = new LevelWrapper({ location, createLevelDatabase: this.config.createLevelDatabase, keyEncoding: 'utf8', valueEncoding: 'utf8' }, db);
        // A binary-valued view over the same underlying Level instance — its partitions carry their
        // own value encoding, so block bytes and JSON strings coexist in one atomic batch domain.
        const binaryView = new LevelWrapper({ location, createLevelDatabase: this.config.createLevelDatabase, keyEncoding: 'utf8', valueEncoding: 'binary' }, db);
        return {
            root,
            blocks: await binaryView.partition(BLOCKS_PARTITION),
            log: await root.partition(LOG_PARTITION),
            cidToSeq: await root.partition(CID_TO_SEQ_PARTITION),
            fingerprints: await root.partition(FINGERPRINT_PARTITION),
            heads: await root.partition(HEADS_PARTITION),
            meta: await root.partition(META_PARTITION),
        };
    }
    /**
     * The query index over the shared root. Constructed lazily alongside the partitions.
     */
    indexPromise;
    async index() {
        this.indexPromise ??= (async () => {
            const partitions = await this.partitions();
            const indexRoot = await partitions.root.partition(INDEX_PARTITION);
            return new IndexLevel({
                location: this.config.location,
                createLevelDatabase: this.config.createLevelDatabase,
                compoundIndexes: this.config.compoundIndexes ?? DEFAULT_COMPOUND_INDEXES,
            }, indexRoot);
        })().catch((error) => {
            this.indexPromise = undefined;
            throw error;
        });
        return this.indexPromise;
    }
    /**
     * Returns the persisted store epoch, generating and persisting a fresh `crypto.randomUUID()`
     * at first open. Replaced only by `clear()` — a full local reset with no surviving cursors.
     */
    async epoch() {
        return this.getEpoch();
    }
    async getEpoch() {
        this.epochPromise ??= this.initializeEpoch().catch((error) => {
            this.epochPromise = undefined;
            throw error;
        });
        return this.epochPromise;
    }
    async initializeEpoch() {
        const partitions = await this.partitions();
        const existing = await partitions.meta.get(EPOCH_KEY);
        if (existing !== undefined) {
            return existing;
        }
        const freshEpoch = crypto.randomUUID();
        await partitions.meta.put(EPOCH_KEY, freshEpoch);
        return freshEpoch;
    }
    async get(tenant, cidString, options) {
        options?.signal?.throwIfAborted();
        const partitions = await executeUnlessAborted(this.partitions(), options?.signal);
        const tenantBlocks = await executeUnlessAborted(partitions.blocks.partition(tenant), options?.signal);
        const cid = CID.parse(cidString);
        const bytes = await tenantBlocks.get(cid.toString(), options);
        if (!bytes) {
            return undefined;
        }
        const decodedBlock = await executeUnlessAborted(block.decode({ bytes, codec: cbor, hasher: sha256 }), options?.signal);
        const message = decodedBlock.value;
        return message;
    }
    async query(tenant, filters, messageSort, pagination, options) {
        options?.signal?.throwIfAborted();
        // creates the query options including sorting and pagination.
        // this adds 1 to the limit if provided, that way we can check to see if there are additional results and provide a return cursor.
        const queryOptions = MessageStoreLevel.buildQueryOptions(messageSort, pagination);
        const index = await this.index();
        const results = await index.query(tenant, filters, queryOptions, options);
        let cursor;
        // checks to see if the returned results are greater than the limit, which would indicate additional results.
        if (pagination?.limit !== undefined && pagination.limit < results.length) {
            // has additional records, remove last record and set cursor
            results.splice(-1);
            // set cursor to the last item remaining after the spliced result.
            cursor = IndexLevel.createCursorFromLastArrayItem(results, queryOptions.sortProperty);
        }
        const messages = [];
        for (let i = 0; i < results.length; i++) {
            const { messageCid } = results[i];
            const message = await this.get(tenant, messageCid, options);
            if (message) {
                messages.push(message);
            }
        }
        return { messages, cursor };
    }
    async count(tenant, filters, messageSort, options) {
        options?.signal?.throwIfAborted();
        const queryOptions = MessageStoreLevel.buildQueryOptions(messageSort);
        const index = await this.index();
        return index.count(tenant, filters, queryOptions, options);
    }
    /**
     * Builds the IndexLevel QueryOptions object given MessageStore sort and pagination parameters.
     */
    static buildQueryOptions(messageSort = {}, pagination = {}) {
        let { limit, cursor } = pagination;
        const { dateCreated, datePublished, messageTimestamp } = messageSort;
        let sortDirection = SortDirection.Ascending; // default
        // `keyof MessageSort` = name of all properties of `MessageSort` defaults to messageTimestamp
        let sortProperty = 'messageTimestamp';
        // set the sort property
        if (dateCreated !== undefined) {
            sortProperty = 'dateCreated';
        }
        else if (datePublished !== undefined) {
            sortProperty = 'datePublished';
        }
        else if (messageTimestamp !== undefined) {
            sortProperty = 'messageTimestamp';
        }
        if (messageSort[sortProperty] !== undefined) {
            sortDirection = messageSort[sortProperty];
        }
        // we add one more to the limit to determine whether there are additional results and to return a cursor.
        if (limit !== undefined && limit > 0) {
            limit = limit + 1;
        }
        return { sortDirection, sortProperty, limit, cursor };
    }
    async put(tenant, message, indexes, options) {
        options?.signal?.throwIfAborted();
        const partitions = await executeUnlessAborted(this.partitions(), options?.signal);
        const index = await executeUnlessAborted(this.index(), options?.signal);
        const encodedMessageBlock = await executeUnlessAborted(block.encode({ value: message, codec: cbor, hasher: sha256 }), options?.signal);
        // MessageStore data may contain `encodedData` which is not taken into account when calculating the blockCID as it is optional data.
        const messageCid = Cid.parseCid(await Message.getCid(message)).toString();
        return this.withTenantWriteLock(tenant, async () => {
            options?.signal?.throwIfAborted();
            const tenantCidToSeq = await partitions.cidToSeq.partition(tenant);
            const existingSeq = await tenantCidToSeq.get(messageCid, options);
            if (existingSeq !== undefined) {
                // Duplicates mutate nothing and publish no wake.
                return { status: 'duplicate' };
            }
            const head = await this.getHead(partitions, tenant);
            const seq = head + 1n;
            const fingerprintScopes = Replication.computeFingerprintScopes(message, indexes);
            const logEntry = {
                seq: seq.toString(),
                messageCid,
                indexes,
                fingerprintScopes,
            };
            const tenantBlocks = await partitions.blocks.partition(tenant);
            const blockOperation = tenantBlocks.createOperation({ type: 'put', key: messageCid, value: encodedMessageBlock.bytes });
            const tenantLog = await partitions.log.partition(tenant);
            const operations = [
                blockOperation,
                ...await index.createPutOperations(tenant, messageCid, indexes),
                tenantLog.createOperation({ type: 'put', key: Replication.encodePositionKey(seq), value: JSON.stringify(logEntry) }),
                tenantCidToSeq.createOperation({ type: 'put', key: messageCid, value: seq.toString() }),
                partitions.heads.createOperation({ type: 'put', key: tenant, value: seq.toString() }),
                ...await this.createFingerprintFoldOperations(partitions, tenant, messageCid, fingerprintScopes),
            ];
            await partitions.root.batch(operations);
            // Store-owned wake, post-commit, for `inserted` only.
            this.publishWake(tenant, seq);
            return {
                status: 'inserted',
                position: await this.buildToken(tenant, seq, messageCid),
            };
        });
    }
    async updateIndexes(tenant, messageCid, indexes, options) {
        options?.signal?.throwIfAborted();
        const partitions = await executeUnlessAborted(this.partitions(), options?.signal);
        const index = await executeUnlessAborted(this.index(), options?.signal);
        await this.withTenantWriteLock(tenant, async () => {
            const { entry, positionKey, tenantLog } = await this.getLogEntryForMutation(partitions, tenant, messageCid, DwnErrorCode.MessageStoreUpdateIndexesMessageNotFound);
            const storedMessage = await this.readStoredMessage(partitions, tenant, messageCid, DwnErrorCode.MessageStoreUpdateIndexesMessageNotFound, options);
            Replication.assertFingerprintScopesUntouched(entry.fingerprintScopes, storedMessage, messageCid, indexes);
            // Same row, same seq: indexes replaced; fingerprint scopes carried forward verbatim;
            // fingerprints and head stay untouched.
            const updatedEntry = { ...entry, indexes };
            const operations = [
                ...await index.createDeleteOperations(tenant, messageCid),
                ...await index.createPutOperations(tenant, messageCid, indexes),
                tenantLog.createOperation({ type: 'put', key: positionKey, value: JSON.stringify(updatedEntry) }),
            ];
            await partitions.root.batch(operations);
        });
    }
    async updateMessageAndIndexes(tenant, messageCid, message, indexes, options) {
        options?.signal?.throwIfAborted();
        const computedMessageCid = Cid.parseCid(await Message.getCid(message)).toString();
        const normalizedMessageCid = Cid.parseCid(messageCid).toString();
        if (computedMessageCid !== normalizedMessageCid) {
            throw new DwnError(DwnErrorCode.MessageStoreUpdateMessageAndIndexesCidMismatch, `replacement message CID ${computedMessageCid} does not match target CID ${normalizedMessageCid}`);
        }
        const partitions = await executeUnlessAborted(this.partitions(), options?.signal);
        const index = await executeUnlessAborted(this.index(), options?.signal);
        const encodedMessageBlock = await executeUnlessAborted(block.encode({ value: message, codec: cbor, hasher: sha256 }), options?.signal);
        await this.withTenantWriteLock(tenant, async () => {
            const { entry, positionKey, tenantLog } = await this.getLogEntryForMutation(partitions, tenant, normalizedMessageCid, DwnErrorCode.MessageStoreUpdateMessageAndIndexesMessageNotFound);
            Replication.assertFingerprintScopesUntouched(entry.fingerprintScopes, message, normalizedMessageCid, indexes);
            const updatedEntry = { ...entry, indexes };
            const tenantBlocks = await partitions.blocks.partition(tenant);
            const blockOperation = tenantBlocks.createOperation({ type: 'put', key: normalizedMessageCid, value: encodedMessageBlock.bytes });
            const operations = [
                blockOperation,
                ...await index.createDeleteOperations(tenant, normalizedMessageCid),
                ...await index.createPutOperations(tenant, normalizedMessageCid, indexes),
                tenantLog.createOperation({ type: 'put', key: positionKey, value: JSON.stringify(updatedEntry) }),
            ];
            await partitions.root.batch(operations);
        });
    }
    async delete(tenant, cidString, options) {
        options?.signal?.throwIfAborted();
        const partitions = await executeUnlessAborted(this.partitions(), options?.signal);
        const index = await executeUnlessAborted(this.index(), options?.signal);
        const messageCid = CID.parse(cidString).toString();
        await this.withTenantWriteLock(tenant, async () => {
            const tenantCidToSeq = await partitions.cidToSeq.partition(tenant);
            const seqString = await tenantCidToSeq.get(messageCid, options);
            if (seqString === undefined) {
                // Idempotent no-op — the row does not exist.
                return;
            }
            const seq = BigInt(seqString);
            const positionKey = Replication.encodePositionKey(seq);
            const tenantLog = await partitions.log.partition(tenant);
            const serializedEntry = await tenantLog.get(positionKey);
            if (serializedEntry === undefined) {
                throw new DwnError(DwnErrorCode.MessageStoreDeleteLogEntryMissing, `cid index for tenant ${tenant} points to missing log entry at seq ${seqString} (CID ${messageCid})`);
            }
            const entry = JSON.parse(serializedEntry);
            const tenantBlocks = await partitions.blocks.partition(tenant);
            const blockOperation = tenantBlocks.createOperation({ type: 'del', key: messageCid });
            // XOR is self-inverse: folding the persisted scopes again removes the row's contribution.
            const operations = [
                blockOperation,
                ...await index.createDeleteOperations(tenant, messageCid),
                tenantLog.createOperation({ type: 'del', key: positionKey }),
                tenantCidToSeq.createOperation({ type: 'del', key: messageCid }),
                ...await this.createFingerprintFoldOperations(partitions, tenant, messageCid, entry.fingerprintScopes),
            ];
            await partitions.root.batch(operations);
        });
    }
    /**
     * deletes everything in the underlying store, then persists a fresh epoch — a full local
     * reset after which no previously issued cursor is valid.
     */
    async clear() {
        const partitions = await this.partitions();
        await partitions.root.clear();
        this.epochPromise = undefined;
        await this.getEpoch();
    }
    // ---------------------------------------------------------------------------
    // ReplicationFeedReader
    // ---------------------------------------------------------------------------
    async logRead(tenant, options = {}) {
        const partitions = await this.partitions();
        const { cursor, limit, filters } = options;
        // Head-captured-first: the per-tenant write mutex serializes commits in position order, so an
        // observed head H is a visibility barrier — every position <= H is already committed when the
        // range scans below run, and anything committed afterward has a position > H and waits for
        // the next page.
        const head = await this.getHead(partitions, tenant);
        if (cursor !== undefined) {
            await this.validateCursor(partitions, tenant, cursor, head);
        }
        const startPosition = cursor === undefined ? 0n : BigInt(cursor.position);
        if (head === 0n) {
            // Nothing to scan — caught up at the input position.
            return { events: [], cursor, drained: true };
        }
        const maxResults = limit ?? Number.MAX_SAFE_INTEGER;
        if (maxResults <= 0) {
            return { events: [], cursor, drained: startPosition >= head };
        }
        if (startPosition >= head) {
            // Nothing to scan — caught up at the input position.
            return { events: [], cursor, drained: true };
        }
        const tenantLog = await partitions.log.partition(tenant);
        const tenantBlocks = await partitions.blocks.partition(tenant);
        const iteratorRange = { gt: Replication.encodePositionKey(startPosition), lte: Replication.encodePositionKey(head) };
        const logIterator = tenantLog.iterator(iteratorRange);
        const events = [];
        let drained = true;
        let lastScannedPosition = startPosition;
        let lastDeliveredPosition;
        let lastDeliveredMessageCid;
        for await (const [positionKey, serializedEntry] of logIterator) {
            const position = BigInt(positionKey);
            lastScannedPosition = position;
            const entry = JSON.parse(serializedEntry);
            const event = await this.readEventFromLogEntry(tenantBlocks, position, entry, filters);
            if (event === undefined) {
                continue;
            }
            events.push(event);
            lastDeliveredPosition = position;
            lastDeliveredMessageCid = event.messageCid;
            if (events.length >= maxResults) {
                drained = position >= head;
                break;
            }
        }
        // High-water cursor: the highest position scanned — head when the scan completed, the stop
        // position otherwise. `messageCid` is set only when the cursor position is a delivered row.
        const cursorPosition = drained ? head : lastScannedPosition;
        const cursorMessageCid = lastDeliveredPosition === cursorPosition ? lastDeliveredMessageCid : undefined;
        const resultCursor = await this.buildToken(tenant, cursorPosition, cursorMessageCid);
        return { events, cursor: resultCursor, drained };
    }
    async readEventFromLogEntry(tenantBlocks, position, entry, filters) {
        if (filters !== undefined && filters.length > 0 && !FilterUtility.matchAnyFilter(entry.indexes, filters)) {
            return undefined;
        }
        const bytes = await tenantBlocks.get(entry.messageCid);
        if (bytes === undefined) {
            // The row was deleted after the head capture; skip — its positions are gone with it.
            return undefined;
        }
        const decodedBlock = await block.decode({ bytes, codec: cbor, hasher: sha256 });
        const message = decodedBlock.value;
        return {
            seq: entry.seq,
            position: position.toString(),
            event: { message },
            indexes: entry.indexes,
            messageCid: entry.messageCid,
        };
    }
    async logBounds(tenant) {
        const partitions = await this.partitions();
        const head = await this.getHead(partitions, tenant);
        if (head === 0n) {
            return undefined;
        }
        // The log is the live set — replay from zero is always available, so the oldest resumable
        // position is always 0 regardless of compaction.
        const oldest = await this.buildToken(tenant, 0n);
        // Resolve the head position for the latest token's messageCid.
        const headKey = Replication.encodePositionKey(head);
        const tenantLog = await partitions.log.partition(tenant);
        let headMessageCid;
        const headLogEntry = await tenantLog.get(headKey);
        if (headLogEntry !== undefined) {
            headMessageCid = JSON.parse(headLogEntry).messageCid;
        }
        const latest = await this.buildToken(tenant, head, headMessageCid);
        return { oldest, latest };
    }
    async fingerprint(tenant, scopes) {
        const partitions = await this.partitions();
        const tenantFingerprints = await partitions.fingerprints.partition(tenant);
        let composed = Replication.emptyFingerprint();
        for (const scope of scopes) {
            const storedHex = await tenantFingerprints.get(MessageStoreLevel.fingerprintKey(scope));
            if (storedHex !== undefined) {
                composed = Replication.xorFingerprint(composed, Replication.hexToFingerprint(storedHex));
            }
        }
        return Replication.fingerprintToHex(composed);
    }
    // ---------------------------------------------------------------------------
    // Internals
    // ---------------------------------------------------------------------------
    /**
     * Serializes all log-mutating operations for a tenant: the lock spans seq assignment through
     * batch write, so commit order equals seq order (synchronous assignment alone is insufficient —
     * a later batch could land first).
     */
    async withTenantWriteLock(tenant, task) {
        const previous = this.writeLocks.get(tenant) ?? Promise.resolve();
        let release;
        const current = new Promise((resolve) => { release = resolve; });
        this.writeLocks.set(tenant, current);
        await previous;
        try {
            return await task();
        }
        finally {
            release();
            if (this.writeLocks.get(tenant) === current) {
                this.writeLocks.delete(tenant);
            }
        }
    }
    /**
     * Reads the tenant's counter high-water (the highest position ever issued), `0n` when unused.
     */
    async getHead(partitions, tenant) {
        const headString = await partitions.heads.get(tenant);
        return headString === undefined ? 0n : BigInt(headString);
    }
    async assertNoPreSubstrateLayout(partitions) {
        if (await partitions.meta.get(EPOCH_KEY) !== undefined) {
            return;
        }
        for await (const key of partitions.root.keys()) {
            const partition = MessageStoreLevel.parseSublevelPartition(key);
            if (partition === undefined || CURRENT_PARTITIONS.has(partition)) {
                continue;
            }
            throw new DwnError(DwnErrorCode.MessageStorePreSubstrateLayout, `message store location ${this.config.location} contains pre-substrate Level data; reset the store before opening it`);
        }
    }
    static parseSublevelPartition(key) {
        if (!key.startsWith('!')) {
            return undefined;
        }
        const end = key.indexOf('!', 1);
        return end === -1 ? undefined : key.slice(1, end);
    }
    async readStoredMessage(partitions, tenant, messageCid, notFoundErrorCode, options) {
        const tenantBlocks = await partitions.blocks.partition(tenant);
        const bytes = await tenantBlocks.get(messageCid, options);
        if (bytes === undefined) {
            throw new DwnError(notFoundErrorCode, `no message block found for tenant ${tenant} with CID ${messageCid}`);
        }
        const decodedBlock = await block.decode({ bytes, codec: cbor, hasher: sha256 });
        return decodedBlock.value;
    }
    /**
     * Validates a replication cursor against the tenant stream and persisted store epoch.
     * Throws `EventLogProgressGap` with bounds metadata when the cursor cannot be replayed.
     */
    async validateCursor(partitions, tenant, cursor, head) {
        const expectedStreamId = await Replication.deriveStreamId(tenant);
        const reason = await this.validateCursorPosition(partitions, tenant, cursor, head, expectedStreamId);
        if (reason === undefined) {
            return;
        }
        const bounds = await this.logBounds(tenant);
        const gapInfo = {
            requested: cursor,
            oldestAvailable: bounds?.oldest ?? cursor,
            latestAvailable: bounds?.latest ?? cursor,
            reason,
        };
        const error = new DwnError(DwnErrorCode.EventLogProgressGap, `progress token gap: ${reason}`);
        error.gapInfo = gapInfo;
        throw error;
    }
    async validateCursorPosition(partitions, tenant, cursor, head, expectedStreamId) {
        if (cursor.streamId !== expectedStreamId) {
            return 'stream_mismatch';
        }
        if (cursor.epoch !== await this.getEpoch()) {
            return 'epoch_mismatch';
        }
        const cursorPosition = BigInt(cursor.position);
        if (cursorPosition > head) {
            return 'token_too_new';
        }
        if (cursor.messageCid === undefined) {
            return undefined;
        }
        const positionMessageCid = await this.getMessageCidAtPosition(partitions, tenant, cursorPosition);
        if (positionMessageCid !== undefined && positionMessageCid !== cursor.messageCid) {
            return 'message_mismatch';
        }
        return undefined;
    }
    async getMessageCidAtPosition(partitions, tenant, position) {
        if (position <= 0n) {
            return undefined;
        }
        const positionKey = Replication.encodePositionKey(position);
        const tenantLog = await partitions.log.partition(tenant);
        const serializedEntry = await tenantLog.get(positionKey);
        if (serializedEntry !== undefined) {
            return JSON.parse(serializedEntry).messageCid;
        }
        return undefined;
    }
    /**
     * Resolves the log row for a same-CID mutation, throwing the given code when no row exists.
     */
    async getLogEntryForMutation(partitions, tenant, messageCid, notFoundErrorCode) {
        const tenantCidToSeq = await partitions.cidToSeq.partition(tenant);
        const seqString = await tenantCidToSeq.get(messageCid);
        if (seqString === undefined) {
            throw new DwnError(notFoundErrorCode, `no message found for tenant ${tenant} with CID ${messageCid}`);
        }
        const positionKey = Replication.encodePositionKey(BigInt(seqString));
        const tenantLog = await partitions.log.partition(tenant);
        const serializedEntry = await tenantLog.get(positionKey);
        if (serializedEntry === undefined) {
            throw new DwnError(notFoundErrorCode, `no log entry found for tenant ${tenant} at seq ${seqString} (CID ${messageCid})`);
        }
        return { entry: JSON.parse(serializedEntry), positionKey, tenantLog };
    }
    /**
     * Creates the batch operations that fold a message CID's contribution into the given
     * fingerprint domains. XOR is self-inverse, so the same operations serve insert and delete.
     */
    async createFingerprintFoldOperations(partitions, tenant, messageCid, scopes) {
        const contribution = await Replication.hashMessageCid(messageCid);
        const tenantFingerprints = await partitions.fingerprints.partition(tenant);
        const operations = [];
        for (const scope of scopes) {
            const key = MessageStoreLevel.fingerprintKey(scope);
            const storedHex = await tenantFingerprints.get(key);
            const current = storedHex === undefined ? Replication.emptyFingerprint() : Replication.hexToFingerprint(storedHex);
            const folded = Replication.xorFingerprint(current, contribution);
            operations.push(tenantFingerprints.createOperation({ type: 'put', key, value: Replication.fingerprintToHex(folded) }));
        }
        return operations;
    }
    /**
     * Encodes a fingerprint domain name as a Level key. The uniform prefix keeps the global
     * domain (the empty string) a valid key.
     */
    static fingerprintKey(scope) {
        return `d${scope}`;
    }
    async buildToken(tenant, position, messageCid) {
        const token = {
            streamId: await Replication.deriveStreamId(tenant),
            epoch: await this.getEpoch(),
            position: position.toString(),
        };
        if (messageCid !== undefined) {
            token.messageCid = messageCid;
        }
        return token;
    }
    /**
     * Publishes a wake post-commit. Best-effort by contract — never throws into the write path.
     */
    publishWake(tenant, position) {
        try {
            this.wakePublisher?.publish({ tenant, seq: position.toString() });
        }
        catch {
            // A lost wake only delays delivery; consumers' idle re-drain bounds the latency.
        }
    }
}
//# sourceMappingURL=message-store-level.js.map