import { IndexError, ShardLoadError, ShardCorruptionError } from "../../errors/index.js";
import defaultCodec from "../../utils/defaultCodec.js";
import nullLogger from "../../utils/nullLogger.js";
import LRUCache from "../../utils/LRUCache.js";
import { getRoaringBitmap32 } from "../../utils/roaring.js";
import { isValidShardOid } from "../../utils/validateShardOid.js";
/** Default maximum number of shards to cache. */
const DEFAULT_MAX_CACHED_SHARDS = 100;
const LARGE_ID_CACHE_WARNING_THRESHOLD = 1_000_000;
const ESTIMATED_ID_CACHE_ENTRY_BYTES = 40;
function isNonEmptyString(value) {
    return typeof value === 'string' && value.length > 0;
}
function isPlainLoadedShard(value) {
    return value !== null
        && value !== undefined
        && typeof value === 'object'
        && !Array.isArray(value)
        && Object.getPrototypeOf(value) === Object.prototype;
}
function isMetaShardPath(path) {
    return /^meta_[0-9a-f]{2}(?:\.chunk-\d{6})?\.cbor$/.test(path);
}
function isChunkedVariant(path, basePath) {
    const base = basePath.endsWith('.cbor') ? basePath.slice(0, -5) : basePath;
    return path.startsWith(`${base}.chunk-`) && path.endsWith('.cbor');
}
/**
 * Service for querying a loaded bitmap index.
 *
 * Provides O(1) lookups for parent/child relationships by lazily loading
 * sharded CBOR bitmap data from storage. Shards are cached after first access.
 *
 * Shard format: plain CBOR (no envelopes). git-cas handles integrity
 * at the storage layer.
 *
 * @example
 * const reader = new BitmapIndexReader({ storage });
 * reader.setup(shardOids);
 * const parents = await reader.getParents('abc123...');
 */
export default class BitmapIndexReader {
    storage;
    strict;
    logger;
    _codec;
    maxCachedShards;
    shardOids;
    loadedShards;
    _idToShaCache;
    constructor(options) {
        const { storage, strict = true, logger = nullLogger, maxCachedShards = DEFAULT_MAX_CACHED_SHARDS, codec } = options;
        BitmapIndexReader._assertStorage(storage);
        this.storage = storage;
        this.strict = strict;
        this.logger = logger;
        this._codec = codec ?? defaultCodec;
        this.maxCachedShards = maxCachedShards;
        this.shardOids = new Map();
        this.loadedShards = new LRUCache(maxCachedShards);
        this._idToShaCache = null;
    }
    static _assertStorage(storage) {
        if (storage === null || storage === undefined) {
            throw new IndexError('BitmapIndexReader requires a storage adapter', {
                code: 'E_INDEX_STORAGE_REQUIRED',
            });
        }
    }
    /**
     * Configures the reader with shard OID mappings for lazy loading.
     */
    setup(shardOids) {
        const entries = Object.entries(shardOids);
        const validEntries = [];
        for (const [path, oid] of entries) {
            if (isValidShardOid(oid)) {
                validEntries.push([path, oid]);
            }
            else if (this.strict) {
                throw new ShardCorruptionError('Invalid shard OID', {
                    shardPath: path,
                    oid,
                    reason: 'invalid_oid',
                });
            }
            else {
                this.logger.warn('Skipping shard with invalid OID', {
                    operation: 'setup',
                    shardPath: path,
                    oid,
                    reason: 'invalid_oid',
                });
            }
        }
        this.shardOids = new Map(validEntries);
        this._idToShaCache = null;
        this.loadedShards.clear();
    }
    /**
     * Looks up the numeric ID for a SHA.
     */
    async lookupId(sha) {
        const prefix = sha.substring(0, 2);
        const path = `meta_${prefix}.cbor`;
        for (const actualPath of this._resolveShardPaths(path)) {
            const idMap = await this._getOrLoadShard(actualPath);
            const id = idMap[sha];
            if (typeof id === 'number') {
                return id;
            }
        }
        return undefined;
    }
    /**
     * Gets parent SHAs for a node (O(1) via reverse bitmap).
     */
    async getParents(sha) {
        return await this._getEdges(sha, 'rev');
    }
    /**
     * Gets child SHAs for a node (O(1) via forward bitmap).
     */
    async getChildren(sha) {
        return await this._getEdges(sha, 'fwd');
    }
    async _getEdges(sha, type) {
        const prefix = sha.substring(0, 2);
        const shardPath = `shards_${type}_${prefix}.cbor`;
        const neighborIds = new Set();
        for (const actualPath of this._resolveShardPaths(shardPath)) {
            const shard = await this._getOrLoadShard(actualPath);
            const bitmapBytes = shard[sha];
            if (bitmapBytes === undefined || bitmapBytes === null) {
                continue;
            }
            if (!(bitmapBytes instanceof Uint8Array)) {
                this._handleInvalidBitmapValue(actualPath, sha);
                continue;
            }
            if (bitmapBytes.length === 0) {
                continue;
            }
            const ids = this._deserializeBitmapIds(bitmapBytes, actualPath);
            for (const id of ids) {
                neighborIds.add(id);
            }
        }
        const idToSha = await this._buildIdToShaMapping();
        const result = [];
        for (const id of neighborIds) {
            const neighbor = idToSha[id];
            if (typeof neighbor === 'string') {
                result.push(neighbor);
            }
        }
        return result;
    }
    _deserializeBitmapIds(bitmapBytes, shardPath) {
        try {
            const RoaringBitmap32 = getRoaringBitmap32();
            const bitmap = RoaringBitmap32.deserialize(bitmapBytes, true);
            return bitmap.toArray();
        }
        catch (err) {
            const oid = this.shardOids.get(shardPath);
            const shardOid = isNonEmptyString(oid) ? oid : shardPath;
            const corruptionError = new ShardCorruptionError('Failed to deserialize bitmap', {
                shardPath,
                oid: shardOid,
                reason: 'bitmap_deserialize_error',
                context: { originalError: err instanceof Error ? err.message : String(err) },
            });
            if (this.strict) {
                throw corruptionError;
            }
            this.logger.warn('Bitmap deserialization failed', {
                operation: 'deserializeBitmap',
                shardPath,
                oid: shardOid,
            });
            return [];
        }
    }
    async _buildIdToShaMapping() {
        if (this._idToShaCache !== null) {
            return this._idToShaCache;
        }
        const cache = [];
        this._idToShaCache = cache;
        for (const [path] of this.shardOids) {
            if (!isMetaShardPath(path)) {
                continue;
            }
            const shard = await this._getOrLoadShard(path);
            for (const [sha, id] of Object.entries(shard)) {
                cache[id] = sha;
            }
        }
        this._warnLargeIdCache(cache.length);
        return cache;
    }
    _warnLargeIdCache(entryCount) {
        if (entryCount < LARGE_ID_CACHE_WARNING_THRESHOLD) {
            return;
        }
        this.logger.warn('ID-to-SHA cache has high memory usage', {
            operation: '_buildIdToShaMapping',
            entryCount,
            estimatedMemoryBytes: entryCount * ESTIMATED_ID_CACHE_ENTRY_BYTES,
        });
    }
    async _getOrLoadShard(path) {
        const cached = this.loadedShards.get(path);
        if (cached !== undefined) {
            return cached;
        }
        const oid = this.shardOids.get(path);
        if (!isNonEmptyString(oid)) {
            return {};
        }
        const buffer = await this._loadShardBuffer(path, oid);
        return this._decodeAndCacheShard(buffer, path, oid);
    }
    _resolveShardPaths(basePath) {
        const exact = this.shardOids.has(basePath) ? [basePath] : [];
        const chunked = Array.from(this.shardOids.keys())
            .filter((path) => isChunkedVariant(path, basePath))
            .sort();
        return [...exact, ...chunked];
    }
    _decodeAndCacheShard(buffer, path, oid) {
        try {
            const data = this._codec.decode(buffer);
            if (!isPlainLoadedShard(data)) {
                return this._handleInvalidShardShape(path, oid, 'shard_not_object');
            }
            this.loadedShards.set(path, data);
            return data;
        }
        catch (err) {
            return this._handleDecodeError(err, path, oid);
        }
    }
    _handleDecodeError(err, path, oid) {
        const corruptionError = new ShardCorruptionError('Failed to decode shard', {
            shardPath: path,
            oid,
            reason: 'decode_error',
            context: { originalError: err instanceof Error ? err.message : String(err) },
        });
        if (this.strict) {
            throw corruptionError;
        }
        this.logger.warn('Shard decode failed', {
            operation: 'loadShard',
            shardPath: path,
            oid,
        });
        return {};
    }
    _handleInvalidShardShape(path, oid, reason) {
        const corruptionError = new ShardCorruptionError('Invalid shard shape', {
            shardPath: path,
            oid,
            reason,
        });
        if (this.strict) {
            throw corruptionError;
        }
        this.logger.warn('Shard shape invalid', {
            operation: 'loadShard',
            shardPath: path,
            oid,
            reason,
        });
        return {};
    }
    _handleInvalidBitmapValue(path, sha) {
        const oid = this.shardOids.get(path);
        const shardOid = isNonEmptyString(oid) ? oid : path;
        const corruptionError = new ShardCorruptionError('Invalid bitmap value', {
            shardPath: path,
            oid: shardOid,
            reason: 'bitmap_value_not_bytes',
            context: { sha },
        });
        if (this.strict) {
            throw corruptionError;
        }
        this.logger.warn('Bitmap value invalid', {
            operation: 'deserializeBitmap',
            shardPath: path,
            oid: shardOid,
            reason: 'bitmap_value_not_bytes',
            sha,
        });
    }
    async _loadShardBuffer(path, oid) {
        try {
            return await this.storage.readBlob(oid);
        }
        catch (cause) {
            throw new ShardLoadError('Failed to load shard from storage', {
                shardPath: path,
                oid,
                cause: cause,
            });
        }
    }
}
