/**
 * Writer - WARP writer abstraction for safe graph mutations.
 *
 * A Writer is the only way to mutate a WarpRuntime state. It owns a writerId
 * and maintains a single-writer chain under refs/warp/<graph>/writers/<writerId>.
 *
 * Key guarantees:
 * - Single-writer chain per writerId
 * - Ref-safe identity
 * - CAS-based updates to prevent concurrent forks
 * - Schema:2 only (Patch ops with OR-Set semantics)
 *
 * @module domain/warp/Writer
 * @see WARP Writer Spec v1
 */
import nullLogger from "../utils/nullLogger.js";
import { validateWriterId, buildWriterRef } from "../utils/RefLayout.js";
import { PatchSession } from "./PatchSession.js";
import { PatchBuilder } from "../services/PatchBuilder.js";
import { DEFAULT_COMMIT_MESSAGE_CODEC } from "../services/codec/WarpMessageCodec.js";
import WriterError from "../errors/WriterError.js";
// Re-export for backward compatibility — consumers importing from Writer.ts
// should migrate to importing from '../errors/WriterError.ts' directly.
export { WriterError };
/**
 * Asserts that a Lamport timestamp is a valid positive finite integer.
 */
function _assertValidLamport(lamport, commitSha) {
    if (typeof lamport !== 'number' || !Number.isFinite(lamport) || lamport < 1) {
        throw new WriterError(`Malformed Lamport timestamp in commit ${commitSha}: ${JSON.stringify(lamport)}`, // nosemgrep: ts-no-json-stringify-in-core -- 0025B
        { code: 'E_LAMPORT_CORRUPT' });
    }
}
/** Validates that a PatchJournalPort is provided. */
function _validateJournal(patchJournal) {
    if (patchJournal === null || patchJournal === undefined) {
        throw new WriterError('patchJournal is required — Writer.beginPatch() produces patches that must be persisted via a PatchJournalPort.', { code: 'E_MISSING_JOURNAL' });
    }
}
/**
 * Writer class for creating and committing patches to a WARP graph.
 */
export class Writer {
    _persistence;
    _graphName;
    _writerId;
    _versionVector;
    _getCurrentState;
    _onCommitSuccess;
    _onDeleteWithData;
    _patchJournal;
    _commitMessageCodec;
    _logger;
    _blobStorage;
    _commitInProgress;
    constructor(opts) {
        validateWriterId(opts.writerId);
        _validateJournal(opts.patchJournal);
        this._persistence = opts.persistence;
        this._graphName = opts.graphName;
        this._writerId = opts.writerId;
        this._versionVector = opts.versionVector;
        this._getCurrentState = opts.getCurrentState;
        this._onCommitSuccess = opts.onCommitSuccess;
        this._onDeleteWithData = opts.onDeleteWithData ?? 'warn';
        this._patchJournal = opts.patchJournal;
        this._commitMessageCodec = opts.commitMessageCodec ?? DEFAULT_COMMIT_MESSAGE_CODEC;
        this._logger = opts.logger ?? nullLogger;
        this._blobStorage = opts.blobStorage ?? null;
        this._commitInProgress = false;
    }
    /** Gets the writer ID. */
    get writerId() {
        return this._writerId;
    }
    /** Gets the graph name. */
    get graphName() {
        return this._graphName;
    }
    /**
     * Gets the current writer head SHA.
     *
     * @returns The tip SHA or null if no commits yet
     */
    async head() {
        const writerRef = buildWriterRef(this._graphName, this._writerId);
        return await this._persistence.readRef(writerRef);
    }
    /**
     * Begins a new patch session.
     *
     * Reads the current writer head and captures it as the expected parent
     * for CAS-based commit.
     */
    async beginPatch() {
        const persistence = this._persistence;
        const graphName = this._graphName;
        const writerId = this._writerId;
        const writerRef = buildWriterRef(graphName, writerId);
        const expectedOldHead = await persistence.readRef(writerRef);
        const lamport = await this._resolveNextLamport(expectedOldHead);
        const builderOpts = this._buildPatchOpts({ persistence, graphName, writerId, lamport, expectedParentSha: expectedOldHead });
        const builder = new PatchBuilder(builderOpts);
        return new PatchSession({ builder, persistence, graphName, writerId, expectedOldHead });
    }
    /**
     * Constructs PatchBuilder options from Writer state.
     */
    _buildPatchOpts(core) {
        const opts = {
            ...core,
            versionVector: this._versionVector.clone(),
            getCurrentState: this._getCurrentState,
            onDeleteWithData: this._onDeleteWithData,
            patchJournal: this._patchJournal,
            commitMessageCodec: this._commitMessageCodec,
            logger: this._logger,
        };
        if (this._onCommitSuccess) {
            opts.onCommitSuccess = this._onCommitSuccess;
        }
        if (this._blobStorage) {
            opts.blobStorage = this._blobStorage;
        }
        return opts;
    }
    /**
     * Reads the previous commit's Lamport timestamp and returns the next value.
     */
    async _resolveNextLamport(headSha) {
        if (headSha === null || headSha === undefined) {
            return 1;
        }
        const commitMessage = await this._persistence.showNode(headSha);
        if (this._commitMessageCodec.detectKind(commitMessage) !== 'patch') {
            return 1;
        }
        const { lamport } = this._commitMessageCodec.decodePatch(commitMessage);
        _assertValidLamport(lamport, headSha);
        return lamport + 1;
    }
    /**
     * Convenience method to build and commit a patch in one call.
     */
    async commitPatch(build) {
        if (this._commitInProgress) {
            throw new WriterError('commitPatch() is not reentrant. Use beginPatch() for nested or concurrent patches.', { code: 'COMMIT_IN_PROGRESS' });
        }
        this._commitInProgress = true;
        try {
            const patch = await this.beginPatch();
            await build(patch);
            return await patch.commit();
        }
        finally {
            this._commitInProgress = false;
        }
    }
}
