import defaultCodec from "../../utils/defaultCodec.js";
import BitmapIndexBuilder from "./BitmapIndexBuilder.js";
import BitmapIndexReader from "./BitmapIndexReader.js";
import StreamingBitmapIndexBuilder from "./StreamingBitmapIndexBuilder.js";
import { loadIndexFrontier, checkStaleness } from "./IndexStalenessChecker.js";
import nullLogger from "../../utils/nullLogger.js";
import { checkAborted } from "../../utils/cancellation.js";
import IndexError from "../../errors/IndexError.js";
function isStreamingStorage(storage) {
    return typeof storage.writeBlobStream === 'function'
        && typeof storage.readBlobStream === 'function';
}
/**
 * Service for building and loading the bitmap index from the graph.
 *
 * This service orchestrates index creation by walking the graph and persisting
 * the resulting bitmap shards to storage via the IndexStoragePort. The bitmap
 * index enables O(1) neighbor lookups (children/parents) after a one-time
 * O(N) rebuild cost.
 *
 * @module domain/services/index/IndexRebuildService
 * @see BitmapIndexBuilder
 * @see BitmapIndexReader
 * @see StreamingBitmapIndexBuilder
 */
export default class IndexRebuildService {
    graphService;
    storage;
    logger;
    _codec;
    constructor(options) {
        const { graphService, storage, logger = nullLogger, codec, crypto } = options ?? {};
        if (graphService === undefined || graphService === null) {
            throw new IndexError('IndexRebuildService requires a graphService', { code: 'E_INDEX_MISSING_GRAPH_SERVICE' });
        }
        if (storage === undefined || storage === null) {
            throw new IndexError('IndexRebuildService requires a storage adapter', { code: 'E_INDEX_MISSING_STORAGE' });
        }
        this.graphService = graphService;
        this.storage = storage;
        this.logger = logger;
        this._codec = codec ?? defaultCodec;
        void crypto; // reserved for future use
    }
    /**
     * Rebuilds the bitmap index by walking the graph from a ref.
     *
     * @returns OID of the created tree containing the index
     */
    async rebuild(ref, options = {}) {
        const { limit = 10_000_000, maxMemoryBytes, onFlush, onProgress, signal, frontier } = options;
        if (maxMemoryBytes !== undefined && maxMemoryBytes <= 0) {
            throw new IndexError('maxMemoryBytes must be a positive number', { code: 'E_INDEX_INVALID_MEMORY_LIMIT', context: { maxMemoryBytes } });
        }
        const mode = maxMemoryBytes !== undefined ? 'streaming' : 'in-memory';
        this.logger.info('Starting index rebuild', {
            operation: 'rebuild',
            ref,
            limit,
            mode,
            maxMemoryBytes: maxMemoryBytes ?? null,
        });
        let treeOid;
        if (maxMemoryBytes !== undefined) {
            // Build the required fields first, then attach optional ones only when present
            // to satisfy exactOptionalPropertyTypes.
            treeOid = await this._rebuildStreaming(ref, this._buildStreamOpts(limit, maxMemoryBytes, onFlush, onProgress, signal, frontier));
        }
        else {
            const memOpts = { limit };
            if (onProgress) {
                memOpts.onProgress = onProgress;
            }
            if (signal) {
                memOpts.signal = signal;
            }
            if (frontier) {
                memOpts.frontier = frontier;
            }
            treeOid = await this._rebuildInMemory(ref, memOpts);
        }
        this.logger.info('Index rebuild complete', {
            operation: 'rebuild',
            ref,
            mode,
            treeOid,
        });
        return treeOid;
    }
    _buildStreamOpts(limit, maxMemoryBytes, onFlush, onProgress, signal, frontier) {
        const base = { limit, maxMemoryBytes };
        if (onFlush !== undefined) {
            base.onFlush = onFlush;
        }
        if (onProgress !== undefined) {
            base.onProgress = onProgress;
        }
        if (signal !== undefined) {
            base.signal = signal;
        }
        if (frontier !== undefined) {
            base.frontier = frontier;
        }
        return base;
    }
    _requireStreamingStorage() {
        const { storage } = this;
        if (!isStreamingStorage(storage)) {
            throw new IndexError('streaming rebuild requires a streaming index storage adapter', { code: 'E_INDEX_STREAMING_STORAGE_REQUIRED' });
        }
        return storage;
    }
    async _rebuildInMemory(ref, options) {
        const { limit, onProgress, signal, frontier } = options;
        const builder = new BitmapIndexBuilder();
        let processedNodes = 0;
        for await (const node of this.graphService.iterateNodes({ ref, limit })) {
            builder.registerNode(node.sha);
            for (const parentSha of node.parents) {
                builder.addEdge(parentSha, node.sha);
            }
            processedNodes++;
            if (processedNodes % 10000 === 0) {
                checkAborted(signal, 'rebuild');
                if (onProgress) {
                    onProgress({ processedNodes, currentMemoryBytes: null });
                }
            }
        }
        return await this._persistIndex(builder, frontier ? { frontier } : {});
    }
    async _rebuildStreaming(ref, options) {
        const { limit, maxMemoryBytes, onFlush, onProgress, signal, frontier } = options;
        const streamOpts = {
            storage: this._requireStreamingStorage(),
            maxMemoryBytes,
        };
        if (onFlush) {
            streamOpts.onFlush = onFlush;
        }
        const builder = new StreamingBitmapIndexBuilder(streamOpts);
        let processedNodes = 0;
        for await (const node of this.graphService.iterateNodes({ ref, limit })) {
            await builder.registerNode(node.sha);
            for (const parentSha of node.parents) {
                await builder.addEdge(parentSha, node.sha);
            }
            processedNodes++;
            if (processedNodes % 10000 === 0) {
                checkAborted(signal, 'rebuild');
                if (onProgress) {
                    const stats = builder.getMemoryStats();
                    onProgress({
                        processedNodes,
                        currentMemoryBytes: stats.estimatedBitmapBytes,
                    });
                }
            }
        }
        const finalizeOpts = {};
        if (signal) {
            finalizeOpts.signal = signal;
        }
        if (frontier) {
            finalizeOpts.frontier = frontier;
        }
        return await builder.finalize(finalizeOpts);
    }
    async _persistIndex(builder, options) {
        const { frontier } = options ?? {};
        const treeStructure = builder.serialize(frontier ? { frontier } : {});
        const flatEntries = [];
        for (const [path, buffer] of Object.entries(treeStructure)) {
            const oid = await this.storage.writeBlob(buffer);
            flatEntries.push(`100644 blob ${oid}\t${path}`);
        }
        return await this.storage.writeTree(flatEntries);
    }
    /**
     * Loads a previously built index from a tree OID.
     *
     * @returns Configured reader ready for O(1) queries.
     */
    async load(treeOid, options = {}) {
        const { strict = true, currentFrontier, autoRebuild = false, rebuildRef } = options;
        this.logger.debug('Loading index', {
            operation: 'load',
            treeOid,
            strict,
        });
        if (autoRebuild && (rebuildRef === undefined || rebuildRef.length === 0)) {
            throw new IndexError('rebuildRef is required when autoRebuild is true', { code: 'E_INDEX_MISSING_REBUILD_REF' });
        }
        const shardOids = await this.storage.readTreeOids(treeOid);
        const shardCount = Object.keys(shardOids).length;
        // Staleness check
        if (currentFrontier) {
            const indexFrontier = await loadIndexFrontier(shardOids, this.storage, { codec: this._codec });
            if (indexFrontier) {
                const result = checkStaleness(indexFrontier, currentFrontier);
                if (result.stale) {
                    this.logger.warn('Index is stale', {
                        operation: 'load',
                        reason: result.reason,
                        hint: 'Rebuild the index or pass autoRebuild: true',
                    });
                    if (autoRebuild && rebuildRef !== undefined && rebuildRef.length > 0) {
                        const newTreeOid = await this.rebuild(rebuildRef, { frontier: currentFrontier });
                        return await this.load(newTreeOid, { strict });
                    }
                }
            }
            else {
                this.logger.debug('No frontier in index (legacy); skipping staleness check', {
                    operation: 'load',
                });
            }
        }
        const reader = new BitmapIndexReader({
            storage: this.storage,
            strict,
            logger: this.logger.child({ component: 'BitmapIndexReader' }),
        });
        reader.setup(shardOids);
        this.logger.debug('Index loaded', {
            operation: 'load',
            treeOid,
            shardCount,
        });
        return reader;
    }
}
