/**
 * TraversalContext — shared infrastructure for graph traversal algorithms.
 *
 * Holds the neighbor provider, LRU cache, logger, and exposes the common
 * operations (neighbor lookup, start validation, stats, weight resolution,
 * path reconstruction) that all algorithm modules need.
 *
 * One instance per GraphTraversal; algorithm modules receive it as a
 * constructor parameter.
 *
 * @module domain/services/query/TraversalContext
 */
import nullLogger from "../../utils/nullLogger.js";
import TraversalError from "../../errors/TraversalError.js";
import LRUCache from "../../utils/LRUCache.js";
// ── Constants ────────────────────────────────────────────────────────
export const DEFAULT_MAX_NODES = 100000;
export const DEFAULT_MAX_DEPTH = 1000;
export const DEFAULT_WEIGHT_FN = () => 1;
/**
 * Lexicographic nodeId comparator for MinHeap tie-breaking.
 */
export const lexTieBreaker = (a, b) => a < b ? -1 : a > b ? 1 : 0;
// ── Free functions ───────────────────────────────────────────────────
/**
 * Strips keys whose value is `undefined` from an object so that
 * `exactOptionalPropertyTypes` doesn't complain about explicit `undefined`.
 */
export function stripUndefined(obj) {
    const out = {}; // nosemgrep: ts-no-record-string-unknown-outside-adapters -- 0025B; nosemgrep: ts-no-unknown-outside-adapters -- 0025B
    for (const key of Object.keys(obj)) {
        if (obj[key] !== undefined) {
            out[key] = obj[key];
        }
    }
    return out;
}
/**
 * Distinguishes true topological cycles from maxNodes truncation.
 */
export function computeTopoHasCycle(params) {
    const stoppedByLimit = params.sortedLength >= params.maxNodes && params.readyRemaining;
    return !stoppedByLimit && params.sortedLength < params.discoveredSize;
}
// ── Class ────────────────────────────────────────────────────────────
export default class TraversalContext {
    provider;
    logger;
    _neighborCache;
    constructor(params) {
        this.provider = params.provider;
        this.logger = params.logger ?? nullLogger;
        this._neighborCache = params.provider.latencyClass === 'sync'
            ? null
            : new LRUCache(params.neighborCacheSize ?? 256);
    }
    newRunStats() {
        return { cacheHits: 0, cacheMisses: 0, edgesTraversed: 0 };
    }
    stats(nodesVisited, rs) {
        return {
            nodesVisited,
            edgesTraversed: rs.edgesTraversed,
            cacheHits: rs.cacheHits,
            cacheMisses: rs.cacheMisses,
        };
    }
    async getNeighbors(nodeId, direction, rs, options) {
        const cache = this._neighborCache;
        if (!cache) {
            return await this.provider.getNeighbors(nodeId, direction, options);
        }
        const labelsKey = options?.labels
            ? [...options.labels].sort().join('\0')
            : '*';
        const key = `${nodeId}\0${direction}\0${labelsKey}`;
        const cached = cache.get(key);
        if (cached !== undefined) {
            rs.cacheHits++;
            return cached;
        }
        rs.cacheMisses++;
        const result = await this.provider.getNeighbors(nodeId, direction, options);
        cache.set(key, result);
        return result;
    }
    async validateStart(nodeId) {
        const exists = await this.provider.hasNode(nodeId);
        if (!exists) {
            throw new TraversalError(`Start node '${nodeId}' does not exist in the graph`, {
                code: 'INVALID_START',
                context: { nodeId },
            });
        }
    }
    resolveWeightFn(weightFn, nodeWeightFn) {
        if (weightFn && nodeWeightFn) {
            throw new TraversalError('Cannot provide both weightFn and nodeWeightFn — they are mutually exclusive', { code: 'E_WEIGHT_FN_CONFLICT', context: {} });
        }
        if (nodeWeightFn) {
            return this._buildNodeWeightResolver(nodeWeightFn);
        }
        return weightFn ?? DEFAULT_WEIGHT_FN;
    }
    reconstructPath(predMap, start, goal) {
        const path = [goal];
        let current = goal;
        while (current !== start) {
            const pred = predMap.get(current);
            if (pred === undefined) {
                break;
            }
            path.push(pred);
            current = pred;
        }
        path.reverse();
        return path;
    }
    reconstructBiPath(fwdPrev, bwdNext, start, goal, meeting) {
        const fwdHalf = [meeting];
        let cur = meeting;
        while (cur !== start && fwdPrev.has(cur)) {
            cur = fwdPrev.get(cur);
            fwdHalf.push(cur);
        }
        fwdHalf.reverse();
        cur = meeting;
        while (cur !== goal && bwdNext.has(cur)) {
            cur = bwdNext.get(cur);
            fwdHalf.push(cur);
        }
        return fwdHalf;
    }
    shouldUpdatePredecessor(predMap, nodeId, candidatePred) {
        const current = predMap.get(nodeId);
        if (current === undefined) {
            return true;
        }
        return candidatePred < current;
    }
    _buildNodeWeightResolver(nodeWeightFn) {
        const cache = new Map();
        return (_from, to, _label) => {
            const cached = cache.get(to);
            if (cached !== undefined) {
                return cached;
            }
            const result = nodeWeightFn(to);
            if (typeof result === 'number') {
                cache.set(to, result);
                return result;
            }
            return result.then((v) => {
                cache.set(to, v);
                return v;
            });
        };
    }
}
