/** QueryRunner - pure executor for QueryPlan instances. */
import QueryError from "../../errors/QueryError.js";
import BoundedSupportRule from "./BoundedSupportRule.js";
import CausalIndexPlan from "./CausalIndexPlan.js";
import SupportFragmentPlan from "./SupportFragmentPlan.js";
import { runAggregate } from "./QueryAggregation.js";
// ── Batch concurrency ───────────────────────────────────────────────
async function batchMap(items, fn, limit = 100) {
    const results = [];
    for (let i = 0; i < items.length; i += limit) {
        const batch = items.slice(i, i + limit);
        const batchResults = await Promise.all(batch.map(fn));
        results.push(...batchResults);
    }
    return results;
}
// ── Snapshot construction ───────────────────────────────────────────
function sortIds(ids) {
    return [...ids].sort();
}
function buildPropsSnapshot(propsRecord) {
    const props = {};
    for (const key of Object.keys(propsRecord).sort()) {
        const value = propsRecord[key];
        if (value !== undefined) {
            props[key] = value;
        }
    }
    return Object.freeze(props);
}
function compareStrings(a, b) {
    if (a < b) {
        return -1;
    }
    if (a > b) {
        return 1;
    }
    return 0;
}
function edgePeer(edge, peerKey) {
    return peerKey === 'to' ? edge.to ?? '' : edge.from ?? '';
}
function compareEdgeEntries(a, b, peerKey) {
    const labelComparison = compareStrings(a.label, b.label);
    if (labelComparison !== 0) {
        return labelComparison;
    }
    return compareStrings(edgePeer(a, peerKey), edgePeer(b, peerKey));
}
async function collectNodeEdges(readModel, nodeId, direction) {
    const peerKey = direction === 'outgoing' ? 'to' : 'from';
    const list = [];
    for await (const edge of readModel.neighbors(nodeId, { direction })) {
        list.push(Object.freeze(peerKey === 'to'
            ? { label: edge.label, to: edge.nodeId }
            : { label: edge.label, from: edge.nodeId }));
    }
    list.sort((a, b) => compareEdgeEntries(a, b, peerKey));
    return Object.freeze(list);
}
async function createNodeSnapshot(readModel, nodeId) {
    const [propsRecord, edgesOut, edgesIn] = await Promise.all([
        readModel.nodeProps(nodeId),
        collectNodeEdges(readModel, nodeId, 'outgoing'),
        collectNodeEdges(readModel, nodeId, 'incoming'),
    ]);
    return Object.freeze({
        id: nodeId,
        props: buildPropsSnapshot(propsRecord ?? {}),
        edgesOut,
        edgesIn,
    });
}
// ── Traversal ───────────────────────────────────────────────────────
async function collectMatchingNeighbors(readModel, nodeId, options) {
    const result = [];
    for await (const entry of readModel.neighbors(nodeId, options)) {
        const neighbor = entry;
        result.push(neighbor.nodeId);
    }
    return result;
}
async function applyHop(params) {
    const next = new Set();
    for (const nodeId of params.strand) {
        const options = {
            direction: params.direction,
            ...(params.label !== undefined ? { label: params.label } : {}),
        };
        for (const id of await collectMatchingNeighbors(params.readModel, nodeId, options)) {
            next.add(id);
        }
    }
    return sortIds(next);
}
async function expandBfsLevel(params) {
    const nextLevel = new Set();
    for (const nodeId of params.currentLevel) {
        const neighbors = await collectMatchingNeighbors(params.readModel, nodeId, params.options);
        for (const id of neighbors) {
            if (!params.visited.has(id)) {
                params.visited.add(id);
                nextLevel.add(id);
            }
        }
    }
    return nextLevel;
}
function addAllTo(source, target) {
    for (const id of source) {
        target.add(id);
    }
}
async function runBfsLoop(params) {
    const [minDepth, maxDepth] = params.depth;
    const result = new Set();
    const visited = new Set(params.strand);
    let currentLevel = new Set(params.strand);
    if (minDepth === 0) {
        addAllTo(currentLevel, result);
    }
    for (let hop = 1; hop <= maxDepth; hop++) {
        currentLevel = await expandBfsLevel({
            currentLevel,
            visited,
            readModel: params.readModel,
            options: params.options,
        });
        if (hop >= minDepth) {
            addAllTo(currentLevel, result);
        }
        if (currentLevel.size === 0) {
            break;
        }
    }
    return result;
}
async function applyMultiHop(params) {
    const options = {
        direction: params.direction,
        ...(params.label !== undefined ? { label: params.label } : {}),
    };
    const result = await runBfsLoop({
        readModel: params.readModel,
        options,
        strand: params.strand,
        depth: params.depth,
    });
    return sortIds(result);
}
async function applyWhereOp(params) {
    const { strand, predicate, readModel } = params;
    const snapshots = await batchMap(strand, async (nodeId) => ({
        nodeId,
        snapshot: await createNodeSnapshot(readModel, nodeId),
    }));
    return sortIds(snapshots
        .filter(({ snapshot }) => predicate(snapshot))
        .map(({ nodeId }) => nodeId));
}
async function applyTraversalOp(strand, op, readModel) {
    const [minD, maxD] = op.depth;
    if (minD === 1 && maxD === 1) {
        return await applyHop({
            direction: op.type,
            label: op.label,
            strand,
            readModel,
        });
    }
    return await applyMultiHop({
        direction: op.type,
        label: op.label,
        strand,
        readModel,
        depth: op.depth,
    });
}
async function applyOperations(params) {
    const { operations, readModel } = params;
    let current = params.strand;
    for (const op of operations) {
        if (op.type === 'where') {
            current = await applyWhereOp({
                strand: current,
                predicate: op.fn,
                readModel,
            });
        }
        else {
            current = await applyTraversalOp(current, op, readModel);
        }
    }
    return current;
}
// ── Select + result building ────────────────────────────────────────
const ALLOWED_FIELDS = new Set(['id', 'props']);
function assertAllFieldsKnown(fields) {
    for (const field of fields) {
        if (!ALLOWED_FIELDS.has(field)) {
            throw new QueryError(`Unknown select field: ${field}`, {
                code: 'E_QUERY_SELECT_FIELD',
                context: { field },
            });
        }
    }
}
function validateSelectFields(select) {
    if (select === null || select.length === 0) {
        return null;
    }
    const fields = select.slice();
    assertAllFieldsKnown(fields);
    return fields;
}
async function buildResultNodes(strand, selectFields, getProps) {
    const includeId = !selectFields || selectFields.includes('id');
    const includeProps = !selectFields || selectFields.includes('props');
    return await batchMap(strand, async (nodeId) => {
        const entry = {};
        if (includeId) {
            entry.id = nodeId;
        }
        if (includeProps) {
            const props = buildPropsSnapshot(await getProps(nodeId));
            if (selectFields !== null || Object.keys(props).length > 0) {
                entry.props = props;
            }
        }
        return entry;
    });
}
// ── Runner ──────────────────────────────────────────────────────────
const DEFAULT_PATTERN = '*';
function createPropsMemo(readModel) {
    const memo = new Map();
    return async (nodeId) => {
        const cached = memo.get(nodeId);
        if (cached !== undefined) {
            return cached;
        }
        const raw = await readModel.nodeProps(nodeId);
        const record = raw ?? {};
        memo.set(nodeId, record);
        return record;
    };
}
function isSingleExactPattern(pattern) {
    return typeof pattern === 'string' && !pattern.includes('*');
}
async function collectInitialStrand(readModel, request) {
    const strand = [];
    const nodes = readModel.nodes(request);
    for await (const node of nodes) {
        strand.push(node.id);
        if (isSingleExactPattern(request.pattern)) {
            break;
        }
    }
    return sortIds(strand);
}
export default class QueryRunner {
    #provider;
    constructor(provider) {
        this.#provider = provider;
    }
    async run(plan) {
        const selectFields = validateSelectFields(plan.select);
        const request = {
            pattern: plan.pattern ?? DEFAULT_PATTERN,
            select: selectFields,
        };
        const readModel = await this.#provider.openQueryReadModel(openRequest(plan, request));
        const getProps = createPropsMemo(readModel);
        const matched = await collectInitialStrand(readModel, request);
        const strand = await applyOperations({
            strand: matched,
            operations: plan.operations,
            readModel,
        });
        if (plan.aggregate) {
            return await runAggregate({
                strand,
                stateHash: readModel.stateHash,
                getProps,
                spec: plan.aggregate,
            });
        }
        const nodes = await buildResultNodes(strand, selectFields, getProps);
        return { stateHash: readModel.stateHash, nodes };
    }
}
function openRequest(plan, nodeRequest) {
    const supportRule = BoundedSupportRule.fromQueryPlan(plan);
    const causalIndexPlan = CausalIndexPlan.fromSupportRule(supportRule);
    return {
        nodeRequest,
        operations: plan.operations,
        aggregate: plan.aggregate !== null,
        supportRule,
        causalIndexPlan,
        supportFragmentPlan: SupportFragmentPlan.fromSupportAndIndex({
            supportRule,
            causalIndexPlan,
        }),
    };
}
