/**
 * ComparisonEngine — coordinate comparison and transfer planning logic.
 *
 * Contains the implementations for compareCoordinates, compareStrand,
 * planCoordinateTransfer, and planStrandTransfer. All functions are
 * free (not methods) — the ComparisonController facade delegates here.
 *
 * @module domain/services/controllers/ComparisonEngine
 */
import QueryError from "../../errors/QueryError.js";
import { computeChecksum } from "../../utils/checksumUtils.js";
import { buildCoordinateComparisonFact, buildCoordinateTransferPlanFact, } from "../CoordinateFactExport.js";
import { createStateReader } from "../state/StateReader.js";
import GraphDiff from "../comparison/GraphDiff.js";
import { compareVisibleState } from "../comparison/VisibleStateComparison.js";
import { planVisibleStateTransfer } from "../transfer/VisibleStateTransferPlanner.js";
import { normalizeVisibleStateScope } from "../VisibleStateScope.js";
import { normalizeSelector, normalizeRequiredString, normalizeLamportCeiling, normalizeOptionalString, uniqueSortedPatchShas, compareStrings, } from "./ComparisonSelector.js";
const COORDINATE_COMPARISON_VERSION = 'coordinate-compare/v1';
const COORDINATE_TRANSFER_PLAN_VERSION = 'coordinate-transfer-plan/v1';
// ── Divergence ───────────────────────────────────────────────────────
function patchTouchesEntity(patch, entityId) {
    const reads = Array.isArray(patch.reads) ? patch.reads : [];
    const writes = Array.isArray(patch.writes) ? patch.writes : [];
    return reads.includes(entityId) || writes.includes(entityId);
}
function targetPatchShas(entries, targetId) {
    const filtered = entries.filter(({ patch }) => patchTouchesEntity(patch, targetId));
    return [...new Set(filtered.map(({ sha }) => sha))].sort(compareStrings);
}
function buildTargetDivergence(leftEntries, rightEntries, targetId) {
    const leftTarget = targetPatchShas(leftEntries, targetId);
    const rightTarget = targetPatchShas(rightEntries, targetId);
    const rightTargetSet = new Set(rightTarget);
    const leftTargetSet = new Set(leftTarget);
    return {
        targetId,
        leftCount: leftTarget.length,
        rightCount: rightTarget.length,
        sharedCount: leftTarget.filter((sha) => rightTargetSet.has(sha)).length,
        leftOnlyCount: leftTarget.filter((sha) => !rightTargetSet.has(sha)).length,
        rightOnlyCount: rightTarget.filter((sha) => !leftTargetSet.has(sha)).length,
        leftOnlyPatchShas: leftTarget.filter((sha) => !rightTargetSet.has(sha)),
        rightOnlyPatchShas: rightTarget.filter((sha) => !leftTargetSet.has(sha)),
    };
}
export function buildPatchDivergenceImpl(leftEntries, rightEntries, targetId) {
    const leftShas = uniqueSortedPatchShas(leftEntries);
    const rightShas = uniqueSortedPatchShas(rightEntries);
    const rightSet = new Set(rightShas);
    const leftSet = new Set(leftShas);
    const leftOnly = leftShas.filter((sha) => !rightSet.has(sha));
    const rightOnly = rightShas.filter((sha) => !leftSet.has(sha));
    const base = {
        sharedCount: leftShas.filter((sha) => rightSet.has(sha)).length,
        leftOnlyCount: leftOnly.length,
        rightOnlyCount: rightOnly.length,
        leftOnlyPatchShas: leftOnly,
        rightOnlyPatchShas: rightOnly,
    };
    if (targetId !== null && targetId !== undefined && targetId !== '') {
        return { ...base, target: buildTargetDivergence(leftEntries, rightEntries, targetId) };
    }
    return base;
}
// ── Option-object validation ─────────────────────────────────────────
function assertOptionsObject(options, callerName) {
    if (options === null || options === undefined
        || typeof options !== 'object' || Array.isArray(options)) {
        throw new QueryError(`${callerName} options must be an object`, {
            code: 'invalid_coordinate',
        });
    }
}
function assertRequiredOptions(options, callerName) {
    if (options === null || options === undefined
        || typeof options !== 'object' || Array.isArray(options)) {
        throw new QueryError(`${callerName} requires an options object`, {
            code: 'invalid_coordinate',
        });
    }
}
function isStrandLiteralKind(value) {
    return value !== null && typeof value === 'object'
        && 'kind' in value && value.kind === 'strand';
}
function normalizeAgainstSelector(normalizedStrandId, against, againstCeiling) {
    if (against === 'base') {
        return { kind: 'strand_base', strandId: normalizedStrandId, ceiling: againstCeiling };
    }
    if (against === 'live') {
        return { kind: 'live', ceiling: againstCeiling };
    }
    if (isStrandLiteralKind(against)) {
        return {
            kind: 'strand',
            strandId: normalizeRequiredString(against.strandId, 'against.strandId'),
            ceiling: againstCeiling,
        };
    }
    throw new QueryError('against must be base, live, or { kind: "strand", strandId }', {
        code: 'invalid_coordinate',
    });
}
function normalizeIntoSelector(normalizedStrandId, into, intoCeiling) {
    if (into === 'base') {
        return { kind: 'strand_base', strandId: normalizedStrandId, ceiling: intoCeiling };
    }
    if (into === 'live') {
        return { kind: 'live', ceiling: intoCeiling };
    }
    if (isStrandLiteralKind(into)) {
        return {
            kind: 'strand',
            strandId: normalizeRequiredString(into.strandId, 'into.strandId'),
            ceiling: intoCeiling,
        };
    }
    throw new QueryError('into must be base, live, or { kind: "strand", strandId }', {
        code: 'invalid_coordinate',
    });
}
// ── Blob reading ─────────────────────────────────────────────────────
async function readContentBlobByOid(graph, oid) {
    const buf = (graph._blobStorage !== null && graph._blobStorage !== undefined)
        ? await graph._blobStorage.retrieve(oid)
        : await graph._persistence.readBlob(oid);
    if (!(buf instanceof Uint8Array)) {
        throw new QueryError(`content blob '${oid}' is missing from the object store`, {
            code: 'invalid_coordinate', context: { oid },
        });
    }
    return buf;
}
// ── Core comparison ──────────────────────────────────────────────────
function extractComparisonInputs(options) {
    return {
        normalizedLeft: normalizeSelector(options.left, 'left'),
        normalizedRight: normalizeSelector(options.right, 'right'),
        targetId: normalizeOptionalString(options.targetId, 'targetId'),
        scope: normalizeVisibleStateScope(options.scope, 'scope'),
    };
}
export async function compareCoordinatesImpl(graph, selectorContext, options) {
    assertRequiredOptions(options, 'compareCoordinates()');
    const { normalizedLeft, normalizedRight, targetId, scope } = extractComparisonInputs(options);
    const liveFrontier = (normalizedLeft.kind === 'live' || normalizedRight.kind === 'live')
        ? await selectorContext.coordinateReader.liveFrontier()
        : null;
    const left = await normalizedLeft.resolve(selectorContext, scope, liveFrontier);
    const right = await normalizedRight.resolve(selectorContext, scope, liveFrontier);
    const visiblePatchDivergence = buildPatchDivergenceImpl(left.patchEntries, right.patchEntries, targetId);
    const visibleState = compareVisibleState(left.state, right.state, { targetId });
    const fact = buildCoordinateComparisonFact({
        comparisonVersion: COORDINATE_COMPARISON_VERSION,
        ...(scope !== null && scope !== undefined ? { scope } : {}),
        left: { requested: left.requested, resolved: left.resolved },
        right: { requested: right.requested, resolved: right.resolved },
        visiblePatchDivergence,
        visibleState,
    });
    const digest = await computeChecksum(fact, graph._crypto);
    return {
        comparisonVersion: COORDINATE_COMPARISON_VERSION,
        comparisonDigest: digest,
        ...(scope !== null && scope !== undefined ? { scope } : {}),
        left: { requested: left.requested, resolved: left.resolved },
        right: { requested: right.requested, resolved: right.resolved },
        visiblePatchDivergence,
        visibleState,
    };
}
function normalizeDiffTick(value, field) {
    if (!Number.isInteger(value) || value < 0) {
        throw new QueryError(`${field} must be a non-negative integer`, {
            code: 'invalid_coordinate',
            context: { field, value },
        });
    }
    return value;
}
export async function diffImpl(graph, selectorContext, options) {
    assertRequiredOptions(options, 'diff()');
    const from = normalizeDiffTick(options.from, 'from');
    const to = normalizeDiffTick(options.to, 'to');
    if (from > to) {
        throw new QueryError('diff() requires from <= to', {
            code: 'invalid_coordinate',
            context: { from, to },
        });
    }
    const scope = normalizeVisibleStateScope(options.scope, 'scope');
    const comparison = await compareCoordinatesImpl(graph, selectorContext, {
        left: { kind: 'live', ceiling: from },
        right: { kind: 'live', ceiling: to },
        targetId: normalizeOptionalString(options.targetId, 'targetId'),
        ...(scope !== null && scope !== undefined ? { scope } : {}),
    });
    return new GraphDiff({ comparison });
}
export async function compareStrandImpl(graph, selectorContext, strandId, options = {}) {
    assertOptionsObject(options, 'compareStrand()');
    const normalizedStrandId = normalizeRequiredString(strandId, 'strandId');
    const ceiling = normalizeLamportCeiling(options.ceiling, 'ceiling');
    const againstCeiling = normalizeLamportCeiling(options.againstCeiling, 'againstCeiling');
    const targetId = normalizeOptionalString(options.targetId, 'targetId');
    const scope = normalizeVisibleStateScope(options.scope, 'scope');
    const left = { kind: 'strand', strandId: normalizedStrandId, ceiling };
    const right = normalizeAgainstSelector(normalizedStrandId, options.against ?? 'base', againstCeiling);
    return await compareCoordinatesImpl(graph, selectorContext, {
        left,
        right,
        targetId,
        ...(scope ? { scope } : {}),
    });
}
// ── Transfer planning ────────────────────────────────────────────────
async function finalizeTransferPlan(params) {
    const { graph, sourceSide, targetSide, transfer, comparisonDigest, scope } = params;
    const changed = transfer.summary.opCount > 0;
    const sides = {
        source: { requested: sourceSide.requested, resolved: sourceSide.resolved },
        target: { requested: targetSide.requested, resolved: targetSide.resolved },
    };
    const fact = buildCoordinateTransferPlanFact({
        transferVersion: COORDINATE_TRANSFER_PLAN_VERSION, comparisonDigest,
        ...(scope ? { scope } : {}), changed, ...sides,
        summary: transfer.summary, ops: transfer.ops,
    });
    const digest = await computeChecksum(fact, graph._crypto);
    return {
        transferVersion: COORDINATE_TRANSFER_PLAN_VERSION,
        transferDigest: digest,
        comparisonDigest,
        changed,
        ...sides,
        summary: transfer.summary,
        ops: transfer.ops,
        ...(scope ? { scope } : {}),
    };
}
export async function planCoordinateTransferImpl(graph, selectorContext, options) {
    assertRequiredOptions(options, 'planCoordinateTransfer()');
    const normalizedSource = normalizeSelector(options.source, 'source');
    const normalizedTarget = normalizeSelector(options.target, 'target');
    const scope = normalizeVisibleStateScope(options.scope, 'scope');
    const liveFrontier = (normalizedSource.kind === 'live' || normalizedTarget.kind === 'live')
        ? await selectorContext.coordinateReader.liveFrontier()
        : null;
    const comp = await compareCoordinatesImpl(graph, selectorContext, {
        left: options.source,
        right: options.target,
        ...(scope !== null && scope !== undefined ? { scope } : {}),
    });
    const sourceSide = await normalizedSource.resolve(selectorContext, scope, liveFrontier);
    const targetSide = await normalizedTarget.resolve(selectorContext, scope, liveFrontier);
    const loadNodeContent = async (_nodeId, meta) => await readContentBlobByOid(graph, meta.oid);
    const loadEdgeContent = async (_edge, meta) => await readContentBlobByOid(graph, meta.oid);
    const transfer = await planVisibleStateTransfer(createStateReader(sourceSide.state), createStateReader(targetSide.state), { loadNodeContent, loadEdgeContent });
    return await finalizeTransferPlan({
        graph, sourceSide, targetSide, transfer,
        comparisonDigest: comp.comparisonDigest, scope,
    });
}
export async function planStrandTransferImpl(graph, selectorContext, strandId, options = {}) {
    assertOptionsObject(options, 'planStrandTransfer()');
    const normalizedStrandId = normalizeRequiredString(strandId, 'strandId');
    const ceiling = normalizeLamportCeiling(options.ceiling, 'ceiling');
    const intoCeiling = normalizeLamportCeiling(options.intoCeiling, 'intoCeiling');
    const scope = normalizeVisibleStateScope(options.scope, 'scope');
    const source = { kind: 'strand', strandId: normalizedStrandId, ceiling };
    const target = normalizeIntoSelector(normalizedStrandId, options.into ?? 'live', intoCeiling);
    return await planCoordinateTransferImpl(graph, selectorContext, {
        source,
        target,
        ...(scope ? { scope } : {}),
    });
}
