/**
 * GraphTopology — topology and component algorithms.
 *
 * Contains: connectedComponent, topologicalSort, commonAncestors,
 * weightedLongestPath. Receives a BfsFn callback from the facade
 * to avoid circular imports.
 *
 * @module domain/services/query/GraphTopology
 */
import TraversalError from "../../errors/TraversalError.js";
import MinHeap from "../../utils/MinHeap.js";
import { checkAborted } from "../../utils/cancellation.js";
import { DEFAULT_MAX_NODES, DEFAULT_MAX_DEPTH, lexTieBreaker, stripUndefined, computeTopoHasCycle, } from "./TraversalContext.js";
export default class GraphTopology {
    _ctx;
    _bfs;
    constructor(ctx, bfs) {
        this._ctx = ctx;
        this._bfs = bfs;
    }
    async connectedComponent(params) {
        return await this._bfs(stripUndefined({
            start: params.start,
            direction: 'both',
            options: params.options,
            maxNodes: params.maxNodes,
            maxDepth: params.maxDepth,
            signal: params.signal,
        }));
    }
    async topologicalSort(params) {
        const direction = params.direction ?? 'out';
        const maxNodes = params.maxNodes ?? DEFAULT_MAX_NODES;
        const throwOnCycle = params.throwOnCycle ?? false;
        const { signal, options } = params;
        const _returnAdjList = params._returnAdjList ?? false;
        const _lightweight = params._lightweight ?? !_returnAdjList;
        const lightweight = _lightweight && !_returnAdjList;
        const rs = this._ctx.newRunStats();
        const starts = [...new Set(Array.isArray(params.start) ? params.start : [params.start])];
        for (const s of starts) {
            await this._ctx.validateStart(s);
        }
        // Phase 1: Discover all reachable nodes + compute in-degrees
        const adjList = lightweight ? null : new Map();
        const neighborEdgeMap = new Map();
        const inDegree = new Map();
        const discovered = new Set();
        const queue = [...starts];
        let qHead = 0;
        for (const s of starts) {
            discovered.add(s);
        }
        while (qHead < queue.length) {
            if (discovered.size % 1000 === 0) {
                checkAborted(signal, 'topologicalSort');
            }
            const nodeId = queue[qHead++];
            const neighbors = await this._ctx.getNeighbors(nodeId, direction, rs, options);
            rs.edgesTraversed += neighbors.length;
            for (const { neighborId } of neighbors) {
                inDegree.set(neighborId, (inDegree.get(neighborId) ?? 0) + 1);
                if (!discovered.has(neighborId)) {
                    discovered.add(neighborId);
                    queue.push(neighborId);
                }
            }
            if (adjList) {
                adjList.set(nodeId, neighbors.map(({ neighborId }) => neighborId));
            }
            if (_returnAdjList) {
                neighborEdgeMap.set(nodeId, neighbors);
            }
        }
        for (const s of starts) {
            if (!inDegree.has(s)) {
                inDegree.set(s, 0);
            }
        }
        const getNeighborIds = this._createTopoNeighborIdReader(adjList, direction, rs, options);
        // Phase 2: Kahn's — MinHeap for O(N log N) zero-indegree processing
        const ready = new MinHeap({ tieBreaker: lexTieBreaker });
        for (const nodeId of discovered) {
            if ((inDegree.get(nodeId) ?? 0) === 0) {
                ready.insert(nodeId, 0);
            }
        }
        const sorted = [];
        while (!ready.isEmpty() && sorted.length < maxNodes) {
            if (sorted.length % 1000 === 0) {
                checkAborted(signal, 'topologicalSort');
            }
            const nodeId = ready.extractMin();
            sorted.push(nodeId);
            const neighbors = await getNeighborIds(nodeId);
            for (const neighborId of neighbors) {
                const deg = inDegree.get(neighborId) - 1;
                inDegree.set(neighborId, deg);
                if (deg === 0) {
                    ready.insert(neighborId, 0);
                }
            }
        }
        const hasCycle = computeTopoHasCycle({
            sortedLength: sorted.length,
            discoveredSize: discovered.size,
            maxNodes,
            readyRemaining: !ready.isEmpty(),
        });
        if (hasCycle && throwOnCycle) {
            const cycleWitness = await this._findTopoCycleWitness({ discovered, sorted, getNeighborIds });
            throw new TraversalError('Graph contains a cycle', {
                code: 'ERR_GRAPH_HAS_CYCLES',
                context: {
                    nodesInCycle: discovered.size - sorted.length,
                    cycleWitness: typeof cycleWitness.from === 'string' && cycleWitness.from.length > 0 ? cycleWitness : undefined,
                },
            });
        }
        return {
            sorted,
            hasCycle,
            stats: this._ctx.stats(sorted.length, rs),
            ...(_returnAdjList ? { _neighborEdgeMap: neighborEdgeMap } : {}),
        };
    }
    async commonAncestors(params) {
        const { nodes, options, signal } = params;
        const maxDepth = params.maxDepth ?? DEFAULT_MAX_DEPTH;
        const maxResults = params.maxResults ?? 100;
        if (nodes.length === 0) {
            return { ancestors: [], stats: this._ctx.stats(0, this._ctx.newRunStats()) };
        }
        const ancestorCounts = new Map();
        const requiredCount = nodes.length;
        const totalStats = {
            nodesVisited: 0, edgesTraversed: 0, cacheHits: 0, cacheMisses: 0,
        };
        for (const nodeId of nodes) {
            checkAborted(signal, 'commonAncestors');
            const { nodes: ancestors, stats } = await this._bfs(stripUndefined({
                start: nodeId,
                direction: 'in',
                options,
                maxDepth,
                signal,
            }));
            totalStats.nodesVisited += stats.nodesVisited;
            totalStats.edgesTraversed += stats.edgesTraversed;
            totalStats.cacheHits += stats.cacheHits;
            totalStats.cacheMisses += stats.cacheMisses;
            for (const a of ancestors) {
                ancestorCounts.set(a, (ancestorCounts.get(a) ?? 0) + 1);
            }
        }
        const common = [];
        const entries = [...ancestorCounts.entries()]
            .filter(([, count]) => count === requiredCount)
            .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
        for (const [ancestor] of entries) {
            common.push(ancestor);
            if (common.length >= maxResults) {
                break;
            }
        }
        return { ancestors: common, stats: totalStats };
    }
    async weightedLongestPath(params) {
        const { start, goal, direction = 'out', options, signal } = params;
        const maxNodes = params.maxNodes ?? DEFAULT_MAX_NODES;
        const effectiveWeightFn = this._ctx.resolveWeightFn(params.weightFn, params.nodeWeightFn);
        await this._ctx.validateStart(start);
        const { sorted, _neighborEdgeMap } = await this.topologicalSort(stripUndefined({
            start, direction, options, maxNodes,
            throwOnCycle: true, signal,
            _returnAdjList: true,
        }));
        const rs = this._ctx.newRunStats();
        const dist = new Map([[start, 0]]);
        const prev = new Map();
        for (const nodeId of sorted) {
            if (!dist.has(nodeId)) {
                continue;
            }
            const neighbors = _neighborEdgeMap
                ? (_neighborEdgeMap.get(nodeId) ?? [])
                : await this._ctx.getNeighbors(nodeId, direction, rs, options);
            rs.edgesTraversed += neighbors.length;
            for (const { neighborId, label } of neighbors) {
                const w = await effectiveWeightFn(nodeId, neighborId, label);
                const alt = dist.get(nodeId) + w;
                const best = dist.get(neighborId) ?? -Infinity;
                if (alt > best || (alt === best && this._ctx.shouldUpdatePredecessor(prev, neighborId, nodeId))) {
                    dist.set(neighborId, alt);
                    prev.set(neighborId, nodeId);
                }
            }
        }
        if (!dist.has(goal)) {
            throw new TraversalError(`No path from ${start} to ${goal}`, {
                code: 'NO_PATH',
                context: { start, goal },
            });
        }
        const path = this._ctx.reconstructPath(prev, start, goal);
        return { path, totalCost: dist.get(goal), stats: this._ctx.stats(sorted.length, rs) };
    }
    // ── Topo-specific private helpers ──────────────────────────────────
    async _loadTopoNeighborIds(nodeId, direction, rs, options) {
        const neighbors = await this._ctx.getNeighbors(nodeId, direction, rs, options);
        rs.edgesTraversed += neighbors.length;
        return neighbors.map(({ neighborId }) => neighborId);
    }
    _createTopoNeighborIdReader(adjList, direction, rs, options) {
        if (adjList) {
            return (nodeId) => Promise.resolve(adjList.get(nodeId) ?? []);
        }
        return (nodeId) => this._loadTopoNeighborIds(nodeId, direction, rs, options);
    }
    async _findTopoCycleWitness(params) {
        const inSorted = new Set(params.sorted);
        for (const nodeId of params.discovered) {
            if (inSorted.has(nodeId)) {
                continue;
            }
            const neighbors = await params.getNeighborIds(nodeId);
            for (const neighborId of neighbors) {
                if (!inSorted.has(neighborId)) {
                    return { from: nodeId, to: neighborId };
                }
            }
        }
        return {};
    }
}
