import VersionVector from "../crdt/VersionVector.js";
import { Dot } from "../crdt/Dot.js";
import PatchError from "../errors/PatchError.js";
import Patch from "../types/Patch.js";
import { hydrateKnownDecodedOp } from "./OpNormalizer.js";
const OP_NORMALIZERS = Object.freeze({
    BlobValue: normalizeDecodedBlobValue,
    EdgeAdd: normalizeDecodedEdgeAdd,
    EdgePropSet: normalizeDecodedEdgePropSet,
    EdgeRemove: normalizeDecodedEdgeRemove,
    NodeAdd: normalizeDecodedNodeAdd,
    NodePropSet: normalizeDecodedNodePropSet,
    NodeRemove: normalizeDecodedNodeRemove,
    PropSet: normalizeDecodedPropSet,
});
function failPatch(message, context) {
    throw new PatchError(message, context === undefined ? undefined : { context });
}
function isRecord(value) {
    return value !== null && value !== undefined && typeof value === 'object' && !Array.isArray(value);
}
function isUnknownArray(value) {
    return Array.isArray(value);
}
function isString(value) {
    return typeof value === 'string';
}
function isInteger(value) {
    return typeof value === 'number' && Number.isInteger(value);
}
function isStringArray(value) {
    return isUnknownArray(value) && value.every(isString);
}
function isStringSet(value) {
    return value instanceof Set && [...value].every(isString);
}
function isDecodedOpType(value) {
    return isString(value) && Object.hasOwn(OP_NORMALIZERS, value);
}
function expectRecord(value, label) {
    if (!isRecord(value)) {
        failPatch(`Decoded patch ${label} must be an object`, {
            label,
            actual: typeof value,
        });
    }
    return value;
}
function expectArray(value, field) {
    if (!isUnknownArray(value)) {
        failPatch(`Decoded patch field '${field}' must be an array`, {
            field,
            actual: typeof value,
        });
    }
    return value;
}
function readOptionalString(record, field) {
    const value = record[field];
    return isString(value) ? value : undefined;
}
function readRequiredString(value, field) {
    if (!isString(value)) {
        failPatch(`Decoded patch requires string '${field}'`, {
            field,
            actual: typeof value,
        });
    }
    return value;
}
function readRequiredInteger(value, field) {
    if (!isInteger(value)) {
        failPatch(`Decoded patch requires integer '${field}'`, {
            field,
            actual: typeof value,
        });
    }
    return value;
}
function readStringArray(value, field) {
    if (value === null || value === undefined) {
        return undefined;
    }
    if (!isStringArray(value)) {
        failPatch(`Decoded patch field '${field}' must be an array of strings`, {
            field,
            actual: typeof value,
        });
    }
    return [...value];
}
function readContextFromMap(context) {
    const normalized = {};
    for (const [key, value] of context) {
        if (!isString(key) || !isInteger(value)) {
            failPatch('Decoded patch context Map must contain string -> number entries', {
                keyType: typeof key,
                valueType: typeof value,
            });
        }
        normalized[key] = value;
    }
    return normalized;
}
function readContextFromRecord(context) {
    const normalized = {};
    for (const [key, value] of Object.entries(context)) {
        if (!isInteger(value)) {
            failPatch(`Decoded patch context '${key}' must be a number`, {
                key,
                actual: typeof value,
            });
        }
        normalized[key] = value;
    }
    return normalized;
}
function readContext(value) {
    if (value === null || value === undefined) {
        return {};
    }
    if (value instanceof VersionVector) {
        return value;
    }
    if (value instanceof Map) {
        return readContextFromMap(value);
    }
    return readContextFromRecord(expectRecord(value, 'context'));
}
function readDotWriterId(dot, opType) {
    const { writerId } = dot;
    if (isString(writerId)) {
        return writerId;
    }
    const { writer } = dot;
    if (isString(writer)) {
        return writer;
    }
    return failPatch(`${opType} dot requires writerId/writer`, {
        opType,
        actual: typeof writerId,
    });
}
function readDotCounter(dot, opType) {
    const { counter } = dot;
    if (isInteger(counter)) {
        return counter;
    }
    const { seq } = dot;
    if (isInteger(seq)) {
        return seq;
    }
    return failPatch(`${opType} dot requires integer counter/seq`, {
        opType,
        actual: typeof counter,
    });
}
function readTupleDot(dot, opType) {
    const [writerId, counter] = dot;
    if (dot.length !== 2 || !isString(writerId) || !isInteger(counter)) {
        failPatch(`${opType} dot tuple must be [writerId, counter]`, {
            opType,
            actual: typeof dot,
        });
    }
    return new Dot(writerId, counter);
}
function readRecordDot(dot, opType) {
    return new Dot(readDotWriterId(dot, opType), readDotCounter(dot, opType));
}
function normalizeDot(dot, opType) {
    if (isUnknownArray(dot)) {
        return readTupleDot(dot, opType);
    }
    return readRecordDot(expectRecord(dot, `${opType}.dot`), opType);
}
function readObservedDots(record) {
    const { observedDots } = record;
    if (isStringArray(observedDots) || isStringSet(observedDots)) {
        return observedDots;
    }
    return undefined;
}
function normalizeDecodedNodeAdd(record) {
    const node = readOptionalString(record, 'node') ?? readOptionalString(record, 'id');
    return {
        type: 'NodeAdd',
        ...(node !== undefined ? { node } : {}),
        dot: normalizeDot(record['dot'], 'NodeAdd'),
    };
}
function normalizeDecodedEdgeAdd(record) {
    const from = readOptionalString(record, 'from');
    const to = readOptionalString(record, 'to');
    const label = readOptionalString(record, 'label');
    return {
        type: 'EdgeAdd',
        ...(from !== undefined ? { from } : {}),
        ...(to !== undefined ? { to } : {}),
        ...(label !== undefined ? { label } : {}),
        dot: normalizeDot(record['dot'], 'EdgeAdd'),
    };
}
function normalizeDecodedNodeRemove(record) {
    const node = readOptionalString(record, 'node');
    const observedDots = readObservedDots(record);
    return {
        type: 'NodeRemove',
        ...(node !== undefined ? { node } : {}),
        ...(observedDots !== undefined ? { observedDots } : {}),
    };
}
function normalizeDecodedEdgeRemove(record) {
    const from = readOptionalString(record, 'from');
    const to = readOptionalString(record, 'to');
    const label = readOptionalString(record, 'label');
    const observedDots = readObservedDots(record);
    return {
        type: 'EdgeRemove',
        ...(from !== undefined ? { from } : {}),
        ...(to !== undefined ? { to } : {}),
        ...(label !== undefined ? { label } : {}),
        ...(observedDots !== undefined ? { observedDots } : {}),
    };
}
function normalizeDecodedPropSet(record) {
    const node = readOptionalString(record, 'node');
    const key = readOptionalString(record, 'key');
    return {
        type: 'PropSet',
        ...(node !== undefined ? { node } : {}),
        ...(key !== undefined ? { key } : {}),
        value: record['value'],
    };
}
function normalizeDecodedNodePropSet(record) {
    const node = readOptionalString(record, 'node');
    const key = readOptionalString(record, 'key');
    return {
        type: 'NodePropSet',
        ...(node !== undefined ? { node } : {}),
        ...(key !== undefined ? { key } : {}),
        value: record['value'],
    };
}
function normalizeDecodedEdgePropSet(record) {
    const from = readOptionalString(record, 'from');
    const to = readOptionalString(record, 'to');
    const label = readOptionalString(record, 'label');
    const key = readOptionalString(record, 'key');
    return {
        type: 'EdgePropSet',
        ...(from !== undefined ? { from } : {}),
        ...(to !== undefined ? { to } : {}),
        ...(label !== undefined ? { label } : {}),
        ...(key !== undefined ? { key } : {}),
        value: record['value'],
    };
}
function normalizeDecodedBlobValue(record) {
    const node = readOptionalString(record, 'node');
    const oid = readOptionalString(record, 'oid');
    return {
        type: 'BlobValue',
        ...(node !== undefined ? { node } : {}),
        ...(oid !== undefined ? { oid } : {}),
    };
}
function readDecodedOpType(record) {
    const { type } = record;
    if (!isString(type)) {
        failPatch("Decoded op requires string 'type'", { actual: typeof type });
    }
    if (!isDecodedOpType(type)) {
        failPatch(`Decoded patch contains unknown op type '${type}'`, { opType: type }); // nosemgrep: ts-no-unknown-outside-adapters -- 0025B
    }
    return type;
}
function normalizeDecodedOp(rawOp) {
    const record = expectRecord(rawOp, 'op');
    return OP_NORMALIZERS[readDecodedOpType(record)](record);
}
function readOps(value) {
    const ops = expectArray(value, 'ops');
    const normalized = [];
    for (const rawOp of ops) {
        normalized.push(hydrateKnownDecodedOp(normalizeDecodedOp(rawOp)));
    }
    return normalized;
}
export function hydrateDecodedPatch(decoded) {
    const record = expectRecord(decoded, 'root');
    const { schema } = record;
    const { writer } = record;
    const { lamport } = record;
    return new Patch({
        schema: schema === 3 ? 3 : 2,
        writer: readRequiredString(writer, 'writer'),
        lamport: readRequiredInteger(lamport, 'lamport'),
        context: readContext(record['context']),
        ops: readOps(record['ops']),
        reads: readStringArray(record['reads'], 'reads'),
        writes: readStringArray(record['writes'], 'writes'),
    });
}
