import StrandError from "../../errors/StrandError.js";
import { PatchBuilder } from "../PatchBuilder.js";
import { maxPatchLamport, normalizeStringArray, } from "./strandShared.js";
import { isGitCasPatchStorage, LEGACY_EXTERNAL_PATCH_STORAGE, LEGACY_GIT_BLOB_PATCH_STORAGE, } from "../../../ports/CommitMessageCodecPort.js";
function readBuilderContentBlobs(builder) {
    return builder.contentBlobs;
}
export default class StrandPatchService {
    _graph;
    _loadStrandOrThrow;
    _materializeDescriptor;
    _writeDescriptor;
    _buildOverlayRef;
    _buildIntentId;
    /**
     * Create a patch/commit boundary over strand overlay writes and queued intent construction.
     */
    constructor({ graph, loadStrandOrThrow, materializeDescriptor, writeDescriptor, buildOverlayRef, buildIntentId, }) {
        this._graph = graph;
        this._loadStrandOrThrow = loadStrandOrThrow;
        this._materializeDescriptor = materializeDescriptor;
        this._writeDescriptor = writeDescriptor;
        this._buildOverlayRef = buildOverlayRef;
        this._buildIntentId = buildIntentId;
    }
    /**
     * Create a fluent patch builder wired to the strand's overlay ref.
     */
    async createPatchBuilder(strandId) {
        const descriptor = await this._loadStrandOrThrow(strandId);
        return await this._createPatchBuilderForDescriptor(descriptor);
    }
    /**
     * Build and commit a patch within a reentrancy guard.
     */
    async patch(strandId, build) {
        if (this._graph._patchInProgress) {
            throw new StrandError('graph.patchStrand() is not reentrant. Use createStrandPatch() for nested or concurrent patches.', { code: 'E_STRAND_REENTRANT' });
        }
        this._graph._patchInProgress = true;
        try {
            const builder = await this.createPatchBuilder(strandId);
            await build(builder);
            return await builder.commit();
        }
        finally {
            this._graph._patchInProgress = false;
        }
    }
    /**
     * Build a queued intent from a descriptor and user-supplied build callback.
     */
    async buildQueuedIntent(descriptor, build) {
        this._assertWritableDescriptor(descriptor);
        // descriptor.intentQueue is already typed by the hydration path.
        const { intentQueue } = descriptor;
        const { state, allPatches } = await this._materializeDescriptor(descriptor, {
            collectReceipts: false,
            ceiling: null,
        });
        const builder = this._buildQueuedIntentBuilder(descriptor, state, allPatches);
        await build(builder);
        return this._freezeQueuedIntent(descriptor, intentQueue, builder);
    }
    /**
     * Update the strand descriptor and graph caches after a successful overlay commit.
     */
    async syncOverlayDescriptor(descriptor, { patch, sha }) {
        const now = String(this._graph._maxObservedLamport);
        const nextDescriptor = {
            ...descriptor,
            updatedAt: now,
            overlay: {
                ...descriptor.overlay,
                headPatchSha: sha,
                patchCount: descriptor.overlay.patchCount + 1,
            },
        };
        await this._writeDescriptor(nextDescriptor);
        if (patch.lamport > this._graph._maxObservedLamport) {
            this._graph._maxObservedLamport = patch.lamport;
        }
        this._graph._stateDirty = true;
        this._graph._cachedViewHash = null;
        this._graph._cachedCeiling = null;
        this._graph._cachedFrontier = null;
    }
    /**
     * Encode, persist, and commit a single queued patch to the overlay chain.
     */
    async commitQueuedPatch({ strandId, overlayId, parentSha, patch, contentBlobOids, lamport, }) {
        const committedPatch = {
            ...patch,
            writer: overlayId,
            lamport,
        };
        const patchBlobOid = await this._writeCommittedPatchBlob(committedPatch, overlayId);
        const patchStorage = this._resolveCommittedPatchStorage();
        const treeEntries = this._buildPatchTreeEntries(patchBlobOid, contentBlobOids, patchStorage);
        const treeOid = await this._graph._persistence.writeTree(treeEntries);
        const sha = await this._commitPatchTree({
            treeOid,
            overlayId,
            lamport,
            patchBlobOid,
            patchStorage,
            schema: committedPatch.schema,
            parentSha,
        });
        await this._graph._persistence.updateRef(this._buildOverlayRef(strandId), sha);
        return {
            sha,
            patch: committedPatch,
        };
    }
    async _createPatchBuilderForDescriptor(descriptor) {
        this._assertWritableDescriptor(descriptor);
        const { state, allPatches } = await this._materializeDescriptor(descriptor, {
            collectReceipts: false,
            ceiling: null,
        });
        return new PatchBuilder(this._buildOverlayPatchBuilderOptions(descriptor, state, allPatches));
    }
    _buildPatchBuilderOptions({ descriptor, lamport, versionVector, getCurrentState, expectedParentSha, targetRefPath, onCommitSuccess, }) {
        const pbOpts = {
            persistence: this._graph._persistence,
            graphName: this._graph._graphName,
            writerId: descriptor.overlay.overlayId,
            lamport,
            versionVector,
            getCurrentState,
            expectedParentSha,
            onDeleteWithData: this._graph._onDeleteWithData,
        };
        if (targetRefPath !== undefined) {
            pbOpts.targetRefPath = targetRefPath;
        }
        if (onCommitSuccess !== undefined) {
            pbOpts.onCommitSuccess = onCommitSuccess;
        }
        this._attachOptionalPatchBuilderDeps(pbOpts);
        return pbOpts;
    }
    _assertWritableDescriptor(descriptor) {
        if (!descriptor.overlay.writable) {
            throw new StrandError(`Strand '${descriptor.strandId}' has no active writable overlay in its current braid configuration`, {
                code: 'E_STRAND_INVALID_ARGS',
                context: { strandId: descriptor.strandId, writable: false },
            });
        }
    }
    _buildQueuedIntentBuilder(descriptor, state, allPatches) {
        return new PatchBuilder(this._buildPatchBuilderOptions({
            descriptor,
            lamport: maxPatchLamport(allPatches) + 1,
            versionVector: state.observedFrontier,
            getCurrentState: () => state,
            expectedParentSha: descriptor.overlay.headPatchSha ?? null,
        }));
    }
    _freezeQueuedIntent(descriptor, intentQueue, builder) {
        const patch = builder.build();
        if (!Array.isArray(patch.ops) || patch.ops.length === 0) {
            throw new StrandError('Cannot queue empty strand intent: no operations added', {
                code: 'E_STRAND_EMPTY_INTENT',
            });
        }
        return Object.freeze({
            intentId: this._buildIntentId(descriptor.strandId, intentQueue.nextIntentSeq),
            enqueuedAt: String(this._graph._maxObservedLamport),
            patch,
            reads: normalizeStringArray(patch.reads, 'reads[]'),
            writes: normalizeStringArray(patch.writes, 'writes[]'),
            contentBlobOids: normalizeStringArray(readBuilderContentBlobs(builder), 'contentBlobOids[]'),
        });
    }
    async _writeCommittedPatchBlob(committedPatch, overlayId) {
        const journal = this._graph._patchJournal;
        if (journal !== undefined && journal !== null) {
            return await journal.writePatch(committedPatch);
        }
        const patchCbor = this._graph._codec.encode(committedPatch);
        return this._graph._patchBlobStorage
            ? await this._graph._patchBlobStorage.store(patchCbor, {
                slug: `${this._graph._graphName}/${overlayId}/patch`,
            })
            : await this._graph._persistence.writeBlob(patchCbor);
    }
    _resolveCommittedPatchStorage() {
        const journal = this._graph._patchJournal;
        if (journal !== undefined && journal !== null) {
            return journal.writeStorage ?? LEGACY_GIT_BLOB_PATCH_STORAGE;
        }
        return this._graph._patchBlobStorage
            ? LEGACY_EXTERNAL_PATCH_STORAGE
            : LEGACY_GIT_BLOB_PATCH_STORAGE;
    }
    _buildPatchTreeEntries(patchBlobOid, contentBlobOids, patchStorage) {
        const treeEntries = [isGitCasPatchStorage(patchStorage)
                ? `040000 tree ${patchBlobOid}\tpatch`
                : `100644 blob ${patchBlobOid}\tpatch.cbor`];
        const uniqueBlobOids = [...new Set(contentBlobOids)];
        for (const blobOid of uniqueBlobOids) {
            treeEntries.push(`040000 tree ${blobOid}\t_content_${blobOid}`);
        }
        return treeEntries;
    }
    async _commitPatchTree({ treeOid, overlayId, lamport, patchBlobOid, patchStorage, schema, parentSha, }) {
        const commitMessage = this._graph._commitMessageCodec.encodePatch({
            kind: 'patch',
            graph: this._graph._graphName,
            writer: overlayId,
            lamport,
            patchOid: patchBlobOid,
            schema,
            storage: patchStorage,
        });
        const parents = parentSha !== null ? [parentSha] : [];
        return await this._graph._persistence.commitNodeWithTree({
            treeOid,
            parents,
            message: commitMessage,
        });
    }
    _attachOptionalPatchBuilderDeps(pbOpts) {
        this._attachOptionalPatchJournal(pbOpts);
        this._attachOptionalCommitMessageCodec(pbOpts);
        this._attachOptionalLogger(pbOpts);
        this._attachOptionalBlobStorage(pbOpts);
    }
    _buildOverlayPatchBuilderOptions(descriptor, state, allPatches) {
        const overlayRef = this._buildOverlayRef(descriptor.strandId);
        // Use the strand-materialized state (not the live _cachedState) so that
        // PatchBuilder can see strand-local nodes/edges when validating operations.
        const strandState = state;
        return this._buildPatchBuilderOptions({
            descriptor,
            lamport: maxPatchLamport(allPatches) + 1,
            versionVector: state.observedFrontier,
            getCurrentState: () => strandState,
            expectedParentSha: descriptor.overlay.headPatchSha ?? null,
            targetRefPath: overlayRef,
            onCommitSuccess: async (result) => {
                await this.syncOverlayDescriptor(descriptor, result);
            },
        });
    }
    _attachOptionalPatchJournal(pbOpts) {
        if (this._graph._patchJournal !== null && this._graph._patchJournal !== undefined) {
            pbOpts.patchJournal = this._graph._patchJournal;
        }
    }
    _attachOptionalCommitMessageCodec(pbOpts) {
        pbOpts.commitMessageCodec = this._graph._commitMessageCodec;
    }
    _attachOptionalLogger(pbOpts) {
        if (this._graph._logger !== null && this._graph._logger !== undefined) {
            pbOpts.logger = this._graph._logger;
        }
    }
    _attachOptionalBlobStorage(pbOpts) {
        if (this._graph._blobStorage !== null && this._graph._blobStorage !== undefined) {
            pbOpts.blobStorage = this._graph._blobStorage;
        }
    }
}
