/**
 * PatchCommitter — persistence pipeline for committing a built patch.
 *
 * Extracted from PatchBuilder to keep file sizes under 500 LOC.
 * Handles: CAS race detection, lamport resolution, blob persistence,
 * tree construction, commit creation, ref update, and success callback.
 *
 * @module domain/services/PatchCommitter
 */
import VersionVector from "../crdt/VersionVector.js";
import Patch from "../types/Patch.js";
import { lowerCanonicalOp } from "./OpNormalizer.js";
import { buildWriterRef } from "../utils/RefLayout.js";
import WriterError from "../errors/WriterError.js";
import PatchError from "../errors/PatchError.js";
import PersistenceError from "../errors/PersistenceError.js";
import { DEFAULT_COMMIT_MESSAGE_CODEC } from "./codec/WarpMessageCodec.js";
import { isGitCasPatchStorage, } from "../../ports/CommitMessageCodecPort.js";
/**
 * Commits a patch built by PatchBuilder to the Git object store.
 *
 * Steps: CAS check → lamport resolution → build Patch → persist blob →
 * build tree → create commit → update ref → invoke callback.
 */
export async function commitPatch(state) {
    if (state.ops.length === 0) {
        throw new PatchError('Cannot commit empty patch: no operations added', { code: 'E_PATCH_EMPTY' });
    }
    // CAS: check if writer ref has advanced since builder creation
    const writerRef = (state.targetRefPath !== null && state.targetRefPath !== '')
        ? state.targetRefPath
        : buildWriterRef(state.graphName, state.writerId);
    const currentRefSha = await state.persistence.readRef(writerRef);
    if (currentRefSha !== state.expectedParentSha) {
        throw buildWriterCasConflict(state.expectedParentSha, currentRefSha);
    }
    // Lamport resolution from parent chain
    let { lamport } = state;
    let parentCommit = null;
    if (currentRefSha !== null && currentRefSha !== undefined && currentRefSha !== '') {
        parentCommit = currentRefSha;
        const commitMessage = await state.persistence.showNode(currentRefSha);
        const kind = state.commitMessageCodec.detectKind(commitMessage);
        if (kind === 'patch') {
            let patchInfo;
            try {
                patchInfo = state.commitMessageCodec.decodePatch(commitMessage);
            }
            catch (err) {
                throw new PatchError(`Failed to parse lamport from writer ref ${writerRef}: ` +
                    `commit ${currentRefSha} has invalid patch message format`, {
                    code: 'E_PATCH_LAMPORT_PARSE',
                    context: { writerRef, currentRefSha, cause: err instanceof Error ? err.message : String(err) },
                });
            }
            lamport = Math.max(state.lamport, patchInfo.lamport + 1);
        }
    }
    // Build Patch
    const schema = state.hasEdgeProps ? 3 : 2;
    const rawOps = state.ops.map((op) => lowerCanonicalOp(op));
    const patch = new Patch({
        schema,
        writer: state.writerId,
        lamport,
        context: VersionVector.serialize(state.vv),
        ops: rawOps,
        reads: [...state.observedOperands].sort(),
        writes: [...state.writes].sort(),
    });
    // Persist patch blob
    if (state.patchJournal === null || state.patchJournal === undefined) {
        throw new PersistenceError('patchJournal is required for committing patches', 'E_MISSING_JOURNAL');
    }
    const patchBlobOid = await state.patchJournal.writePatch(patch);
    const patchStorage = state.patchJournal.writeStorage;
    // Build tree with patch blob + content blobs
    const treeEntries = [isGitCasPatchStorage(patchStorage)
            ? `040000 tree ${patchBlobOid}\tpatch`
            : `100644 blob ${patchBlobOid}\tpatch.cbor`];
    const uniqueBlobs = [...new Set(state.contentBlobs)];
    for (const blobOid of uniqueBlobs) {
        treeEntries.push(`040000 tree ${blobOid}\t_content_${blobOid}`);
    }
    const treeOid = await state.persistence.writeTree(treeEntries);
    // Create commit
    const messageCodec = state.commitMessageCodec ?? DEFAULT_COMMIT_MESSAGE_CODEC;
    const message = messageCodec.encodePatch({
        kind: 'patch',
        graph: state.graphName,
        writer: state.writerId,
        lamport,
        patchOid: patchBlobOid,
        schema,
        storage: patchStorage,
    });
    const parents = (parentCommit !== null && parentCommit !== '') ? [parentCommit] : [];
    const newCommitSha = await state.persistence.commitNodeWithTree({
        treeOid, parents, message,
    });
    // Atomically update writer ref
    await compareAndSwapWriterRef(state.persistence, writerRef, newCommitSha, state.expectedParentSha);
    await assertWriterRefVisible(state.persistence, writerRef, newCommitSha);
    // Invoke success callback
    if (state.onCommitSuccess) {
        try {
            await state.onCommitSuccess({ patch, sha: newCommitSha });
        }
        catch (err) {
            const errValue = err instanceof Error ? err : String(err);
            state.logger.warn(`[warp] onCommitSuccess callback failed (sha=${newCommitSha}):`, { error: errValue });
        }
    }
    return newCommitSha;
}
/** Builds a WriterError that preserves expected and actual writer-ref heads. */
function buildWriterCasConflict(expectedSha, actualSha) {
    const err = new WriterError('Commit failed: writer ref was updated by another process. Re-materialize and retry.', { code: 'WRITER_CAS_CONFLICT' });
    err.expectedSha = expectedSha;
    err.actualSha = actualSha;
    return err;
}
/** Advances a writer ref atomically and translates stale-head failures. */
async function compareAndSwapWriterRef(persistence, writerRef, newCommitSha, expectedSha) {
    try {
        await persistence.compareAndSwapRef(writerRef, newCommitSha, expectedSha);
    }
    catch (err) {
        const actualSha = await persistence.readRef(writerRef);
        if (actualSha === newCommitSha) {
            return;
        }
        if (actualSha !== expectedSha) {
            throw buildWriterCasConflict(expectedSha, actualSha);
        }
        throw err;
    }
}
/** Verifies that a successful commit is immediately visible at the writer ref. */
async function assertWriterRefVisible(persistence, writerRef, expectedSha) {
    const actualSha = await persistence.readRef(writerRef);
    if (actualSha === expectedSha) {
        return;
    }
    throw new PersistenceError(`Commit ${expectedSha} is not visible at writer ref ${writerRef}`, PersistenceError.E_REF_IO, {
        context: {
            writerRef,
            expectedSha,
            actualSha: actualSha ?? '(missing)',
        },
    });
}
