import CheckpointTailBasisLoader from "./CheckpointTailBasisLoader.js";
import CheckpointShardFactReader from "./CheckpointShardFactReader.js";
import CheckpointTailFactReducer from "./CheckpointTailFactReducer.js";
import CheckpointTailReadFailure from "./CheckpointTailReadFailure.js";
import CheckpointTailReadIdentityBuilder from "./CheckpointTailReadIdentityBuilder.js";
import CheckpointTailTraversalReader from "./CheckpointTailTraversalReader.js";
import QueryError from "../../errors/QueryError.js";
import NodeOpticReadResult from "./NodeOpticReadResult.js";
import NodePropertyOpticReadResult from "./NodePropertyOpticReadResult.js";
import CheckpointTailWitnessScan from "./CheckpointTailWitnessScan.js";
import NeighborhoodOpticReadResult from "./NeighborhoodOpticReadResult.js";
import Optic from "./Optic.js";
import OpticReadTarget from "./OpticReadTarget.js";
import OpticSupportRule from "./OpticSupportRule.js";
const DEFAULT_MAX_TAIL_PATCHES = 10_000;
export default class CheckpointTailWitnessLocator {
    _graphName;
    _basisLoader;
    _shardReader;
    _factReducer;
    _readIdentityBuilder;
    _tailScan;
    constructor(options) {
        this._graphName = options.source.graphName;
        this._basisLoader = new CheckpointTailBasisLoader({ source: options.source });
        this._shardReader = new CheckpointShardFactReader({ source: options.source });
        this._factReducer = new CheckpointTailFactReducer({ graphName: options.source.graphName });
        this._readIdentityBuilder = new CheckpointTailReadIdentityBuilder({
            worldline: options.source.graphName,
        });
        this._tailScan = new CheckpointTailWitnessScan({
            source: options.source,
            maxTailPatches: options.maxTailPatches ?? DEFAULT_MAX_TAIL_PATCHES,
        });
        Object.freeze(this);
    }
    async readNode(optic) {
        requireOpticKind(optic, 'node');
        const nodeId = optic.nodeId();
        try {
            return await this._readNodeResult(nodeId);
        }
        catch (error) {
            if (error instanceof QueryError) {
                throw new CheckpointTailReadFailure({
                    graphName: this._graphName,
                    opticKind: 'node',
                    nodeId,
                }).enrich(error);
            }
            throw error;
        }
    }
    async readNodeProperty(optic) {
        requireOpticKind(optic, 'node-property');
        const nodeId = optic.nodeId();
        const propertyKey = optic.propertyKey();
        try {
            return await this._readNodePropertyResult(nodeId, propertyKey);
        }
        catch (error) {
            if (error instanceof QueryError) {
                throw new CheckpointTailReadFailure({
                    graphName: this._graphName,
                    opticKind: 'node-property',
                    nodeId,
                    propertyKey,
                }).enrich(error);
            }
            throw error;
        }
    }
    async readNeighborhood(optic, options) {
        requireOpticKind(optic, 'neighborhood');
        const nodeId = optic.nodeId();
        try {
            return await this._readNeighborhoodResult(nodeId, options);
        }
        catch (error) {
            if (error instanceof QueryError) {
                throw new CheckpointTailReadFailure({
                    graphName: this._graphName,
                    opticKind: 'neighborhood',
                    nodeId,
                }).enrich(error);
            }
            throw error;
        }
    }
    async readTraversal(optic, options) {
        requireOpticKind(optic, 'traversal');
        const startNodeId = optic.nodeId();
        try {
            requireExecutableTraversalSupport(optic, options);
            return await this._readTraversalResult(optic, options);
        }
        catch (error) {
            if (error instanceof QueryError) {
                throw new CheckpointTailReadFailure({
                    graphName: this._graphName,
                    opticKind: 'traversal',
                    nodeId: startNodeId,
                }).enrich(error);
            }
            throw error;
        }
    }
    async _readNodeResult(nodeId) {
        const basis = await this._basisLoader.load();
        const baseAlive = await this._shardReader.readNodeAlive(basis, nodeId);
        const tail = await this._scanTailForNode(basis, nodeId);
        const alive = this._factReducer.reduceNodeLiveness(baseAlive, tail.entries, nodeId);
        return new NodeOpticReadResult({
            nodeId,
            alive,
            readIdentity: this._readIdentityBuilder.nodeLiveness({
                basis,
                nodeId,
                checkpointIndexShards: this._shardReader.nodeLivenessShardIdentities(basis, nodeId),
                tailWitnesses: tail.witnesses,
            }),
        });
    }
    async _readNodePropertyResult(nodeId, propertyKey) {
        const basis = await this._basisLoader.load();
        const baseValue = await this._shardReader.readProperty(basis, nodeId, propertyKey);
        const tail = await this._scanTailForProperty(basis, nodeId, propertyKey);
        const value = this._factReducer.reduceProperty({
            baseValue,
            tailEntries: tail.entries,
            nodeId,
            propertyKey,
        });
        return new NodePropertyOpticReadResult({
            nodeId,
            key: propertyKey,
            value,
            readIdentity: this._readIdentityBuilder.nodeProperty({
                basis,
                nodeId,
                propertyKey,
                checkpointIndexShards: this._shardReader.propertyShardIdentities(basis, nodeId),
                tailWitnesses: tail.witnesses,
            }),
        });
    }
    async _readNeighborhoodResult(nodeId, options) {
        const direction = normalizeDirection(options.direction);
        const labels = normalizeLabels(options.labels ?? []);
        const basis = await this._basisLoader.load();
        const tail = await this._scanTailForNeighborhood(basis, { nodeId, direction, labels });
        this._factReducer.assertNeighborhoodTailStable(tail.entries);
        const allEdges = await this._shardReader.readNeighborhood(basis, { nodeId, direction, labels });
        const windowed = windowNeighborhoodEdges(allEdges, {
            cursor: options.cursor ?? null,
            limit: options.limit ?? null,
        });
        return neighborhoodReadResult({
            basis,
            direction,
            labels,
            nodeId,
            readIdentityBuilder: this._readIdentityBuilder,
            shardReader: this._shardReader,
            tail,
            windowed,
        });
    }
    async _readTraversalResult(optic, options) {
        return await this._traversalReaderFor(optic).read(optic.nodeId(), options);
    }
    _traversalReaderFor(optic) {
        return new CheckpointTailTraversalReader({
            readNeighborhood: async (nodeId, readOptions) => await this.readNeighborhood(optic.withTarget(OpticReadTarget.neighborhood(nodeId), OpticSupportRule.neighborhood()), readOptions),
        });
    }
    async _scanTailForNode(basis, nodeId) {
        return await this._tailScan.collect({
            basis,
            includeEntry: (entry) => this._factReducer.includesNodeLiveness(entry, nodeId),
        });
    }
    async _scanTailForProperty(basis, nodeId, propertyKey) {
        return await this._tailScan.collect({
            basis,
            includeEntry: (entry) => this._factReducer.includesProperty(entry, nodeId, propertyKey),
        });
    }
    async _scanTailForNeighborhood(basis, options) {
        return await this._tailScan.collect({
            basis,
            includeEntry: (entry) => this._factReducer.includesNeighborhood(entry, options),
        });
    }
}
function normalizeDirection(direction) {
    if (direction === undefined) {
        return 'out';
    }
    if (direction === 'in' || direction === 'out' || direction === 'both') {
        return direction;
    }
    throw new QueryError('Neighborhood optic requires a valid direction.', {
        code: 'E_OPTIC_NEIGHBORHOOD_OPTIONS',
        context: { field: 'direction' },
    });
}
function requireOpticKind(optic, opticKind) {
    if (!(optic instanceof Optic) || optic.target.opticKind !== opticKind) {
        throw new QueryError('Checkpoint-tail read requires a matching Optic.', {
            code: 'E_OPTIC_SCHEMA',
            context: { expectedOpticKind: opticKind },
        });
    }
}
function requireExecutableTraversalSupport(optic, options) {
    if (optic.supportRule.isTraversalWindow()) {
        return;
    }
    if (hasTraversalWindowOptions(options)) {
        throw new QueryError('Traversal optic support rule refuses bounded traversal execution.', {
            code: 'E_OPTIC_SCHEMA',
            context: {
                field: 'supportRule',
                supportRule: optic.supportRule.toString(),
                reason: 'requires-global-scan',
            },
        });
    }
    throw new QueryError('Traversal optic requires explicit bounded traversal limits.', {
        code: 'E_OPTIC_TRAVERSAL_UNBOUNDED',
        context: { field: 'supportRule', reason: 'requires-global-scan' },
    });
}
function hasTraversalWindowOptions(options) {
    return (options.maxDepth !== undefined
        && options.maxNodes !== undefined
        && options.maxEdges !== undefined);
}
function normalizeLabels(labels) {
    return Object.freeze([...new Set(labels)].sort());
}
function windowNeighborhoodEdges(edges, options) {
    const start = parseCursor(options.cursor);
    const limit = parseLimit(options.limit);
    if (limit === null) {
        return { edges: Object.freeze(edges.slice(start)), cursor: null };
    }
    const end = start + limit;
    const window = edges.slice(start, end);
    return {
        edges: Object.freeze(window),
        cursor: end < edges.length ? String(end) : null,
    };
}
function parseCursor(cursor) {
    if (cursor === null || cursor.length === 0) {
        return 0;
    }
    const parsed = Number.parseInt(cursor, 10);
    if (!isCanonicalCursor(parsed, cursor)) {
        throw new QueryError('Neighborhood optic cursor must be a non-negative integer string.', {
            code: 'E_OPTIC_NEIGHBORHOOD_OPTIONS',
            context: { field: 'cursor' },
        });
    }
    return parsed;
}
function isCanonicalCursor(parsed, raw) {
    return Number.isInteger(parsed) && parsed >= 0 && String(parsed) === raw;
}
function parseLimit(limit) {
    if (limit === null) {
        return null;
    }
    if (!Number.isInteger(limit) || limit < 1) {
        throw new QueryError('Neighborhood optic limit must be a positive integer.', {
            code: 'E_OPTIC_NEIGHBORHOOD_OPTIONS',
            context: { field: 'limit' },
        });
    }
    return limit;
}
function neighborhoodReadResult(options) {
    const readIdentity = neighborhoodReadIdentity(options);
    return new NeighborhoodOpticReadResult({
        nodeId: options.nodeId,
        direction: options.direction,
        edges: options.windowed.edges,
        completeness: options.windowed.cursor === null ? 'complete' : 'truncated',
        cursor: options.windowed.cursor,
        readIdentity,
    });
}
function neighborhoodReadIdentity(options) {
    return options.readIdentityBuilder.neighborhood({
        basis: options.basis,
        nodeId: options.nodeId,
        direction: options.direction,
        labels: options.labels,
        checkpointIndexShards: options.shardReader.neighborhoodShardIdentities(options.basis, {
            nodeId: options.nodeId,
            direction: options.direction,
            labels: options.labels,
        }),
        tailWitnesses: options.tail.witnesses,
    });
}
