/**
 * PatchController — state mutation, writer lifecycle, discovery, and CRDT join.
 *
 * Extracted from patch.methods.js. WarpRuntime delegates to this controller
 * via defineProperty loops on the prototype.
 *
 * Discovery helpers (lamport, chain loading, tick enumeration) live in
 * PatchDiscovery.ts and are composed here.
 *
 * @module domain/services/controllers/PatchController
 */
import { PatchBuilder } from "../PatchBuilder.js";
import { joinStates, applyWithDiff, applyWithReceipt } from "../JoinReducer.js";
import { buildWriterRef } from "../../utils/RefLayout.js";
import { Writer } from "../../warp/Writer.js";
import { resolveWriterId } from "../../utils/WriterId.js";
import EncryptionError from "../../errors/EncryptionError.js";
import PersistenceError from "../../errors/PersistenceError.js";
import PatchError from "../../errors/PatchError.js";
import QueryError from "../../errors/QueryError.js";
import { PatchDiscovery, } from "./PatchDiscovery.js";
import { E_NO_STATE_MSG, E_STALE_STATE_MSG } from "./QueryStateMessages.js";
/**
 * PatchController-level assertion that _persistence implements ConfigPort.
 * The underlying adapter provides ConfigPort methods (configGet/configSet)
 * but the narrow CorePersistence type doesn't carry them. This assertion
 * declares the runtime compatibility without a value-level cast.
 */
function assertConfigPortPersistence(host) {
    void host;
}
// ── PatchController ───────────────────────────────────────────────────────────
/**
 * Handles state mutation, writer lifecycle, discovery, and CRDT join
 * on behalf of WarpRuntime.
 */
export default class PatchController {
    _host;
    _discovery;
    constructor(host) {
        this._host = host;
        this._discovery = new PatchDiscovery(host);
    }
    // ── Patch creation ──────────────────────────────────────────────────────────
    /**
     * Creates a new PatchBuilder for this graph.
     */
    async createPatch() {
        const h = this._host;
        const { lamport, parentSha } = await this._nextLamport();
        const opts = {
            persistence: h._persistence,
            graphName: h._graphName,
            writerId: h._writerId,
            lamport,
            versionVector: h._versionVector,
            getCurrentState: () => h._cachedState,
            expectedParentSha: parentSha,
            onDeleteWithData: h._onDeleteWithData,
            onCommitSuccess: (commitOpts) => this._onPatchCommitted(h._writerId, commitOpts),
            commitMessageCodec: h._commitMessageCodec,
        };
        if (h._patchJournal !== null && h._patchJournal !== undefined) {
            opts.patchJournal = h._patchJournal;
        }
        if (h._logger !== null && h._logger !== undefined) {
            opts.logger = h._logger;
        }
        if (h._blobStorage !== null && h._blobStorage !== undefined) {
            opts.blobStorage = h._blobStorage;
        }
        return new PatchBuilder(opts);
    }
    /**
     * Convenience wrapper: creates a patch, runs the callback, and commits.
     */
    async patch(build) {
        const h = this._host;
        if (h._patchInProgress) {
            throw new PatchError('graph.patch() is not reentrant. Use createPatch() for nested or concurrent patches.', { code: 'E_PATCH_REENTRANT' });
        }
        h._patchInProgress = true;
        try {
            const p = await this.createPatch();
            await build(p);
            return await p.commit();
        }
        finally {
            h._patchInProgress = false;
        }
    }
    /**
     * Applies multiple patches sequentially.
     */
    async patchMany(...builds) {
        if (builds.length === 0) {
            return [];
        }
        const shas = [];
        for (const build of builds) {
            shas.push(await this.patch(build));
        }
        return shas;
    }
    // ── Lamport + chain loading (delegate to PatchDiscovery) ────────────────────
    /**
     * Gets the next lamport timestamp and current parent SHA for this writer.
     */
    async _nextLamport() {
        const h = this._host;
        const writerRef = buildWriterRef(h._graphName, h._writerId);
        return await this._discovery.nextLamportFor(writerRef);
    }
    /**
     * Loads a patch chain starting from an explicit tip SHA.
     */
    async _loadPatchChainFromSha(tipSha, stopAtSha = null) {
        return await this._discovery.loadPatchChainFromSha(tipSha, stopAtSha);
    }
    /**
     * Loads all patches from a writer's ref chain.
     */
    async _loadWriterPatches(writerId, stopAtSha = null) {
        return await this._discovery.loadWriterPatches(writerId, stopAtSha);
    }
    /**
     * Returns patches from a writer's ref chain (public API).
     */
    async getWriterPatches(writerId, stopAtSha = null) {
        return await this._discovery.loadWriterPatches(writerId, stopAtSha);
    }
    /**
     * Discovers all writers that have written to this graph.
     */
    async discoverWriters() {
        return await this._discovery.discoverWriters();
    }
    /**
     * Discovers all distinct Lamport ticks across all writers.
     */
    async discoverTicks() {
        return await this._discovery.discoverTicks();
    }
    // ── Post-commit hook ────────────────────────────────────────────────────────
    /**
     * Post-commit hook: updates version vector, eager re-materialize,
     * provenance index, frontier, and audit service.
     */
    async _onPatchCommitted(writerId, { patch: committed, sha } = {}) {
        const h = this._host;
        h._versionVector.increment(writerId);
        if (committed?.lamport !== undefined && committed.lamport > h._maxObservedLamport) {
            h._maxObservedLamport = committed.lamport;
        }
        h._patchesSinceCheckpoint++;
        if (h._cachedState && !h._stateDirty && committed && typeof sha === 'string' && sha.length > 0) {
            let tickReceipt = null;
            let diff = null;
            if (h._auditService) {
                const result = applyWithReceipt(h._cachedState, committed, sha);
                tickReceipt = result.receipt;
            }
            else {
                const result = applyWithDiff(h._cachedState, committed, sha);
                diff = result.diff;
            }
            await h._setMaterializedState(h._cachedState, { diff });
            if (h._provenanceIndex) {
                h._provenanceIndex.addPatch(sha, committed.reads, committed.writes);
            }
            if (h._lastFrontier) {
                h._lastFrontier.set(writerId, sha);
            }
            if (h._auditService && tickReceipt) {
                try {
                    await h._auditService.commit(tickReceipt);
                }
                catch {
                    // Data commit already succeeded. Logged inside service.
                }
            }
        }
        else {
            h._stateDirty = true;
            h._cachedViewHash = null;
            if (h._auditService) {
                h._auditSkipCount++;
                h._logger?.warn('[warp:audit]', {
                    code: 'AUDIT_SKIPPED_DIRTY_STATE',
                    sha,
                    skipCount: h._auditSkipCount,
                });
            }
        }
    }
    // ── Writer lifecycle ────────────────────────────────────────────────────────
    /**
     * Creates a Writer bound to an existing (or resolved) writer ID.
     */
    async writer(writerId) {
        const h = this._host;
        assertConfigPortPersistence(h);
        const configGet = async (key) => await h._persistence.configGet(key);
        const configSet = async (key, value) => await h._persistence.configSet(key, value);
        const resolvedWriterId = await resolveWriterId({
            graphName: h._graphName,
            explicitWriterId: writerId,
            configGet,
            configSet,
        });
        const persistence = h._persistence;
        const patchJournal = h._patchJournal;
        if (patchJournal === null || patchJournal === undefined) {
            throw new PatchError('patchJournal is required for writer()', { code: 'E_MISSING_JOURNAL' });
        }
        const writerOpts = {
            persistence,
            graphName: h._graphName,
            writerId: resolvedWriterId,
            versionVector: h._versionVector,
            getCurrentState: () => h._cachedState,
            onDeleteWithData: h._onDeleteWithData,
            onCommitSuccess: (opts) => this._onPatchCommitted(resolvedWriterId, opts),
            patchJournal,
            commitMessageCodec: h._commitMessageCodec,
        };
        if (h._logger !== null && h._logger !== undefined) {
            writerOpts.logger = h._logger;
        }
        if (h._blobStorage !== null && h._blobStorage !== undefined) {
            writerOpts.blobStorage = h._blobStorage;
        }
        return new Writer(writerOpts);
    }
    // ── State helpers ───────────────────────────────────────────────────────────
    /**
     * Ensures cached state is fresh.
     */
    _ensureFreshState() {
        const h = this._host;
        if (!h._cachedState) {
            return Promise.reject(new QueryError(E_NO_STATE_MSG, { code: 'E_NO_STATE' }));
        }
        if (h._stateDirty) {
            return Promise.reject(new QueryError(E_STALE_STATE_MSG, { code: 'E_STALE_STATE' }));
        }
        return Promise.resolve();
    }
    /**
     * Reads a patch blob, using patchBlobStorage for encrypted patches.
     */
    async _readPatchBlob(patchMeta) {
        const h = this._host;
        const storage = patchMeta.storage ?? (patchMeta.encrypted === true
            ? { strategy: 'legacy-external-storage', version: null, schema: null, encrypted: true }
            : { strategy: 'legacy-git-blob', version: null, schema: null, encrypted: false });
        if (storage.strategy === 'git-cas') {
            if (!h._blobStorage) {
                throw new EncryptionError('This graph contains git-cas patches; provide blobStorage for CAS restore');
            }
            return await h._blobStorage.retrieve(patchMeta.patchOid);
        }
        if (storage.strategy === 'legacy-external-storage') {
            if (!h._patchBlobStorage) {
                throw new EncryptionError('This graph contains encrypted patches in legacy external storage; provide patchBlobStorage with an encryption key');
            }
            return await h._patchBlobStorage.retrieve(patchMeta.patchOid);
        }
        const blob = await h._persistence.readBlob(patchMeta.patchOid);
        if (blob === null || blob === undefined) {
            throw new PersistenceError(`Patch blob not found: ${patchMeta.patchOid}`, PersistenceError.E_MISSING_OBJECT, { context: { oid: patchMeta.patchOid } });
        }
        return blob;
    }
    // ── CRDT join ───────────────────────────────────────────────────────────────
    /**
     * Joins an external WarpState into the cached state using CRDT merge.
     */
    join(otherState) {
        const h = this._host;
        if (!h._cachedState) {
            throw new QueryError(E_NO_STATE_MSG, { code: 'E_NO_STATE' });
        }
        if (otherState === null ||
            otherState === undefined ||
            !('nodeAlive' in otherState) ||
            !('edgeAlive' in otherState)) {
            throw new QueryError('Invalid state: must be a valid WarpState object', { code: 'E_INVALID_STATE' });
        }
        const beforeNodes = h._cachedState.nodeAlive.elements().length;
        const beforeEdges = h._cachedState.edgeAlive.elements().length;
        const beforeFrontierSize = h._cachedState.observedFrontier.size;
        const mergedState = joinStates(h._cachedState, otherState);
        const afterNodes = mergedState.nodeAlive.elements().length;
        const afterEdges = mergedState.edgeAlive.elements().length;
        const afterFrontierSize = mergedState.observedFrontier.size;
        let propsChanged = 0;
        for (const entry of mergedState.nodeProperties()) {
            const oldReg = h._cachedState.getEncodedProp(entry.encodedKey);
            if (!oldReg || oldReg.value !== entry.register.value) {
                propsChanged++;
            }
        }
        for (const entry of mergedState.edgeProperties()) {
            const oldReg = h._cachedState.getEncodedProp(entry.encodedKey);
            if (!oldReg || oldReg.value !== entry.register.value) {
                propsChanged++;
            }
        }
        const receipt = {
            nodesAdded: Math.max(0, afterNodes - beforeNodes),
            nodesRemoved: Math.max(0, beforeNodes - afterNodes),
            edgesAdded: Math.max(0, afterEdges - beforeEdges),
            edgesRemoved: Math.max(0, beforeEdges - afterEdges),
            propsChanged,
            frontierMerged: afterFrontierSize !== beforeFrontierSize ||
                !this._frontierEquals(h._cachedState.observedFrontier, mergedState.observedFrontier),
        };
        h._cachedState = mergedState;
        h._versionVector = mergedState.observedFrontier.clone();
        const adjacency = h._buildAdjacency(mergedState);
        h._materializedGraph = { state: mergedState, stateHash: null, adjacency };
        h._logicalIndex = null;
        h._propertyReader = null;
        h._cachedViewHash = null;
        h._cachedIndexTree = null;
        h._stateDirty = false;
        return { state: mergedState, receipt };
    }
    /**
     * Compares two version vectors for equality.
     */
    _frontierEquals(a, b) {
        return a.equals(b);
    }
}
