/**
 * StateDiff - Deterministic state diff engine for PULSE subscriptions.
 *
 * Computes what changed between two materialized WarpState states.
 * Used by the subscription system to notify handlers of graph changes.
 *
 * @module domain/services/state/StateDiff
 * @see ROADMAP.md PL/DIFF/1
 */
import { lwwValue } from "../../crdt/LWW.js";
import { decodeEdgeKey, decodePropKey, isEdgePropKey } from "../KeyCodec.js";
import WarpStateClass from "./WarpState.js";
import { compareEdgeChanges, comparePropChanges } from "./StateDiffOrdering.js";
import { stateDiffValuesEqual } from "./StateDiffValueEquality.js";
function freezeStringArray(values) {
    return Object.freeze([...values]);
}
function freezeObjectArray(values) {
    return Object.freeze(values.map((value) => Object.freeze({ ...value })));
}
export class StateDiffResult {
    nodes;
    edges;
    props;
    constructor({ nodes, edges, props }) {
        this.nodes = Object.freeze({
            added: freezeStringArray(nodes.added),
            removed: freezeStringArray(nodes.removed),
        });
        this.edges = Object.freeze({
            added: freezeObjectArray(edges.added),
            removed: freezeObjectArray(edges.removed),
        });
        this.props = Object.freeze({
            set: freezeObjectArray(props.set),
            removed: freezeObjectArray(props.removed),
        });
        Object.freeze(this);
    }
}
function setAdded(before, after) {
    const result = [];
    for (const item of after) {
        if (!before.has(item)) {
            result.push(item);
        }
    }
    return result;
}
function diffNodesAndEdges(before, after) {
    const beforeNodes = before ? new Set(before.nodeAlive.elements()) : new Set();
    const afterNodes = new Set(after.nodeAlive.elements());
    const beforeEdges = before
        ? new Set(before.edgeAlive.elements().filter((edgeKey) => {
            const { from, to } = decodeEdgeKey(edgeKey);
            return beforeNodes.has(from) && beforeNodes.has(to);
        }))
        : new Set();
    const afterEdges = new Set(after.edgeAlive.elements().filter((edgeKey) => {
        const { from, to } = decodeEdgeKey(edgeKey);
        return afterNodes.has(from) && afterNodes.has(to);
    }));
    const nodesAdded = setAdded(beforeNodes, afterNodes);
    const nodesRemoved = setAdded(afterNodes, beforeNodes);
    const edgesAdded = setAdded(beforeEdges, afterEdges).map(decodeEdgeKey);
    const edgesRemoved = setAdded(afterEdges, beforeEdges).map(decodeEdgeKey);
    return { nodesAdded, nodesRemoved, edgesAdded, edgesRemoved };
}
function diffProps(before, after) {
    const propsSet = [];
    const propsRemoved = [];
    const beforeProps = before
        ? new Map(WarpStateClass.allPropEntriesFromState(before))
        : new Map();
    const afterProps = new Map(WarpStateClass.allPropEntriesFromState(after));
    const allPropKeys = new Set([...beforeProps.keys(), ...afterProps.keys()]);
    for (const key of allPropKeys) {
        if (isEdgePropKey(key)) {
            continue;
        }
        accumulatePropChange(key, { beforeProps, afterProps, propsSet, propsRemoved });
    }
    return { propsSet, propsRemoved };
}
function accumulatePropChange(key, ctx) {
    const change = classifyPropChange(key, ctx.beforeProps, ctx.afterProps);
    if (change === undefined) {
        return;
    }
    if ('newValue' in change) {
        ctx.propsSet.push(change);
    }
    else {
        ctx.propsRemoved.push(change);
    }
}
function classifyPropChange(key, beforeProps, afterProps) {
    const beforeReg = beforeProps.get(key);
    const afterReg = afterProps.get(key);
    const { nodeId, propKey } = decodePropKey(key);
    const addition = classifyPropAddition({ key, nodeId, propKey, beforeReg, afterReg });
    if (addition !== undefined) {
        return addition;
    }
    const removal = classifyPropRemoval({ key, nodeId, propKey, beforeReg, afterReg });
    if (removal !== undefined) {
        return removal;
    }
    return classifyPropUpdateIfComplete({ key, nodeId, propKey, beforeReg, afterReg });
}
function classifyPropAddition(opts) {
    const { key, nodeId, propKey, beforeReg, afterReg } = opts;
    if (afterReg !== undefined && beforeReg === undefined) {
        return { key, nodeId, propKey, oldValue: undefined, newValue: lwwValue(afterReg) };
    }
    return undefined;
}
function classifyPropRemoval(opts) {
    const { key, nodeId, propKey, beforeReg, afterReg } = opts;
    if (afterReg === undefined && beforeReg !== undefined) {
        return { key, nodeId, propKey, oldValue: lwwValue(beforeReg) };
    }
    return undefined;
}
function classifyPropUpdateIfComplete(opts) {
    const { key, nodeId, propKey, beforeReg, afterReg } = opts;
    if (beforeReg === undefined || afterReg === undefined) {
        return undefined;
    }
    return classifyPropUpdate({ key, nodeId, propKey, beforeReg, afterReg });
}
function classifyPropUpdate(opts) {
    const { key, nodeId, propKey, beforeReg, afterReg } = opts;
    const beforeValue = lwwValue(beforeReg);
    const afterValue = lwwValue(afterReg);
    if (!stateDiffValuesEqual(beforeValue, afterValue)) {
        return { key, nodeId, propKey, oldValue: beforeValue, newValue: afterValue };
    }
    return undefined;
}
/**
 * Computes a deterministic diff between two materialized states.
 */
export function diffStates(before, after) {
    const { nodesAdded, nodesRemoved, edgesAdded, edgesRemoved } = diffNodesAndEdges(before, after);
    const { propsSet, propsRemoved } = diffProps(before, after);
    nodesAdded.sort();
    nodesRemoved.sort();
    edgesAdded.sort(compareEdgeChanges);
    edgesRemoved.sort(compareEdgeChanges);
    propsSet.sort(comparePropChanges);
    propsRemoved.sort(comparePropChanges);
    return new StateDiffResult({
        nodes: { added: nodesAdded, removed: nodesRemoved },
        edges: { added: edgesAdded, removed: edgesRemoved },
        props: { set: propsSet, removed: propsRemoved },
    });
}
/**
 * Returns true if the diff represents no changes.
 */
export function isEmptyDiff(diff) {
    return isEmptyPair(diff.nodes) && isEmptyPair(diff.edges) && isEmptySetRemoved(diff.props);
}
function isEmptyPair(pair) {
    return pair.added.length === 0 && pair.removed.length === 0;
}
function isEmptySetRemoved(pair) {
    return pair.set.length === 0 && pair.removed.length === 0;
}
/**
 * Creates an empty diff result.
 */
export function createEmptyDiff() {
    return new StateDiffResult({
        nodes: { added: [], removed: [] },
        edges: { added: [], removed: [] },
        props: { set: [], removed: [] },
    });
}
