/**
 * StrandDescriptorStore — boundary over strand refs and descriptor blobs.
 *
 * Responsible for reading, writing, normalizing, and hydrating strand
 * descriptors. Braid-ref synchronization lives here too.
 *
 * @module domain/services/strand/StrandDescriptorStore
 */
import StrandError from "../../errors/StrandError.js";
import { buildStrandBraidRef, buildStrandBraidsPrefix, buildStrandRef, buildStrandOverlayRef, validateWriterId, } from "../../utils/RefLayout.js";
import { parseStrandBlob } from "../../utils/parseStrandBlob.js";
import { textEncode } from "../../utils/bytes.js";
import { isRawBag, normalizeReadOverlays, readOverlaysEqual, overlayMetadataMatches, normalizeIntentQueue, normalizeEvolution, normalizeLastTick, normalizeQueuedIntents, normalizeQueuedIntentEntry, normalizeRejectedCounterfactuals, resolveQueuedIntentIdentity, } from "./descriptorNormalization.js";
// ── Class ────────────────────────────────────────────────────────────────────
export default class StrandDescriptorStore {
    _graph;
    _loadStrandOrThrow;
    _baseObservationsEqual;
    constructor({ graph, loadStrandOrThrow, baseObservationsEqual }) {
        this._graph = graph;
        this._loadStrandOrThrow = loadStrandOrThrow;
        this._baseObservationsEqual = baseObservationsEqual;
    }
    // ── Ref building ─────────────────────────────────────────────────────────
    buildRef(strandId) {
        try {
            validateWriterId(strandId);
        }
        catch (err) {
            throw new StrandError(`Invalid strand id: ${err.message}`, {
                code: 'E_STRAND_ID_INVALID',
                context: { strandId },
            });
        }
        return buildStrandRef(this._graph._graphName, strandId);
    }
    buildOverlayRef(strandId) {
        try {
            validateWriterId(strandId);
        }
        catch (err) {
            throw new StrandError(`Invalid strand id: ${err.message}`, {
                code: 'E_STRAND_ID_INVALID',
                context: { strandId },
            });
        }
        return buildStrandOverlayRef(this._graph._graphName, strandId);
    }
    buildBraidPrefix(strandId) {
        try {
            validateWriterId(strandId);
        }
        catch (err) {
            throw new StrandError(`Invalid strand id: ${err.message}`, {
                code: 'E_STRAND_ID_INVALID',
                context: { strandId },
            });
        }
        return buildStrandBraidsPrefix(this._graph._graphName, strandId);
    }
    buildBraidRef(strandId, braidedStrandId) {
        try {
            validateWriterId(strandId);
            validateWriterId(braidedStrandId);
        }
        catch (err) {
            throw new StrandError(`Invalid strand braid id: ${err.message}`, {
                code: 'E_STRAND_ID_INVALID',
                context: { strandId, braidedStrandId },
            });
        }
        return buildStrandBraidRef(this._graph._graphName, strandId, braidedStrandId);
    }
    // ── Descriptor I/O ───────────────────────────────────────────────────────
    async readDescriptorByOid(oid, strandId) {
        const buf = await this._graph._persistence.readBlob(oid);
        if (buf === null || buf === undefined) {
            throw new StrandError(`Strand '${strandId}' points to a missing blob`, {
                code: 'E_STRAND_MISSING_OBJECT',
                context: { graphName: this._graph._graphName, strandId, oid },
            });
        }
        try {
            const descriptor = parseStrandBlob(buf, `strand '${strandId}'`);
            if (descriptor.graphName !== this._graph._graphName) {
                throw new StrandError('descriptor graphName does not match the current graph', {
                    code: 'E_STRAND_GRAPH_MISMATCH',
                });
            }
            return descriptor;
        }
        catch (err) {
            throw new StrandError(`Strand '${strandId}' is corrupt`, {
                code: 'E_STRAND_CORRUPT',
                context: {
                    graphName: this._graph._graphName,
                    strandId,
                    oid,
                    cause: err.message,
                },
            });
        }
    }
    async writeDescriptor(descriptor) {
        const ref = this.buildRef(descriptor.strandId);
        const oid = await this._graph._persistence.writeBlob(textEncode(JSON.stringify(descriptor)));
        await this._graph._persistence.updateRef(ref, oid);
    }
    // ── Overlay helpers ──────────────────────────────────────────────────────
    async loadBraidedReadOverlays(target, braidedStrandIds) {
        const readOverlays = [];
        for (const braidedStrandId of braidedStrandIds) {
            const braided = await this._loadStrandOrThrow(braidedStrandId);
            if (!this._baseObservationsEqual(braided.baseObservation, target.baseObservation)) {
                throw new StrandError(`Strand '${braidedStrandId}' cannot be braided onto '${target.strandId}' because their pinned base observations differ`, {
                    code: 'E_STRAND_COORDINATE_INVALID',
                    context: {
                        strandId: target.strandId,
                        braidedStrandId,
                        targetBaseObservation: target.baseObservation,
                        braidedBaseObservation: braided.baseObservation,
                    },
                });
            }
            readOverlays.push(this.buildReadOverlayMetadata(braided));
        }
        return readOverlays;
    }
    buildReadOverlayMetadata(descriptor) {
        return {
            strandId: descriptor.strandId,
            overlayId: descriptor.overlay.overlayId,
            kind: descriptor.overlay.kind,
            headPatchSha: descriptor.overlay.headPatchSha,
            patchCount: descriptor.overlay.patchCount,
        };
    }
    async readOverlayMetadata(strandId) {
        const overlayRef = this.buildOverlayRef(strandId);
        const headPatchSha = await this._graph._persistence.readRef(overlayRef);
        if (headPatchSha === null || headPatchSha === undefined) {
            return { headPatchSha: null, patchCount: 0 };
        }
        const overlayPatches = await this._graph._loadPatchChainFromSha(headPatchSha);
        return {
            headPatchSha,
            patchCount: overlayPatches.length,
        };
    }
    // ── Hydration ────────────────────────────────────────────────────────────
    async hydrateDescriptor(descriptor) {
        const braidedReadOverlays = normalizeReadOverlays(descriptor.braid?.readOverlays);
        const normalizedDescriptor = this._buildNormalizedDescriptor(descriptor, braidedReadOverlays);
        const overlay = await this.readOverlayMetadata(descriptor.strandId);
        if (this._matchesHydratedDescriptor(normalizedDescriptor, braidedReadOverlays, overlay)) {
            return normalizedDescriptor;
        }
        return this._withOverlayMetadata(normalizedDescriptor, overlay);
    }
    _buildNormalizedDescriptor(descriptor, braidedReadOverlays) {
        // Narrow the trailing unvalidated blob fields via the type-guard
        // predicate so `unknown` stays inside legitimate x-is-Foo form. // nosemgrep: ts-no-unknown-outside-adapters -- 0025B
        const intentQueueRaw = isRawBag(descriptor['intentQueue'])
            ? descriptor['intentQueue']
            : null;
        const evolutionRaw = isRawBag(descriptor['evolution'])
            ? descriptor['evolution']
            : null;
        return {
            ...descriptor,
            overlay: {
                ...descriptor.overlay,
                writable: descriptor.overlay.writable ?? true,
            },
            braid: {
                readOverlays: braidedReadOverlays,
            },
            intentQueue: this.normalizeIntentQueue(intentQueueRaw),
            evolution: this.normalizeEvolution(evolutionRaw),
        };
    }
    _matchesHydratedDescriptor(descriptor, braidedReadOverlays, overlay) {
        return (overlayMetadataMatches(descriptor, {
            headPatchSha: overlay.headPatchSha,
            patchCount: overlay.patchCount,
            writable: descriptor.overlay.writable,
        }) &&
            readOverlaysEqual(descriptor.braid.readOverlays, braidedReadOverlays));
    }
    _withOverlayMetadata(descriptor, overlay) {
        return {
            ...descriptor,
            overlay: {
                ...descriptor.overlay,
                headPatchSha: overlay.headPatchSha,
                patchCount: overlay.patchCount,
            },
        };
    }
    // ── Normalization (delegates to descriptorNormalization.ts) ──────────────
    normalizeIntentQueue(value) {
        return normalizeIntentQueue(value, (intents) => normalizeQueuedIntents(intents));
    }
    normalizeEvolution(value) {
        return normalizeEvolution(value, (lastTick) => normalizeLastTick(lastTick));
    }
    // ── Braid ref sync ───────────────────────────────────────────────────────
    async syncBraidRefs(strandId, readOverlays) {
        const prefix = this.buildBraidPrefix(strandId);
        const existingRefs = await this._graph._persistence.listRefs(prefix);
        const nextRefs = new Set();
        for (const readOverlay of readOverlays) {
            await this._syncOneBraidRef(strandId, readOverlay, nextRefs);
        }
        for (const existingRef of existingRefs) {
            if (!nextRefs.has(existingRef)) {
                await this._graph._persistence.deleteRef(existingRef);
            }
        }
    }
    async _syncOneBraidRef(strandId, readOverlay, nextRefs) {
        const ref = this.buildBraidRef(strandId, readOverlay.strandId);
        nextRefs.add(ref);
        if (readOverlay.headPatchSha !== null && readOverlay.headPatchSha.length > 0) {
            await this._graph._persistence.updateRef(ref, readOverlay.headPatchSha);
            return;
        }
        if ((await this._graph._persistence.readRef(ref)) !== null) {
            await this._graph._persistence.deleteRef(ref);
        }
    }
    // ── Test-seam private wrappers ───────────────────────────────────────────
    /** @internal for test seam access */
    _normalizeQueuedIntentEntry(rawEntry) {
        return normalizeQueuedIntentEntry(rawEntry);
    }
    /** @internal for test seam access */
    _resolveQueuedIntentIdentity(candidate) {
        return resolveQueuedIntentIdentity(candidate);
    }
    /** @internal for test seam access */
    _normalizeQueuedIntents(value) {
        return normalizeQueuedIntents(value);
    }
    /** @internal for test seam access */
    _normalizeRejectedCounterfactuals(value) {
        return normalizeRejectedCounterfactuals(value);
    }
}
