/**
 * Streaming bitmap index builder with memory-bounded operation.
 *
 * Flushes bitmap data to storage when memory exceeds a threshold,
 * then merges multi-chunk shards at finalization. Delegates bitmap
 * accumulation to BitmapAccumulator.
 *
 * Shard format: plain CBOR via CodecPort. No envelopes, no checksums —
 * git-cas handles integrity at the storage layer.
 *
 * @module domain/services/index/StreamingBitmapIndexBuilder
 */
import defaultCodec from "../../utils/defaultCodec.js";
import nullLogger from "../../utils/nullLogger.js";
import { checkAborted } from "../../utils/cancellation.js";
import { canonicalStringify } from "../../utils/canonicalStringify.js";
import { textEncode } from "../../utils/bytes.js";
import { normalizeToAsyncIterable } from "../../utils/streamUtils.js";
import IndexError from "../../errors/IndexError.js";
import BitmapAccumulator from "./BitmapAccumulator.js";
/** Default memory threshold before flushing (50 MB). */
const DEFAULT_MAX_MEMORY_BYTES = 50 * 1024 * 1024;
export default class StreamingBitmapIndexBuilder {
    _storage;
    _codec;
    _logger;
    _maxMemoryBytes;
    _onFlush;
    _accumulator;
    _flushedChunks = new Map();
    _totalFlushedBytes = 0;
    _flushCount = 0;
    constructor(options) {
        const { storage, maxMemoryBytes } = StreamingBitmapIndexBuilder._validate(options);
        this._storage = storage;
        this._maxMemoryBytes = maxMemoryBytes;
        this._codec = options.codec ?? defaultCodec;
        this._logger = options.logger ?? nullLogger;
        this._onFlush = options.onFlush;
        this._accumulator = new BitmapAccumulator();
    }
    static _validate(options) {
        StreamingBitmapIndexBuilder._assertStorage(options.storage);
        const maxMem = options.maxMemoryBytes ?? DEFAULT_MAX_MEMORY_BYTES;
        StreamingBitmapIndexBuilder._assertMaxMemory(maxMem);
        return { storage: options.storage, maxMemoryBytes: maxMem };
    }
    static _assertStorage(storage) {
        if (storage === null
            || storage === undefined
            || typeof storage.writeBlobStream !== 'function'
            || typeof storage.readBlobStream !== 'function') {
            throw new IndexError('StreamingBitmapIndexBuilder requires a streaming storage adapter', { code: 'E_INDEX_INVALID_OPTIONS', context: { field: 'storage' } });
        }
    }
    static _assertMaxMemory(maxMem) {
        if (typeof maxMem !== 'number' || maxMem <= 0) {
            throw new IndexError('maxMemoryBytes must be a positive number', { code: 'E_INDEX_INVALID_OPTIONS', context: { field: 'maxMemoryBytes', value: maxMem } });
        }
    }
    /** Registers a node and returns its numeric ID. */
    registerNode(sha) {
        return Promise.resolve(this._accumulator.registerNode(sha));
    }
    /** Adds a directed edge. May trigger flush if memory threshold exceeded. */
    async addEdge(srcSha, tgtSha) {
        this._accumulator.addEdge(srcSha, tgtSha);
        if (this._accumulator.estimatedBitmapBytes >= this._maxMemoryBytes) {
            await this.flush();
        }
    }
    /** Flushes in-memory bitmaps to storage as CBOR shard blobs. */
    async flush() {
        if (this._accumulator.bitmapCount === 0) {
            return;
        }
        const flushedBytes = this._accumulator.estimatedBitmapBytes;
        const shards = this._accumulator.serializeBitmapsToShards();
        await this._writeShardsToStorage(shards);
        this._accumulator.clearBitmaps();
        this._totalFlushedBytes += flushedBytes;
        this._flushCount++;
        this._logger.debug('Flushed bitmap data', {
            operation: 'flush',
            flushedBytes,
            totalFlushedBytes: this._totalFlushedBytes,
            flushCount: this._flushCount,
        });
        if (this._onFlush) {
            this._onFlush({
                flushedBytes,
                totalFlushedBytes: this._totalFlushedBytes,
                flushCount: this._flushCount,
            });
        }
    }
    /** Returns current memory statistics for monitoring. */
    getMemoryStats() {
        return {
            estimatedBitmapBytes: this._accumulator.estimatedBitmapBytes,
            estimatedMappingBytes: this._accumulator.estimatedMappingBytes,
            totalFlushedBytes: this._totalFlushedBytes,
            flushCount: this._flushCount,
            nodeCount: this._accumulator.nodeCount,
            bitmapCount: this._accumulator.bitmapCount,
        };
    }
    /**
     * Finalizes the index: flush remaining data, merge chunks, write tree.
     * Returns the OID of the Git tree containing the complete index.
     */
    async finalize(options) {
        const { signal, frontier } = options ?? {};
        this._logger.debug('Finalizing index', {
            operation: 'finalize',
            nodeCount: this._accumulator.nodeCount,
            totalFlushedBytes: this._totalFlushedBytes,
            flushCount: this._flushCount,
        });
        const flatEntries = await this._buildAllEntries(signal);
        if (frontier) {
            await this._writeFrontierEntries(frontier, flatEntries);
        }
        return await this._writeAndLogTree(flatEntries);
    }
    async _buildAllEntries(signal) {
        checkAborted(signal, 'finalize');
        await this.flush();
        checkAborted(signal, 'finalize');
        const metaEntries = await this._writeMetaShards();
        checkAborted(signal, 'finalize');
        const bitmapEntries = this._processBitmapShards(signal);
        return [...metaEntries, ...bitmapEntries];
    }
    async _writeAndLogTree(flatEntries) {
        const treeOid = await this._storage.writeTree(flatEntries);
        this._logger.debug('Index finalized', {
            operation: 'finalize',
            treeOid,
            shardCount: flatEntries.length,
            nodeCount: this._accumulator.nodeCount,
        });
        return treeOid;
    }
    async _writeShardsToStorage(shards) {
        const tasks = [];
        for (const dir of ['fwd', 'rev']) {
            for (const [prefix, data] of Object.entries(shards[dir])) {
                const path = `shards_${dir}_${prefix}.cbor`;
                tasks.push((async () => {
                    const encoded = this._codec.encode(data);
                    const oid = await this._storage.writeBlobStream(normalizeToAsyncIterable(encoded), { slug: path, mime: 'application/cbor', size: encoded.length });
                    if (!this._flushedChunks.has(path)) {
                        this._flushedChunks.set(path, []);
                    }
                    this._flushedChunks.get(path).push(oid);
                })());
            }
        }
        await Promise.all(tasks);
    }
    async _writeMetaShards() {
        const entries = [];
        const chunkOrdinal = new Map();
        const chunkLimit = Math.max(1, Math.floor(this._maxMemoryBytes / 256));
        for (const { prefix, entries: shardEntries } of this._accumulator.iterateMetaShardChunks(chunkLimit)) {
            const chunkIndex = chunkOrdinal.get(prefix) ?? 0;
            const encoded = this._codec.encode(Object.fromEntries(shardEntries));
            const chunkPath = this._chunkPath(`meta_${prefix}.cbor`, chunkIndex);
            const oid = await this._storage.writeBlobStream(normalizeToAsyncIterable(encoded), { slug: chunkPath, mime: 'application/cbor', size: encoded.length });
            entries.push(`100644 blob ${oid}\t${chunkPath}`);
            chunkOrdinal.set(prefix, chunkIndex + 1);
        }
        return entries;
    }
    _processBitmapShards(signal) {
        const entries = [];
        for (const [path, oids] of this._flushedChunks.entries()) {
            checkAborted(signal, 'processBitmapShards');
            for (const [index, oid] of oids.entries()) {
                entries.push(`100644 blob ${oid}\t${this._chunkPath(path, index)}`);
            }
        }
        return entries;
    }
    _chunkPath(path, index) {
        const base = path.endsWith('.cbor') ? path.slice(0, -5) : path;
        return `${base}.chunk-${String(index).padStart(6, '0')}.cbor`;
    }
    async _writeFrontierEntries(frontier, entries) {
        const sorted = {};
        for (const key of Array.from(frontier.keys()).sort()) {
            sorted[key] = frontier.get(key);
        }
        const envelope = { version: 1, writerCount: frontier.size, frontier: sorted };
        const cborOid = await this._storage.writeBlob(this._codec.encode(envelope));
        entries.push(`100644 blob ${cborOid}\tfrontier.cbor`);
        const jsonOid = await this._storage.writeBlob(textEncode(canonicalStringify(envelope)));
        entries.push(`100644 blob ${jsonOid}\tfrontier.json`);
    }
}
