import ORSet from "../../crdt/ORSet.js";
import { LWWRegister } from "../../crdt/LWW.js";
import VersionVector from "../../crdt/VersionVector.js";
import EdgeRecord from "../../graph/EdgeRecord.js";
import NodeRecord from "../../graph/NodeRecord.js";
import WarpError from "../../errors/WarpError.js";
import { encodeEdgeKey } from "../KeyCodec.js";
import { createSnapshotPropValue } from "../ImmutableSnapshot.js";
import ContentAttachmentProjection from "../ContentAttachmentProjection.js";
import EdgePropertyProjection from "../EdgePropertyProjection.js";
import NodePropertyProjection from "../NodePropertyProjection.js";
import ImmutableBytes from "../snapshot/ImmutableBytes.js";
import SnapshotWarpState from "../snapshot/SnapshotWarpState.js";
import WarpState from "./WarpState.js";
const FORBIDDEN_SNAPSHOT_PROPERTY_VALUE_KEYS = new Set([
    '__proto__',
    'constructor',
    'prototype',
]);
// ── Edge key helper ──────────────────────────────────────────────────────────
/** Encodes a visible edge reference into a composite key string. */
export function edgeKeyFromRef(edge) {
    return encodeEdgeKey(edge.from, edge.to, edge.label);
}
/** Returns a projection-capable state from a live or immutable reader source. */
export function createStateReaderProjectionState(state) {
    if (state instanceof WarpState) {
        return state;
    }
    if (state instanceof SnapshotWarpState) {
        return warpStateFromSnapshot(state);
    }
    throw new WarpError('StateReader source must be a WarpState or SnapshotWarpState', 'E_VALIDATION');
}
// ── Cloning helpers ──────────────────────────────────────────────────────────
/** Shallow-clones a property bag. */
export function cloneBag(bag) {
    const clone = {};
    for (const [key, value] of Object.entries(bag)) {
        clone[key] = value;
    }
    return Object.freeze(clone);
}
/** Shallow-clones content metadata or returns null. */
export function cloneMeta(meta) {
    return meta ? { ...meta } : null;
}
/** Shallow-clones an array of neighbor entries. */
export function cloneNeighbors(entries) {
    return entries.map((entry) => ({ ...entry }));
}
/** Hydrates an immutable public snapshot into a projection-local WarpState. */
function warpStateFromSnapshot(snapshot) {
    return new WarpState({
        nodeAlive: orsetFromSnapshot(snapshot.nodeAlive),
        edgeAlive: orsetFromSnapshot(snapshot.edgeAlive),
        prop: propMapFromSnapshot(snapshot.prop),
        observedFrontier: VersionVector.from(new Map(snapshot.observedFrontier.entries())),
        edgeBirthEvent: new Map(snapshot.edgeBirthEvent),
    });
}
/** Rebuilds an OR-Set from the immutable snapshot view. */
function orsetFromSnapshot(snapshot) {
    const entries = new Map();
    for (const entry of snapshot.entries()) {
        entries.set(entry.element, new Set(entry.dots));
    }
    return new ORSet(entries, new Set(snapshot.tombstones()));
}
/** Rebuilds the property map from immutable snapshot registers. */
function propMapFromSnapshot(snapshot) {
    const props = new Map();
    for (const [key, register] of snapshot) {
        props.set(key, new LWWRegister(register.eventId, propValueFromSnapshot(register.value)));
    }
    return props;
}
/** Converts immutable snapshot values back into projection-local values. */
function propValueFromSnapshot(value) {
    return propValueFromSnapshotWithSeen(value, new WeakSet());
}
/** Converts immutable snapshot values while detecting invalid recursion. */
function propValueFromSnapshotWithSeen(value, seen) {
    if (value instanceof ImmutableBytes) {
        return value.toUint8Array();
    }
    if (isSnapshotPropValueArray(value)) {
        return propValueArrayFromSnapshot(value, seen);
    }
    if (isSnapshotPropValueObjectCandidate(value)) {
        return propValueObjectFromSnapshot(value, seen);
    }
    return value;
}
/** Converts a snapshot array branch while rejecting cyclic aliases. */
function propValueArrayFromSnapshot(value, seen) {
    requireUnseenSnapshotPropertyValue(value, seen);
    seen.add(value);
    try {
        return value.map((entry) => propValueFromSnapshotWithSeen(entry, seen));
    }
    finally {
        seen.delete(value);
    }
}
/** Converts a snapshot object branch while rejecting prototype keys. */
function propValueObjectFromSnapshot(value, seen) {
    requireSnapshotPropValueObject(value);
    requireUnseenSnapshotPropertyValue(value, seen);
    seen.add(value);
    try {
        const props = {};
        for (const [key, entry] of Object.entries(value)) {
            requireSnapshotPropertyValueKey(key);
            props[key] = propValueFromSnapshotWithSeen(entry, seen);
        }
        return props;
    }
    finally {
        seen.delete(value);
    }
}
/** Returns true for snapshot array branches. */
function isSnapshotPropValueArray(value) {
    return Array.isArray(value);
}
/** Returns true for possible snapshot property dictionary branches. */
function isSnapshotPropValueObjectCandidate(value) {
    return value !== null
        && typeof value === 'object'
        && !(value instanceof ImmutableBytes)
        && !Array.isArray(value);
}
/** Rejects non-plain or accessor-backed snapshot property dictionaries. */
function requireSnapshotPropValueObject(value) {
    if (!isPlainSnapshotPropValueObject(value)) {
        throw invalidSnapshotPropertyValueError();
    }
    if (!snapshotPropertyObjectHasOnlyDataDescriptors(value)) {
        throw invalidSnapshotPropertyValueError();
    }
}
/** Returns true for plain snapshot property dictionaries. */
function isPlainSnapshotPropValueObject(value) {
    const prototype = Reflect.getPrototypeOf(value);
    return prototype === Object.prototype || prototype === null;
}
/** Returns true when snapshot properties cannot execute accessors. */
function snapshotPropertyObjectHasOnlyDataDescriptors(value) {
    for (const descriptor of Object.values(Object.getOwnPropertyDescriptors(value))) {
        if (!isDataPropertyDescriptor(descriptor)) {
            return false;
        }
    }
    return true;
}
/** Returns true for descriptors that expose data instead of accessors. */
function isDataPropertyDescriptor(descriptor) {
    return Object.hasOwn(descriptor, 'value')
        && descriptor.get === undefined
        && descriptor.set === undefined;
}
/** Rejects cyclic snapshot value aliases before recursive hydration. */
function requireUnseenSnapshotPropertyValue(value, seen) {
    if (seen.has(value)) {
        throw invalidSnapshotPropertyValueError();
    }
}
/** Rejects keys that can mutate object prototypes during hydration. */
function requireSnapshotPropertyValueKey(key) {
    if (FORBIDDEN_SNAPSHOT_PROPERTY_VALUE_KEYS.has(key)) {
        throw invalidSnapshotPropertyValueError();
    }
}
/** Builds the snapshot property hydration validation error. */
function invalidSnapshotPropertyValueError() {
    return new WarpError('Snapshot property value must be property-compatible data', 'E_STATE_READER_INVALID_SNAPSHOT_PROPERTY_VALUE');
}
// ── Index builders ───────────────────────────────────────────────────────────
/** Creates a map of node ID to empty property bags for population. */
export function createNodePropIndex(nodeIds) {
    return new Map(nodeIds.map((nodeId) => [nodeId, {}]));
}
/** Creates a map of edge key to empty property bags for population. */
export function createEdgePropIndex(edges) {
    return new Map(edges.map((edge) => [edgeKeyFromRef(edge), {}]));
}
/** Builds outgoing and incoming neighbor indexes from visible nodes and edges. */
export function createNeighborIndex(nodeIds, edges) {
    const outgoingByNode = new Map(nodeIds.map((nodeId) => [nodeId, []]));
    const incomingByNode = new Map(nodeIds.map((nodeId) => [nodeId, []]));
    for (const edge of edges) {
        outgoingByNode.get(edge.from)?.push({ nodeId: edge.to, label: edge.label, direction: 'outgoing' });
        incomingByNode.get(edge.to)?.push({ nodeId: edge.from, label: edge.label, direction: 'incoming' });
    }
    return { outgoingByNode, incomingByNode };
}
/** Builds projection-backed public node property rows from precomputed records. */
export function createProjectionProps(records) {
    return records.map((record) => ({
        node: record.owner.id.toString(),
        key: record.key.toString(),
        value: record.value.toPropValue(),
    }));
}
/** Populates node property indexes from projection records. */
export function populateVisibleNodeProps(records, nodePropsById) {
    for (const record of records) {
        const props = nodePropsById.get(record.owner.id.toString());
        if (props !== undefined) {
            props[record.key.toString()] = createSnapshotPropValue(record.value.toPropValue());
        }
    }
}
/** Populates edge property indexes from projection records. */
export function populateVisibleEdgeProps(records, edgePropsByKey) {
    for (const record of records) {
        const props = edgePropsByKey.get(edgeKeyFromRecord(record.owner));
        if (props !== undefined) {
            props[record.key.toString()] = createSnapshotPropValue(record.value.toPropValue());
        }
    }
}
/** Creates visible edge views with cloned property bags. */
export function createVisibleEdges(edges, edgePropsByKey) {
    return edges.map((edge) => ({
        ...edge,
        props: cloneBag(edgePropsByKey.get(edgeKeyFromRef(edge)) ?? Object.freeze({})),
    }));
}
/** Builds a content metadata index for all visible nodes. */
export function createNodeContentMetaIndex(state, nodeIds) {
    const byNodeId = new Map(nodeIds.map((nodeId) => [nodeId, null]));
    for (const record of ContentAttachmentProjection.fromState(state)) {
        if (record.owner instanceof NodeRecord) {
            byNodeId.set(record.owner.id.toString(), contentMetaFromRecord(record));
        }
    }
    return byNodeId;
}
/** Builds a content metadata index for all visible edges. */
export function createEdgeContentMetaIndex(state, edges) {
    const byEdgeKey = new Map(edges.map((edge) => [edgeKeyFromRef(edge), null]));
    for (const record of ContentAttachmentProjection.fromState(state)) {
        if (record.owner instanceof EdgeRecord) {
            byEdgeKey.set(edgeKeyFromRecord(record.owner), contentMetaFromRecord(record));
        }
    }
    return byEdgeKey;
}
/** Returns projection records for visible node properties. */
export function createNodePropertyRecords(state) {
    return NodePropertyProjection.fromState(state);
}
/** Returns projection records for visible edge properties. */
export function createEdgePropertyRecords(state) {
    return EdgePropertyProjection.fromState(state);
}
/** Encodes an edge record into the state-reader edge key. */
function edgeKeyFromRecord(record) {
    return edgeKeyFromRef({
        from: record.from.toString(),
        to: record.to.toString(),
        label: record.typeId.toString(),
    });
}
/** Converts a typed content attachment record into public reader metadata. */
function contentMetaFromRecord(record) {
    return {
        oid: record.payload.oid.toString(),
        mime: record.payload.mime?.toString() ?? null,
        size: record.payload.size?.toNumber() ?? null,
    };
}
