/**
 * TickReceipt — immutable record of per-operation outcomes from a single patch application.
 *
 * A tick receipt captures what happened to each operation in a patch during
 * materialization: whether it was applied, superseded by a concurrent write,
 * or redundant (already present in the state).
 *
 * This is a type definition only — emission logic lives in LH/RECEIPTS/2.
 *
 * @module TickReceipt
 * @see Paper II, Section 5 — Tick receipts: event posets recording accepted/rejected matches
 */
import WarpError from "../errors/WarpError.js";
import { sortedReplacer } from "../utils/canonicalStringify.js";
import { requireNonEmptyString } from "../utils/scalarValidation.js";
// ============================================================================
// Constants
// ============================================================================
/**
 * Valid operation types that can appear in a tick receipt.
 */
export const OP_TYPES = Object.freeze([
    'NodeAdd',
    'NodeTombstone',
    'EdgeAdd',
    'EdgeTombstone',
    'PropSet',
    'NodePropSet',
    'EdgePropSet',
    'BlobValue',
]);
/**
 * Valid result values for an operation outcome.
 */
export const RESULT_TYPES = Object.freeze([
    'applied',
    'superseded',
    'redundant',
]);
// ============================================================================
// Validation Helpers
// ============================================================================
const opTypeSet = new Set(OP_TYPES);
const resultTypeSet = new Set(RESULT_TYPES);
/**
 * Asserts that a value is a non-null object.
 */
function isObject(value) {
    return value !== null && typeof value === 'object';
}
/**
 * Validates a single operation outcome entry.
 *
 * @throws If validation fails
 */
function validateOp(op, index) {
    if (!isObject(op)) {
        throw new WarpError(`ops[${index}] must be an object`, 'E_VALIDATION');
    }
    const entry = op;
    validateOpType(entry['op'], index);
    validateOpTarget(entry['target'], index);
    validateOpResult(entry['result'], index);
    if (entry['reason'] !== undefined && typeof entry['reason'] !== 'string') {
        throw new WarpError(`ops[${index}].reason must be a string or undefined`, 'E_VALIDATION');
    }
}
/**
 * Validates that an operation type is one of the allowed OP_TYPES.
 *
 * Valid operation types correspond to the eight receipt operation types:
 * NodeAdd, NodeTombstone, EdgeAdd, EdgeTombstone, PropSet, NodePropSet,
 * EdgePropSet, and BlobValue.
 *
 * @example
 * validateOpType('NodeAdd', 0); // OK
 * validateOpType('InvalidOp', 0); // throws Error
 * validateOpType(123, 0); // throws Error
 */
function validateOpType(value, i) {
    if (typeof value !== 'string' || !opTypeSet.has(value)) {
        throw new WarpError(`ops[${i}].op must be one of: ${OP_TYPES.join(', ')}`, 'E_VALIDATION');
    }
}
/**
 * Validates that an operation target is a non-empty string.
 *
 * The target identifies what entity was affected by the operation:
 * - For node operations: the node ID (e.g., "user:alice")
 * - For edge operations: the edge key (e.g., "user:alice\0user:bob\0follows")
 * - For property operations: the property key (e.g., "user:alice\0name")
 *
 * @example
 * validateOpTarget('user:alice', 0); // OK
 * validateOpTarget('', 0); // throws Error
 * validateOpTarget(null, 0); // throws Error
 */
function validateOpTarget(value, i) {
    if (typeof value !== 'string' || value.length === 0) {
        throw new WarpError(`ops[${i}].target must be a non-empty string`, 'E_VALIDATION');
    }
}
/**
 * Validates that an operation result is one of the allowed RESULT_TYPES.
 *
 * Valid results describe the outcome of applying the operation:
 * - `'applied'`: The operation was successfully applied to the state
 * - `'superseded'`: The operation was overridden by a concurrent write
 *   (e.g., LWW register where another writer had a higher timestamp)
 * - `'redundant'`: The operation had no effect because the state already
 *   reflected it (e.g., adding a node that was already present)
 *
 * @example
 * validateOpResult('applied', 0); // OK
 * validateOpResult('superseded', 1); // OK
 * validateOpResult('failed', 0); // throws Error
 */
function validateOpResult(value, i) {
    if (typeof value !== 'string' || !resultTypeSet.has(value)) {
        throw new WarpError(`ops[${i}].result must be one of: ${RESULT_TYPES.join(', ')}`, 'E_VALIDATION');
    }
}
/**
 * TickReceipt — immutable record of per-operation outcomes from a single patch.
 */
export class TickReceipt {
    /** Lamport timestamp of the patch */
    lamport;
    /** Per-operation outcomes (frozen) */
    ops;
    /** SHA of the patch commit */
    patchSha;
    /** Writer ID that produced the patch */
    writer;
    /**
     * Creates an immutable TickReceipt.
     *
     * @throws If any field is invalid
     */
    constructor({ patchSha, writer, lamport, ops }) {
        requireNonEmptyString(patchSha, 'patchSha');
        requireNonEmptyString(writer, 'writer');
        assertNonNegativeInt(lamport);
        assertOpsArray(ops);
        this.lamport = lamport;
        this.ops = freezeOps(ops);
        this.patchSha = patchSha;
        this.writer = writer;
        Object.freeze(this);
    }
}
/**
 * Creates an immutable TickReceipt.
 */
export function createTickReceipt({ patchSha, writer, lamport, ops }) {
    return new TickReceipt({ patchSha, writer, lamport, ops });
}
/**
 * Asserts that lamport is a non-negative integer.
 */
function assertNonNegativeInt(value) {
    if (!Number.isInteger(value) || value < 0) {
        throw new WarpError('lamport must be a non-negative integer', 'E_VALIDATION');
    }
}
/**
 * Asserts that ops is a valid array and validates each entry.
 */
function assertOpsArray(ops) {
    if (!Array.isArray(ops)) {
        throw new WarpError('ops must be an array', 'E_VALIDATION');
    }
    for (let i = 0; i < ops.length; i++) {
        validateOp(ops[i], i);
    }
}
/**
 * Builds a frozen, defensively-copied array of operation outcomes.
 */
function freezeOps(ops) {
    return Object.freeze(ops.map((o) => {
        const entry = { op: o.op, target: o.target, result: o.result };
        if (o.reason !== undefined) {
            entry.reason = o.reason;
        }
        return Object.freeze(entry);
    }));
}
// ============================================================================
// Canonical JSON Serialization
// ============================================================================
/**
 * Produces a deterministic JSON string for a TickReceipt.
 *
 * Keys are sorted alphabetically at every nesting level, ensuring
 * identical receipts always produce identical byte strings regardless
 * of property insertion order.
 */
export function canonicalJson(receipt) {
    return JSON.stringify(receipt, sortedReplacer); // nosemgrep: ts-no-json-stringify-in-core -- 0025B
}
