import QueryError from "../../errors/QueryError.js";
import { textDecode, textEncode } from "../../utils/bytes.js";
import { deserializeFrontier } from "../Frontier.js";
import { partitionShardOids } from "../MaterializedViewHelpers.js";
import { isCurrentCheckpointSchema, partitionTreeOids, } from "../state/checkpointHelpers.js";
import CheckpointBasisManifest, { CheckpointBasisChunking, CheckpointBasisCompleteness, CheckpointBasisShardGeometry, CheckpointBasisShardRootMap, CheckpointBasisSupportPosture, } from "./CheckpointBasisManifest.js";
const CAS_POINTER_PREFIX = 'git-warp:cas-pointer:v1:';
const CAS_POINTER_PREFIX_BYTES = textEncode(CAS_POINTER_PREFIX);
export default class CheckpointTailBasisLoader {
    _source;
    constructor(options) {
        this._source = options.source;
        Object.freeze(this);
    }
    async load() {
        const checkpointSha = await this._readCheckpointSha();
        const checkpointMessage = await this._decodeCheckpointMessage(checkpointSha);
        if (!isCurrentCheckpointSchema(checkpointMessage.schema)) {
            throwNoBoundedBasis(this._source.graphName, 'checkpoint-without-index-tree');
        }
        const indexShardOids = await this._loadCheckpointIndexShardOids(checkpointMessage.indexOid);
        const frontierBytes = await this._readCheckpointPayloadBlob(checkpointMessage.frontierOid);
        const frontier = deserializeFrontier(frontierBytes, { codec: this._source._codec });
        const { indexOids, propOids } = partitionShardOids(indexShardOids);
        if (Object.keys(indexOids).length === 0 && Object.keys(propOids).length === 0) {
            throwNoBoundedBasis(this._source.graphName, 'checkpoint-missing-index-shards');
        }
        return {
            checkpointSha,
            schema: checkpointMessage.schema,
            frontier,
            manifest: createManifest({
                graphName: this._source.graphName,
                checkpointSha,
                frontier,
                schema: checkpointMessage.schema,
                indexOids,
                propOids,
            }),
            indexOids,
            propOids,
        };
    }
    async _readCheckpointSha() {
        const checkpointSha = await this._source._readCheckpointSha();
        if (checkpointSha === null) {
            throwNoBoundedBasis(this._source.graphName, 'missing-checkpoint');
        }
        return checkpointSha;
    }
    async _decodeCheckpointMessage(checkpointSha) {
        const commitMessage = await this._source._persistence.showNode(checkpointSha);
        return this._source._commitMessageCodec.decodeCheckpoint(commitMessage);
    }
    async _readCheckpointPayloadBlob(oid) {
        const bytes = await this._source._persistence.readBlob(oid);
        const storageOid = decodeCasPayloadPointer(bytes);
        if (storageOid === null) {
            return bytes;
        }
        if (this._source._blobStorage === null) {
            throwNoBoundedBasis(this._source.graphName, 'checkpoint-payload-pointer-without-storage');
        }
        return await this._source._blobStorage.retrieve(storageOid);
    }
    async _loadCheckpointIndexShardOids(checkpointTreeOid) {
        const rawTreeOids = await this._source._persistence.readTreeOids(checkpointTreeOid);
        const { treeOids, indexShardOids } = partitionTreeOids(rawTreeOids);
        if (Object.keys(indexShardOids).length > 0) {
            return indexShardOids;
        }
        const indexTreeOid = treeOids['index'];
        if (indexTreeOid === undefined) {
            return indexShardOids;
        }
        return await this._source._persistence.readTreeOids(indexTreeOid);
    }
}
function createManifest(options) {
    const roots = createManifestRoots(options.indexOids, options.propOids);
    const shardCount = manifestShardCount(roots);
    return new CheckpointBasisManifest({
        schema: options.schema,
        graphName: options.graphName,
        checkpointSha: options.checkpointSha,
        frontier: options.frontier,
        appliedVersionVector: appliedVersionVectorFromFrontier(options.frontier),
        basisIdentity: `basis:${options.graphName}:${options.checkpointSha}:checkpoint-tail-index`,
        semanticReadingIdentity: `reading-basis:${options.graphName}:${options.checkpointSha}:node-property-optics`,
        ...roots,
        provenancePosture: CheckpointBasisSupportPosture.unavailable('checkpoint-tail-provenance-root-unavailable'),
        contentAnchorPosture: CheckpointBasisSupportPosture.unavailable('checkpoint-tail-content-root-unavailable'),
        shardGeometry: checkpointShardGeometry(shardCount),
        chunking: checkpointChunking(shardCount),
        completeness: CheckpointBasisCompleteness.complete(),
    });
}
function createManifestRoots(indexOids, propOids) {
    return {
        livenessRoots: rootsForPrefix('node-liveness', indexOids, 'meta_'),
        propertyRoots: new CheckpointBasisShardRootMap({
            family: 'node-property',
            roots: shardOidMapToMap(propOids),
        }),
        outgoingAdjacencyRoots: rootsForPrefix('outgoing-adjacency', indexOids, 'fwd_'),
        incomingAdjacencyRoots: rootsForPrefix('incoming-adjacency', indexOids, 'rev_'),
        edgeFactRoots: edgeFactRootsFromIndex(indexOids),
    };
}
function manifestShardCount(roots) {
    return Math.max(1, roots.livenessRoots.size
        + roots.propertyRoots.size
        + roots.outgoingAdjacencyRoots.size
        + roots.incomingAdjacencyRoots.size
        + roots.edgeFactRoots.size);
}
function checkpointShardGeometry(shardCount) {
    return new CheckpointBasisShardGeometry({
        layoutFamily: 'checkpoint-tail-index-shards',
        payloadLayout: 'checkpoint-schema-5-index',
        shardKeyStrategy: 'hex-prefix-2',
        shardCount,
    });
}
function checkpointChunking(shardCount) {
    return new CheckpointBasisChunking({ maxFactsPerShard: shardCount, chunkCount: 1 });
}
function rootsForPrefix(family, source, prefix) {
    const roots = new Map();
    for (const [path, oid] of Object.entries(source)) {
        if (path.startsWith(prefix)) {
            roots.set(path, oid);
        }
    }
    return new CheckpointBasisShardRootMap({ family, roots });
}
function edgeFactRootsFromIndex(source) {
    const roots = new Map();
    for (const [path, oid] of Object.entries(source)) {
        if (!path.startsWith('meta_') && !path.startsWith('fwd_') && !path.startsWith('rev_')) {
            roots.set(path, oid);
        }
    }
    return new CheckpointBasisShardRootMap({ family: 'edge-fact', roots });
}
function shardOidMapToMap(source) {
    return new Map(Object.entries(source).sort(([left], [right]) => left.localeCompare(right)));
}
function appliedVersionVectorFromFrontier(frontier) {
    const versionVector = new Map();
    for (const writerId of [...frontier.keys()].sort()) {
        versionVector.set(writerId, 0);
    }
    return versionVector;
}
function decodeCasPayloadPointer(bytes) {
    if (!hasCasPointerPrefix(bytes)) {
        return null;
    }
    const decoded = textDecode(bytes);
    if (!decoded.startsWith(CAS_POINTER_PREFIX)) {
        return null;
    }
    const storageOid = decoded.slice(CAS_POINTER_PREFIX.length);
    if (storageOid.length === 0) {
        throwNoBoundedBasis('checkpoint', 'empty-checkpoint-payload-pointer');
    }
    return storageOid;
}
function hasCasPointerPrefix(bytes) {
    if (bytes.length < CAS_POINTER_PREFIX_BYTES.length) {
        return false;
    }
    for (let index = 0; index < CAS_POINTER_PREFIX_BYTES.length; index += 1) {
        if (bytes[index] !== CAS_POINTER_PREFIX_BYTES[index]) {
            return false;
        }
    }
    return true;
}
function throwNoBoundedBasis(graphName, reason) {
    throw new QueryError('No bounded checkpoint-tail optic basis is available.', {
        code: 'E_OPTIC_NO_BOUNDED_BASIS',
        context: { graphName, reason },
    });
}
