/**
 * ReceiptBuilder — computes per-op outcome results used to populate
 * TickReceipts. Each method returns an `OpOutcomeResult` subclass
 * (OpApplied / OpSuperseded / OpRedundant).
 *
 * The reducer's `applyWithReceipt` path calls these before mutation.
 * The methods are pure — they read state but do not mutate it.
 *
 * The class exposes only static methods, so there is no instance state
 * or constructor-time freeze contract to satisfy here.
 *
 * @module domain/services/ReceiptBuilder
 */
import { encodeDot } from "../crdt/Dot.js";
import { compareEventIds } from "../utils/EventId.js";
import { encodeEdgeKey, encodePropKey, encodeEdgePropKey } from "./KeyCodec.js";
import { OP_TYPES } from "../types/TickReceipt.js";
import OpApplied from "../types/ops/OpApplied.js";
import OpSuperseded from "../types/ops/OpSuperseded.js";
import OpRedundant from "../types/ops/OpRedundant.js";
import DiffCalculator from "./DiffCalculator.js";
/** Set of valid receipt op types (from TickReceipt) for fast membership checks. */
const VALID_RECEIPT_OPS = new Set(OP_TYPES);
/** Type guard: value is a string with length > 0. */
function _isNonEmptyString(value) {
    return typeof value === 'string' && value.length > 0;
}
/** Normalizes an arbitrary observed-dots iterable into a Set. */
function toDotSet(observedDots) {
    if (observedDots instanceof Set) {
        return observedDots;
    }
    return new Set(observedDots);
}
/**
 * Walks the target dots against the ORSet's reverse index and returns
 * true iff at least one dot was both present and not yet tombstoned —
 * i.e. the remove is effective, not redundant.
 */
function hasEffectiveRemoval(orset, targetDots) {
    const dotToElement = DiffCalculator.buildDotToElement(orset, targetDots);
    for (const encodedDot of targetDots) {
        if (!orset.isTombstoned(encodedDot) && dotToElement.has(encodedDot)) {
            return true;
        }
    }
    return false;
}
export default class ReceiptBuilder {
    /**
     * Valid receipt op types (the set accepted by TickReceipt). Exposed
     * so receipt construction can reject unrecognized op entries at the
     * reducer boundary instead of silently dropping them.
     */
    static VALID_RECEIPT_OPS = VALID_RECEIPT_OPS;
    /**
     * NodeAdd receipt outcome. Redundant iff the exact (node, dot)
     * already exists in the ORSet entries (idempotent re-delivery).
     */
    static nodeAddOutcome(orset, op) {
        const encoded = encodeDot(op.dot);
        if (orset.hasDot(op.node, encoded)) {
            return new OpRedundant(op.node);
        }
        return new OpApplied(op.node);
    }
    /**
     * NodeRemove receipt outcome. Applied iff at least one observed dot
     * actually removes a live entry; otherwise redundant.
     */
    static nodeRemoveOutcome(orset, op) {
        const effective = hasEffectiveRemoval(orset, toDotSet(op.observedDots));
        const target = (typeof op.node === 'string' && op.node.length > 0) ? op.node : '*';
        return effective ? new OpApplied(target) : new OpRedundant(target);
    }
    /**
     * EdgeAdd receipt outcome. Redundant iff the exact (edgeKey, dot)
     * already exists.
     */
    static edgeAddOutcome(orset, op, edgeKey) {
        const encoded = encodeDot(op.dot);
        if (orset.hasDot(edgeKey, encoded)) {
            return new OpRedundant(edgeKey);
        }
        return new OpApplied(edgeKey);
    }
    /**
     * EdgeRemove receipt outcome. Target is the encoded edge key when
     * (from, to, label) are all present, otherwise `'*'` for
     * wildcard/unresolved targets.
     */
    static edgeRemoveOutcome(orset, op) {
        const effective = hasEffectiveRemoval(orset, toDotSet(op.observedDots));
        const target = ReceiptBuilder._edgeRemoveTargetKey(op);
        return effective ? new OpApplied(target) : new OpRedundant(target);
    }
    /**
     * Computes the target key for an edge-remove outcome: the encoded edge
     * key when (from, to, label) are all present and non-empty, else `'*'`.
     */
    static _edgeRemoveTargetKey(op) {
        const { from, to, label } = op;
        if (_isNonEmptyString(from) && _isNonEmptyString(to) && _isNonEmptyString(label)) {
            return encodeEdgeKey(from, to, label);
        }
        return '*';
    }
    /**
     * Generic property outcome given a pre-encoded key. Uses LWW
     * semantics: applied if new, superseded if an older EventId loses,
     * redundant on exact EventId match.
     */
    static propOutcomeForKey(state, key, eventId) {
        const current = state.getEncodedProp(key);
        if (!current) {
            return new OpApplied(key);
        }
        const cmp = compareEventIds(eventId, current.eventId);
        if (cmp > 0) {
            return new OpApplied(key);
        }
        if (cmp < 0) {
            return new OpSuperseded(key, current.eventId);
        }
        return new OpRedundant(key);
    }
    /** NodePropSet / legacy PropSet receipt outcome. */
    static propSetOutcome(state, op, eventId) {
        return ReceiptBuilder.propOutcomeForKey(state, encodePropKey(op.node, op.key), eventId);
    }
    /** EdgePropSet receipt outcome. */
    static edgePropSetOutcome(state, op, eventId) {
        return ReceiptBuilder.propOutcomeForKey(state, encodeEdgePropKey(op.from, op.to, op.label, op.key), eventId);
    }
}
