/**
 * WarpState — the core CRDT materialized state object.
 *
 * Holds the alive sets (OR-Set for nodes and edges), property registers
 * (LWW), the observed version vector frontier, and edge birth events.
 *
 * @module domain/services/state/WarpState
 */
import ORSet from "../../crdt/ORSet.js";
import VersionVector from "../../crdt/VersionVector.js";
import { lwwMax, lwwSet } from "../../crdt/LWW.js";
import { compareEventIds } from "../../utils/EventId.js";
import AttachmentKey from "../../graph/AttachmentKey.js";
import AttachmentRecord from "../../graph/AttachmentRecord.js";
import AttachmentSchemaVersion from "../../graph/AttachmentSchemaVersion.js";
import EdgeId from "../../graph/EdgeId.js";
import EdgeRecord from "../../graph/EdgeRecord.js";
import NodeId from "../../graph/NodeId.js";
import NodeRecord from "../../graph/NodeRecord.js";
import { decodeEdgeKey, decodeEdgePropKey, decodePropKey, encodeEdgeKey, encodeEdgePropKey, encodePropKey, isEdgePropKey } from "../KeyCodec.js";
/**
 * The CRDT materialized state for a WARP graph.
 *
 * This is an entity, not a frozen value object: reduce paths mutate a
 * live instance in place for performance, then callers clone or snapshot
 * before handing state to consumers that expect isolation.
 */
export default class WarpState {
    nodeAlive;
    edgeAlive;
    prop;
    observedFrontier;
    /** EdgeKey → EventId of most recent EdgeAdd (for clean-slate prop visibility). */
    edgeBirthEvent;
    constructor(fields) {
        this.nodeAlive = fields.nodeAlive;
        this.edgeAlive = fields.edgeAlive;
        this.prop = fields.prop;
        this.observedFrontier = fields.observedFrontier;
        this.edgeBirthEvent = fields.edgeBirthEvent ?? new Map();
    }
    /** Creates an empty state with fresh OR-Sets and version vector. */
    static empty() {
        return new WarpState({
            nodeAlive: ORSet.empty(),
            edgeAlive: ORSet.empty(),
            prop: new Map(),
            observedFrontier: VersionVector.empty(),
            edgeBirthEvent: new Map(),
        });
    }
    /** Returns live graph nodes as deterministic runtime-backed records. */
    nodeRecords() {
        const nodeIds = this.nodeAlive.elements().sort(compareStrings);
        const records = nodeIds.map((nodeId) => NodeRecord.fromLegacyNodeId(nodeId));
        return Object.freeze(records);
    }
    /** Returns the live graph node record for the given id, if present. */
    getNodeRecord(nodeId) {
        const id = normalizeNodeId(nodeId);
        if (!this.nodeAlive.contains(id.toString())) {
            return null;
        }
        return NodeRecord.fromLegacyNodeId(id.toString());
    }
    /** Returns true when the given node id has a live graph node record. */
    hasNodeRecord(nodeId) {
        return this.getNodeRecord(nodeId) !== null;
    }
    /** Returns the total number of stored property registers (node + edge). */
    propSize() {
        return this.prop.size;
    }
    /** Returns true when the given encoded prop key has a stored register. */
    hasProp(encodedKey) {
        return this.prop.has(encodedKey);
    }
    /** Returns true when the given node has a stored property register for the key. */
    hasNodeProp(nodeId, key) {
        return this.prop.has(encodePropKey(nodeId, key));
    }
    /** Returns the property register for a node property, or undefined if absent. */
    getNodeProp(nodeId, key) {
        return this.prop.get(encodePropKey(nodeId, key));
    }
    /** Returns the property register for an edge property, or undefined if absent. */
    getEdgeProp(from, to, label, key) {
        return this.prop.get(encodeEdgePropKey(from, to, label, key));
    }
    /** Returns the property register for an already-encoded key, or undefined if absent. */
    getEncodedProp(encodedKey) {
        return this.prop.get(encodedKey);
    }
    /**
     * LWW-max write: updates the register for an already-encoded key if the
     * incoming event wins the last-writer-wins comparison.
     */
    mutatePropLWW(encodedKey, eventId, value) {
        const current = this.prop.get(encodedKey);
        const winner = lwwMax(current, lwwSet(eventId, value));
        this.prop.set(encodedKey, winner);
    }
    /** LWW-max write for callers that already hold a validated property register. */
    mutatePropRegisterLWW(encodedKey, register) {
        const current = this.prop.get(encodedKey);
        const winner = lwwMax(current, register);
        this.prop.set(encodedKey, winner);
    }
    /** Yields every node property register with decoded identity. */
    *nodeProperties() {
        for (const [encodedKey, register] of this.prop) {
            if (!isEdgePropKey(encodedKey)) {
                const decoded = decodePropKey(encodedKey);
                yield { encodedKey, nodeId: decoded.nodeId, key: decoded.propKey, register };
            }
        }
    }
    /** Yields every edge property register with decoded identity. */
    *edgeProperties() {
        for (const [encodedKey, register] of this.prop) {
            if (isEdgePropKey(encodedKey)) {
                const decoded = decodeEdgePropKey(encodedKey);
                yield { encodedKey, from: decoded.from, to: decoded.to, label: decoded.label, key: decoded.propKey, register };
            }
        }
    }
    /**
     * Yields node property entries from a raw prop map.
     * Use when a prop map is available but WarpState is not (e.g. stream sessions).
     */
    static *nodePropertiesFromMap(prop) {
        for (const [encodedKey, register] of prop) {
            if (!isEdgePropKey(encodedKey)) {
                const decoded = decodePropKey(encodedKey);
                yield { encodedKey, nodeId: decoded.nodeId, key: decoded.propKey, register };
            }
        }
    }
    /**
     * Yields node property entries from either a live WarpState or a structural
     * state snapshot whose prop map has not been hydrated into WarpState yet.
     */
    static *nodePropertiesFromState(state) {
        for (const [encodedKey, register] of WarpState.allPropEntriesFromState(state)) {
            if (!isEdgePropKey(encodedKey)) {
                const decoded = decodePropKey(encodedKey);
                yield { encodedKey, nodeId: decoded.nodeId, key: decoded.propKey, register };
            }
        }
    }
    /**
     * Yields edge property entries from a raw prop map.
     * Use when a prop map is available but WarpState is not (e.g. stream sessions).
     */
    static *edgePropertiesFromMap(prop) {
        for (const [encodedKey, register] of prop) {
            if (isEdgePropKey(encodedKey)) {
                const decoded = decodeEdgePropKey(encodedKey);
                yield { encodedKey, from: decoded.from, to: decoded.to, label: decoded.label, key: decoded.propKey, register };
            }
        }
    }
    /**
     * Yields edge property entries from either a live WarpState or a structural
     * state snapshot whose prop map has not been hydrated into WarpState yet.
     */
    static *edgePropertiesFromState(state) {
        for (const [encodedKey, register] of WarpState.allPropEntriesFromState(state)) {
            if (isEdgePropKey(encodedKey)) {
                const decoded = decodeEdgePropKey(encodedKey);
                yield { encodedKey, from: decoded.from, to: decoded.to, label: decoded.label, key: decoded.propKey, register };
            }
        }
    }
    /** Yields all prop entries (node + edge) with their encoded keys, for checkpoint serialization. */
    *allPropEntries() {
        for (const [key, register] of this.prop) {
            yield [key, register];
        }
    }
    /**
     * Yields all prop entries from either a live WarpState or a structural state
     * snapshot whose prop map has not been hydrated into WarpState yet.
     */
    static *allPropEntriesFromState(state) {
        if (state instanceof WarpState) {
            for (const [key, register] of state.prop) {
                yield [key, register];
            }
            return;
        }
        for (const [key, register] of state.prop) {
            yield [key, register];
        }
    }
    /** Returns visible graph edges as deterministic runtime-backed records. */
    edgeRecords() {
        const records = [];
        for (const edgeKey of this.edgeAlive.elements()) {
            const edge = decodeEdgeKey(edgeKey);
            if (!this.hasNodeRecord(edge.from) || !this.hasNodeRecord(edge.to)) {
                continue;
            }
            records.push(EdgeRecord.fromLegacyEdge(edge));
        }
        records.sort(compareEdgeRecords);
        return Object.freeze(records);
    }
    /** Returns the visible graph edge record for the given id, if present. */
    getEdgeRecord(edgeId) {
        const id = normalizeEdgeId(edgeId);
        for (const record of this.edgeRecords()) {
            if (record.id.equals(id)) {
                return record;
            }
        }
        return null;
    }
    /** Returns true when the given edge id has a visible graph edge record. */
    hasEdgeRecord(edgeId) {
        return this.getEdgeRecord(edgeId) !== null;
    }
    /** Returns visible node and edge payloads as deterministic attachment records. */
    attachmentRecords() {
        const records = [];
        for (const [propKey, register] of this.prop) {
            const record = attachmentRecordForProperty(this, propKey, register);
            if (record !== null) {
                records.push(record);
            }
        }
        records.sort(compareAttachmentRecords);
        return Object.freeze(records);
    }
    /** Creates a deep clone with independent data structures. */
    clone() {
        return new WarpState({
            nodeAlive: this.nodeAlive.clone(),
            edgeAlive: this.edgeAlive.clone(),
            prop: new Map(this.prop),
            observedFrontier: this.observedFrontier.clone(),
            edgeBirthEvent: new Map(this.edgeBirthEvent),
        });
    }
    /**
     * Normalizes a plain-object or deserialized state into a live
     * `WarpState` instance with cloned inner structures. Used by the
     * reducer and checkpoint loader to accept either class instances or
     * hydrated POJOs at the boundary.
     */
    static cloneFromSnapshot(state) {
        if (state instanceof WarpState) {
            return state.clone();
        }
        return new WarpState({
            nodeAlive: state.nodeAlive.clone(),
            edgeAlive: state.edgeAlive.clone(),
            prop: new Map(state.prop),
            observedFrontier: state.observedFrontier.clone(),
            edgeBirthEvent: new Map(state.edgeBirthEvent ?? []),
        });
    }
    /**
     * CRDT join with another state. Pure — does not mutate either input.
     * Components merge as:
     * - `nodeAlive` / `edgeAlive`: OR-Set join
     * - `prop`: LWW-Max per key
     * - `observedFrontier`: VersionVector merge (component-wise max)
     * - `edgeBirthEvent`: EventId max per edge key
     */
    join(other) {
        return new WarpState({
            nodeAlive: this.nodeAlive.join(other.nodeAlive),
            edgeAlive: this.edgeAlive.join(other.edgeAlive),
            prop: WarpState._mergeProps(this.prop, other.prop),
            observedFrontier: this.observedFrontier.merge(other.observedFrontier),
            edgeBirthEvent: WarpState._mergeEdgeBirthEvent(this.edgeBirthEvent, other.edgeBirthEvent),
        });
    }
    /**
     * Folds a patch's context version vector AND its own dot
     * (writer, lamport) into this state's `observedFrontier`. Mutates
     * `this.observedFrontier` in place.
     */
    foldPatch(patch) {
        const contextVV = patch.context instanceof VersionVector
            ? patch.context
            : VersionVector.from(patch.context ?? {});
        this.observedFrontier = this.observedFrontier.merge(contextVV);
        const current = this.observedFrontier.get(patch.writer) ?? 0;
        if (patch.lamport > current) {
            this.observedFrontier.set(patch.writer, patch.lamport);
        }
    }
    /** LWW-Max merge of two property maps. */
    static _mergeProps(a, b) {
        const result = new Map(a);
        for (const [key, regB] of b) {
            const regA = result.get(key);
            const winner = lwwMax(regA, regB);
            result.set(key, winner);
        }
        return result;
    }
    /** EventId-max merge of two edge-birth-event maps. */
    static _mergeEdgeBirthEvent(a, b) {
        const result = new Map(a ?? []);
        if (b) {
            for (const [key, eventId] of b) {
                const existing = result.get(key);
                if (!existing || compareEventIds(eventId, existing) > 0) {
                    result.set(key, eventId);
                }
            }
        }
        return result;
    }
}
/** Normalizes an edge id carrier for state record reads. */
function normalizeEdgeId(value) {
    if (value instanceof EdgeId) {
        return value;
    }
    return new EdgeId(value);
}
/** Normalizes a node id carrier for state record reads. */
function normalizeNodeId(value) {
    if (value instanceof NodeId) {
        return value;
    }
    return new NodeId(value);
}
/** Compares edge records by deterministic id order. */
function compareEdgeRecords(left, right) {
    return compareStrings(left.id.toString(), right.id.toString());
}
/** Builds a visible attachment record from a legacy property map entry. */
function attachmentRecordForProperty(state, propKey, register) {
    if (isEdgePropKey(propKey)) {
        return edgeAttachmentRecordForProperty(state, propKey, register);
    }
    return nodeAttachmentRecordForProperty(state, propKey, register);
}
/** Builds a node-owned attachment record from a legacy node property. */
function nodeAttachmentRecordForProperty(state, propKey, register) {
    const decoded = decodePropKey(propKey);
    const owner = state.getNodeRecord(decoded.nodeId);
    if (owner === null) {
        return null;
    }
    return new AttachmentRecord({
        owner,
        key: new AttachmentKey(decoded.propKey),
        value: register.value,
        schemaVersion: AttachmentSchemaVersion.current(),
    });
}
/** Builds an edge-owned attachment record from a legacy edge property. */
function edgeAttachmentRecordForProperty(state, propKey, register) {
    const decoded = decodeEdgePropKey(propKey);
    const edgeKey = encodeEdgeKey(decoded.from, decoded.to, decoded.label);
    if (isStaleEdgeAttachment(register, state.edgeBirthEvent.get(edgeKey))) {
        return null;
    }
    if (!state.edgeAlive.contains(edgeKey)) {
        return null;
    }
    if (!state.hasNodeRecord(decoded.from) || !state.hasNodeRecord(decoded.to)) {
        return null;
    }
    const owner = EdgeRecord.fromLegacyEdge(decoded);
    return new AttachmentRecord({
        owner,
        key: new AttachmentKey(decoded.propKey),
        value: register.value,
        schemaVersion: AttachmentSchemaVersion.current(),
    });
}
/** Returns true when an edge attachment predates the current edge birth. */
function isStaleEdgeAttachment(register, birthEvent) {
    if (birthEvent === undefined || register.eventId === null) {
        return false;
    }
    return compareEventIds(register.eventId, birthEvent) < 0;
}
/** Compares attachment records by deterministic owner/key order. */
function compareAttachmentRecords(left, right) {
    return compareStrings(attachmentRecordSortKey(left), attachmentRecordSortKey(right));
}
/** Returns the deterministic sort key for an attachment record. */
function attachmentRecordSortKey(record) {
    if (record.owner instanceof NodeRecord) {
        return `node:${record.owner.id.toString()}:${record.key.toString()}`;
    }
    return `edge:${record.owner.id.toString()}:${record.key.toString()}`;
}
/** Compares protocol strings without locale-sensitive collation. */
function compareStrings(left, right) {
    if (left < right) {
        return -1;
    }
    if (left > right) {
        return 1;
    }
    return 0;
}
