/**
 * QueryReads — graph read operations on materialized state.
 *
 * Functions that read nodes, edges, properties, and neighbors
 * from cached CRDT state. Extracted from QueryController.
 */
import { encodeEdgeKey, decodeEdgeKey, } from "../KeyCodec.js";
import EdgePropertyProjection from "../EdgePropertyProjection.js";
import { createSnapshotPropertyValues, createSnapshotPropValue, createSnapshotWarpState, } from "../ImmutableSnapshot.js";
import NodePropertyProjection from "../NodePropertyProjection.js";
import { isLegacyEdgePropertyProjectionTarget, isLegacyNodePropertyProjectionTarget, } from "../LegacyPropertyProjectionTarget.js";
import EdgeRecord from "../../graph/EdgeRecord.js";
import QueryError from "../../errors/QueryError.js";
// ── Neighbor helpers ────────────────────────────────────────────────
function tagDirection(edges, dir) {
    return edges.map((e) => ({ nodeId: e.neighborId, label: e.label, direction: dir }));
}
async function indexedBothNeighbors(provider, nodeId, opts) {
    const [outEdges, inEdges] = await Promise.all([
        provider.getNeighbors(nodeId, 'out', opts),
        provider.getNeighbors(nodeId, 'in', opts),
    ]);
    return [...tagDirection(outEdges, 'outgoing'), ...tagDirection(inEdges, 'incoming')];
}
async function indexedDirectionalNeighbors(params) {
    const dir = params.direction === 'outgoing' ? 'out' : 'in';
    const edges = await params.provider.getNeighbors(params.nodeId, dir, params.opts);
    return tagDirection(edges, params.direction);
}
function matchesLabel(label, filter) {
    return filter === undefined || label === filter;
}
function checkOutgoing(decoded, nodeId, state) {
    if (decoded.from !== nodeId || !state.nodeAlive.contains(decoded.to)) {
        return null;
    }
    return { nodeId: decoded.to, label: decoded.label, direction: 'outgoing' };
}
function checkIncoming(decoded, nodeId, state) {
    if (decoded.to !== nodeId || !state.nodeAlive.contains(decoded.from)) {
        return null;
    }
    return { nodeId: decoded.from, label: decoded.label, direction: 'incoming' };
}
function linearNeighborsForNode(state, nodeId, edgeLabel) {
    const out = [];
    const incoming = [];
    for (const edgeKey of state.edgeAlive.elements()) {
        const decoded = decodeEdgeKey(edgeKey);
        if (!matchesLabel(decoded.label, edgeLabel)) {
            continue;
        }
        const o = checkOutgoing(decoded, nodeId, state);
        if (o) {
            out.push(o);
        }
        const i = checkIncoming(decoded, nodeId, state);
        if (i) {
            incoming.push(i);
        }
    }
    return { out, in: incoming };
}
function filterByDirection(both, direction) {
    if (direction === 'outgoing') {
        return both.out;
    }
    if (direction === 'incoming') {
        return both.in;
    }
    return [...both.out, ...both.in];
}
// ── State access ────────────────────────────────────────────────────
async function ensureAndGetState(host) {
    await host._ensureFreshState();
    if (host._cachedState === null) {
        throw new QueryError('host state is null after _ensureFreshState', { code: 'E_NO_STATE' });
    }
    return host._cachedState;
}
// ── Read implementations ────────────────────────────────────────────
export async function hasNodeImpl(host, nodeId) {
    const state = await ensureAndGetState(host);
    return state.nodeAlive.contains(nodeId);
}
export async function getNodePropsImpl(host, nodeId) {
    await host._ensureFreshState();
    const indexed = await tryIndexedNodeProps(host, nodeId);
    if (indexed !== undefined) {
        return indexed;
    }
    if (host._cachedState === null) {
        return null;
    }
    return linearNodeProps(host._cachedState, nodeId);
}
function hasIndexForNode(host, nodeId) {
    return Boolean(host._propertyReader) && host._logicalIndex?.isAlive(nodeId) === true;
}
async function tryIndexedNodeProps(host, nodeId) {
    if (!hasIndexForNode(host, nodeId)) {
        return undefined;
    }
    try {
        const record = await host._propertyReader.getNodeProps(nodeId);
        if (record === null || record === undefined) {
            return undefined;
        }
        return createSnapshotPropertyValues(record);
    }
    catch {
        return undefined;
    }
}
function linearNodeProps(state, nodeId) {
    if (!isLegacyNodePropertyProjectionTarget(nodeId)) {
        return null;
    }
    const owner = state.getNodeRecord(nodeId);
    if (owner === null) {
        return null;
    }
    return nodePropertyBagFromRecords(NodePropertyProjection.forNodeRecord(state, owner));
}
function nodePropertyBagFromRecords(records) {
    const props = {};
    for (const record of records) {
        props[record.key.toString()] = createSnapshotPropValue(record.value.toPropValue());
    }
    return Object.freeze(props);
}
export async function getEdgePropsImpl(host, edge) {
    const state = await ensureAndGetState(host);
    return edgePropsFromState(state, edge);
}
function edgePropsFromState(state, edge) {
    if (!isLegacyEdgePropertyProjectionTarget(edge)) {
        return null;
    }
    const owner = state.getEdgeRecord(EdgeRecord.fromLegacyEdge(edge).id);
    if (owner === null) {
        return null;
    }
    return edgePropertyBagFromRecords(EdgePropertyProjection.forEdgeRecord(state, owner));
}
function edgePropertyBagFromRecords(records) {
    const props = {};
    for (const record of records) {
        props[record.key.toString()] = createSnapshotPropValue(record.value.toPropValue());
    }
    return Object.freeze(props);
}
export async function neighborsImpl(host, params) {
    await host._ensureFreshState();
    const indexed = await tryIndexedNeighbors(host, params);
    if (indexed !== undefined) {
        return indexed;
    }
    if (host._cachedState === null) {
        return [];
    }
    const both = linearNeighborsForNode(host._cachedState, params.nodeId, params.edgeLabel);
    return filterByDirection(both, params.direction);
}
function hasNeighborIndex(host, nodeId) {
    const provider = host._materializedGraph?.provider;
    if (!provider || host._logicalIndex?.isAlive(nodeId) !== true) {
        return null;
    }
    return provider;
}
async function tryIndexedNeighbors(host, params) {
    const provider = hasNeighborIndex(host, params.nodeId);
    if (!provider) {
        return undefined;
    }
    try {
        const opts = buildNeighborOpts(params.edgeLabel);
        if (params.direction === 'both') {
            return await indexedBothNeighbors(provider, params.nodeId, opts);
        }
        return await indexedDirectionalNeighbors({ provider, nodeId: params.nodeId, direction: params.direction, ...(opts !== undefined ? { opts } : {}) });
    }
    catch {
        return undefined;
    }
}
function buildNeighborOpts(edgeLabel) {
    if (typeof edgeLabel === 'string' && edgeLabel.length > 0) {
        return { labels: new Set([edgeLabel]) };
    }
    return undefined;
}
export async function getStateSnapshotImpl(host) {
    if (!host._cachedState && !host._autoMaterialize) {
        return null;
    }
    await host._ensureFreshState();
    if (!host._cachedState) {
        return null;
    }
    return createSnapshotWarpState(host._cachedState);
}
export async function getNodesImpl(host) {
    const state = await ensureAndGetState(host);
    return [...state.nodeAlive.elements()];
}
export async function getEdgesImpl(host) {
    const state = await ensureAndGetState(host);
    const edgeProps = buildEdgePropsByKey(state);
    return buildEdgeList(state, edgeProps);
}
function buildEdgePropsByKey(state) {
    const result = new Map();
    for (const record of EdgePropertyProjection.fromState(state)) {
        const edgeKey = encodeEdgeKey(record.owner.from.toString(), record.owner.to.toString(), record.owner.typeId.toString());
        addProjectedEdgePropEntry({ record, edgeKey, result });
    }
    return result;
}
function addProjectedEdgePropEntry(params) {
    const { record, edgeKey, result } = params;
    let bag = result.get(edgeKey);
    if (!bag) {
        bag = {};
        result.set(edgeKey, bag);
    }
    bag[record.key.toString()] = createSnapshotPropValue(record.value.toPropValue());
}
function buildEdgeList(state, edgeProps) {
    const edges = [];
    for (const edgeKey of state.edgeAlive.elements()) {
        const { from, to, label } = decodeEdgeKey(edgeKey);
        if (!state.nodeAlive.contains(from) || !state.nodeAlive.contains(to)) {
            continue;
        }
        const props = edgeProps.get(edgeKey) ?? {};
        edges.push({ from, to, label, props: Object.freeze(props) });
    }
    return edges;
}
export async function getPropertyCountImpl(host) {
    const state = await ensureAndGetState(host);
    return NodePropertyProjection.fromState(state).length + EdgePropertyProjection.fromState(state).length;
}
