import QueryError from "../../errors/QueryError.js";
import TraversalOpticReadResult, { TraversalOpticCursor, } from "./TraversalOpticReadResult.js";
export default class CheckpointTailTraversalReader {
    _readNeighborhood;
    constructor(options) {
        this._readNeighborhood = options.readNeighborhood;
        Object.freeze(this);
    }
    async read(startNodeId, options) {
        const state = createTraversalRunState(startNodeId, options);
        if (isGoalAlreadyVisited(state)) {
            return goalAlreadyVisitedResult(state);
        }
        return await this._drainFrontier(state);
    }
    async _drainFrontier(state) {
        while (state.frontier.length > 0) {
            if (state.resultEdges.length >= state.traversal.maxEdges) {
                return openTraversalResult(state);
            }
            const current = state.frontier.shift();
            if (current === undefined) {
                continue;
            }
            const result = await this._expandFrontierEntry(state, current);
            if (result !== null) {
                return result;
            }
        }
        return traversalStateResult(state, drainedFrontierCompleteness(state));
    }
    async _expandFrontierEntry(state, current) {
        if (current.depth >= state.traversal.maxDepth) {
            return null;
        }
        const neighborhood = await this._readNeighborhoodForEntry(current, state.traversal, state.resultEdges);
        state.readIdentities.push(neighborhood.readIdentity);
        return processNeighborhoodRead(state, current, neighborhood);
    }
    async _readNeighborhoodForEntry(current, traversal, resultEdges) {
        const remainingEdgeBudget = traversal.maxEdges - resultEdges.length;
        return await this._readNeighborhood(current.nodeId, {
            direction: traversal.direction,
            labels: traversal.labels,
            limit: remainingEdgeBudget,
            ...(current.edgeCursor === null ? {} : { cursor: current.edgeCursor }),
        });
    }
}
function processNeighborhoodRead(state, current, neighborhood) {
    const depth = current.depth + 1;
    for (let edgeIndex = 0; edgeIndex < neighborhood.edges.length; edgeIndex += 1) {
        const edge = neighborhood.edges[edgeIndex];
        if (edge === undefined) {
            continue;
        }
        const boundaryResult = maybeOpenForNodeLimit({
            state,
            current,
            neighborId: edge.neighborId,
            edgeIndex,
        });
        if (boundaryResult !== null) {
            return boundaryResult;
        }
        state.resultEdges.push(traversalEdge(current.nodeId, edge, depth));
        const neighborResult = addTraversalNeighbor(state, edge.neighborId, depth);
        if (neighborResult !== null) {
            return neighborResult;
        }
    }
    return maybeOpenForNeighborhoodCursor(state, current, neighborhood.cursor);
}
function maybeOpenForNodeLimit(options) {
    if (options.state.visited.has(options.neighborId) || options.state.visited.size < options.state.traversal.maxNodes) {
        return null;
    }
    options.state.frontier.unshift(frontierEntry(options.current.nodeId, options.current.depth, blockedEdgeCursor(options.current, options.edgeIndex)));
    return openTraversalResult(options.state);
}
function addTraversalNeighbor(state, neighborId, depth) {
    if (state.visited.has(neighborId)) {
        return null;
    }
    state.visited.add(neighborId);
    if (state.traversal.goalNodeId === neighborId) {
        return traversalResult({
            traversal: state.traversal,
            edges: state.resultEdges,
            visited: state.visited,
            frontier: state.frontier,
            completeness: 'goal-found',
            readIdentities: state.readIdentities,
        });
    }
    if (depth < state.traversal.maxDepth) {
        state.frontier.push(frontierEntry(neighborId, depth, null));
    }
    return null;
}
function maybeOpenForNeighborhoodCursor(state, current, cursor) {
    if (cursor === null) {
        return null;
    }
    state.frontier.unshift(frontierEntry(current.nodeId, current.depth, cursor));
    return openTraversalResult(state);
}
function blockedEdgeCursor(current, edgeIndex) {
    return String(traversalEdgeCursorOffset(current.edgeCursor) + edgeIndex);
}
function traversalEdgeCursorOffset(cursor) {
    if (cursor === null || cursor.length === 0) {
        return 0;
    }
    const parsed = Number.parseInt(cursor, 10);
    if (!isCanonicalTraversalEdgeCursor(parsed, cursor)) {
        throw new QueryError('Traversal optic frontier edge cursor must be a non-negative integer string.', {
            code: 'E_OPTIC_TRAVERSAL_OPTIONS',
            context: { field: 'cursor.frontier.edgeCursor' },
        });
    }
    return parsed;
}
function isCanonicalTraversalEdgeCursor(parsed, raw) {
    return Number.isInteger(parsed) && parsed >= 0 && String(parsed) === raw;
}
function createTraversalRunState(startNodeId, options) {
    const traversal = normalizeTraversalOptions(startNodeId, options);
    return {
        traversal,
        visited: new Set(traversal.cursor?.visitedNodeIds ?? [startNodeId]),
        frontier: [...(traversal.cursor?.frontier ?? [frontierEntry(startNodeId, 0, null)])],
        resultEdges: [],
        readIdentities: [],
    };
}
function isGoalAlreadyVisited(state) {
    const { goalNodeId } = state.traversal;
    return goalNodeId !== null && state.visited.has(goalNodeId);
}
function goalAlreadyVisitedResult(state) {
    return traversalResult({
        traversal: state.traversal,
        edges: state.resultEdges,
        visited: state.visited,
        frontier: [],
        completeness: 'goal-found',
        readIdentities: state.readIdentities,
    });
}
function normalizeTraversalOptions(startNodeId, options) {
    const strategy = options.strategy ?? 'breadth-first';
    if (strategy !== 'breadth-first') {
        throw new QueryError('Traversal optic supports breadth-first strategy only.', {
            code: 'E_OPTIC_TRAVERSAL_OPTIONS',
            context: { field: 'strategy' },
        });
    }
    return {
        startNodeId,
        strategy,
        direction: normalizeDirection(options.direction),
        labels: normalizeLabels(options.labels ?? []),
        maxDepth: requireTraversalDepth(options.maxDepth),
        maxNodes: requirePositiveTraversalBound(options.maxNodes, 'maxNodes'),
        maxEdges: requirePositiveTraversalBound(options.maxEdges, 'maxEdges'),
        goalNodeId: options.goalNodeId ?? null,
        cursor: normalizeTraversalCursor(options.cursor),
    };
}
function normalizeDirection(direction) {
    if (direction === undefined) {
        return 'out';
    }
    if (direction === 'in' || direction === 'out' || direction === 'both') {
        return direction;
    }
    throw new QueryError('Traversal optic requires a valid direction.', {
        code: 'E_OPTIC_TRAVERSAL_OPTIONS',
        context: { field: 'direction' },
    });
}
function normalizeLabels(labels) {
    return Object.freeze([...new Set(labels)].sort());
}
function requirePositiveTraversalBound(value, field) {
    if (value === undefined) {
        throw new QueryError('Traversal optic requires explicit bounded traversal limits.', {
            code: 'E_OPTIC_TRAVERSAL_UNBOUNDED',
            context: { field, reason: 'requires-global-scan' },
        });
    }
    if (!Number.isInteger(value) || value < 1) {
        throw new QueryError('Traversal optic limits must be positive integers.', {
            code: 'E_OPTIC_TRAVERSAL_OPTIONS',
            context: { field },
        });
    }
    return value;
}
function requireTraversalDepth(value) {
    if (value === undefined) {
        throw new QueryError('Traversal optic requires explicit bounded traversal limits.', {
            code: 'E_OPTIC_TRAVERSAL_UNBOUNDED',
            context: { field: 'maxDepth', reason: 'requires-global-scan' },
        });
    }
    if (!Number.isInteger(value) || value < 0) {
        throw new QueryError('Traversal optic depth must be a non-negative integer.', {
            code: 'E_OPTIC_TRAVERSAL_OPTIONS',
            context: { field: 'maxDepth' },
        });
    }
    return value;
}
function normalizeTraversalCursor(cursor) {
    if (cursor === undefined) {
        return null;
    }
    if (cursor instanceof TraversalOpticCursor) {
        return cursor;
    }
    throw new QueryError('Traversal optic cursor must be a traversal cursor.', {
        code: 'E_OPTIC_TRAVERSAL_OPTIONS',
        context: { field: 'cursor' },
    });
}
function traversalResult(options) {
    const visitedNodeIds = [...options.visited];
    return new TraversalOpticReadResult({
        startNodeId: options.traversal.startNodeId,
        strategy: options.traversal.strategy,
        direction: options.traversal.direction,
        maxDepth: options.traversal.maxDepth,
        maxNodes: options.traversal.maxNodes,
        maxEdges: options.traversal.maxEdges,
        goalNodeId: options.traversal.goalNodeId,
        edges: options.edges,
        visitedNodeIds,
        frontier: options.frontier,
        completeness: options.completeness,
        cursor: traversalCursor(options.completeness, options.frontier, visitedNodeIds),
        readIdentities: options.readIdentities,
    });
}
function traversalStateResult(state, completeness) {
    return traversalResult({
        traversal: state.traversal,
        edges: state.resultEdges,
        visited: state.visited,
        frontier: state.frontier,
        completeness,
        readIdentities: state.readIdentities,
    });
}
function openTraversalResult(state) {
    return traversalStateResult(state, 'frontier-open');
}
function drainedFrontierCompleteness(state) {
    return state.traversal.goalNodeId === null ? 'complete' : 'goal-not-found-within-boundary';
}
function traversalCursor(completeness, frontier, visitedNodeIds) {
    if (completeness !== 'frontier-open') {
        return null;
    }
    return new TraversalOpticCursor({
        frontier,
        visitedNodeIds,
    });
}
function frontierEntry(nodeId, depth, edgeCursor) {
    return Object.freeze({ nodeId, depth, edgeCursor });
}
function traversalEdge(currentNodeId, edge, depth) {
    if (edge.direction === 'out') {
        return Object.freeze({
            fromNodeId: currentNodeId,
            toNodeId: edge.neighborId,
            label: edge.label,
            depth,
            expansionDirection: edge.direction,
        });
    }
    return Object.freeze({
        fromNodeId: edge.neighborId,
        toNodeId: currentNodeId,
        label: edge.label,
        depth,
        expansionDirection: edge.direction,
    });
}
