/**
 * CheckpointController — snapshot lifecycle, GC, and coverage anchoring.
 *
 * Extracted from checkpoint.methods.js. WarpRuntime delegates to this
 * controller via defineProperty loops on the prototype.
 *
 * @module domain/services/controllers/CheckpointController
 */
import QueryError from "../../errors/QueryError.js";
import { SchemaUnsupportedError } from "../../errors/index.js";
import { buildWriterRef, buildCheckpointRef, buildCoverageRef } from "../../utils/RefLayout.js";
import { createFrontier, updateFrontier, frontierFingerprint } from "../Frontier.js";
import { CURRENT_CHECKPOINT_SCHEMA, isCurrentCheckpointSchema, } from "../state/checkpointHelpers.js";
import { loadCheckpoint } from "../state/checkpointLoad.js";
import { create as createCheckpointCommit } from "../state/checkpointCreate.js";
import executeGC from "../executeGC.js";
import GCMetrics from "../GCMetrics.js";
import { computeAppliedVV } from "../state/CheckpointSerializer.js";
import { cloneState } from "../JoinReducer.js";
import { E_NO_STATE_MSG } from "./QueryStateMessages.js";
/**
 * Narrows codec-decode output to `object | null`. CodecPort.decode
 * currently returns a loose type (0025B1); wrapping the call in a
 * dedicated narrowing function keeps that looseness inside this helper.
 */
function codecDecodeAsObject(codec, bytes) {
    const out = codec.decode(bytes);
    if (out === null || out === undefined || typeof out !== 'object') {
        return null;
    }
    return out;
}
/**
 * Narrows a decoded patch to just the schema marker this controller
 * needs. `in` check walks the shape defensively — callers branch on a
 * typed `schema` field.
 */
function decodePatchSchema(decoded) {
    if (decoded === null) {
        return {};
    }
    if (!('schema' in decoded)) {
        return {};
    }
    const { schema } = decoded;
    return typeof schema === 'number' ? { schema } : {};
}
function assertPatchLoaderSurface(host) {
    void host;
}
export default class CheckpointController {
    _host;
    constructor(host) {
        this._host = host;
    }
    async createCheckpoint() {
        const h = this._host;
        const writers = await h.discoverWriters();
        const frontier = createFrontier();
        const parents = [];
        for (const writerId of writers) {
            const writerRef = buildWriterRef(h._graphName, writerId);
            const sha = await h._persistence.readRef(writerRef);
            if (typeof sha === 'string' && sha.length > 0) {
                updateFrontier(frontier, writerId, sha);
                parents.push(sha);
            }
        }
        const stateCache = h._stateCache ?? null;
        const coordinate = this._coordinateForCheckpoint(frontier);
        if (stateCache !== null) {
            const exactSnapshot = await stateCache.getExact(coordinate);
            if (exactSnapshot !== null) {
                const pinned = exactSnapshot.retention === 'pinned'
                    ? exactSnapshot
                    : await stateCache.pin(exactSnapshot.snapshotId);
                await stateCache.publishCheckpointHead(h._graphName, pinned.snapshotId);
                return pinned.snapshotId;
            }
        }
        const prevCheckpointing = h._checkpointing;
        h._checkpointing = true;
        let state;
        try {
            state = this._requireCheckpointReadingState();
        }
        finally {
            h._checkpointing = prevCheckpointing;
        }
        if (stateCache !== null) {
            const stored = await stateCache.put(await this._buildSnapshotRecord({
                state,
                coordinate,
                provenancePosture: 'full',
            }));
            const pinned = await stateCache.pin(stored.snapshotId);
            await stateCache.publishCheckpointHead(h._graphName, pinned.snapshotId);
            return pinned.snapshotId;
        }
        let indexTree = h._cachedIndexTree;
        if ((indexTree === null || indexTree === undefined) && h._viewService !== null && h._viewService !== undefined) {
            try {
                const { tree } = h._viewService.build(state);
                indexTree = tree;
            }
            catch (err) {
                const message = err instanceof Error ? err.message : String(err);
                h._logger?.warn('[warp] checkpoint index build failed; saving checkpoint without index', {
                    error: message,
                });
                indexTree = null;
            }
        }
        const persistence = h._persistence;
        this._assertCheckpointCreatePersistence(persistence);
        const checkpointStore = h._checkpointStore ?? undefined;
        const stateHashService = h._stateHashService ?? null;
        const checkpointSha = await createCheckpointCommit({
            persistence,
            graphName: h._graphName,
            state,
            frontier,
            parents,
            ...(h._provenanceIndex ? { provenanceIndex: h._provenanceIndex } : {}),
            crypto: h._crypto,
            codec: h._codec,
            commitMessageCodec: h._commitMessageCodec,
            ...(indexTree ? { indexTree } : {}),
            ...(checkpointStore ? { checkpointStore } : {}),
            ...(stateHashService ? { stateHashService } : {}),
        });
        const checkpointRef = buildCheckpointRef(h._graphName);
        await h._persistence.updateRef(checkpointRef, checkpointSha);
        return checkpointSha;
    }
    async _readCheckpointSha() {
        return await this._host._persistence.readRef(buildCheckpointRef(this._host._graphName));
    }
    _requireCheckpointReadingState() {
        const h = this._host;
        if (h._cachedState !== null && !h._stateDirty) {
            return h._cachedState;
        }
        throw new QueryError(E_NO_STATE_MSG, { code: 'E_NO_STATE' });
    }
    _coordinateForCheckpoint(frontier) {
        return {
            frontier,
            ceiling: null,
        };
    }
    async _buildSnapshotRecord(params) {
        const stateHash = await this._computeStateHash(params.state, params.coordinate);
        return {
            snapshotId: `snapshot:${stateHash}`,
            coordinate: params.coordinate,
            retention: 'evictable',
            provenancePosture: params.provenancePosture,
            stateHash,
            payloadRef: `snapshot:${stateHash}`,
            createdAt: 'checkpoint-create',
            state: params.state,
        };
    }
    async _computeStateHash(state, coordinate) {
        const h = this._host;
        if (h._stateHashService !== null && h._stateHashService !== undefined) {
            return await h._stateHashService.compute(state);
        }
        return `frontier:${frontierFingerprint(coordinate.frontier)}:${coordinate.ceiling === null ? 'head' : String(coordinate.ceiling)}`;
    }
    _assertCheckpointCreatePersistence(persistence) {
        void persistence;
    }
    _assertLoadPersistence(persistence) {
        void persistence;
    }
    async syncCoverage() {
        const h = this._host;
        const writers = await h.discoverWriters();
        if (writers.length === 0) {
            return;
        }
        const parents = [];
        for (const writerId of writers) {
            const writerRef = buildWriterRef(h._graphName, writerId);
            const sha = await h._persistence.readRef(writerRef);
            if (typeof sha === 'string' && sha.length > 0) {
                parents.push(sha);
            }
        }
        if (parents.length === 0) {
            return;
        }
        const message = h._commitMessageCodec.encodeAnchor({
            kind: 'anchor',
            graph: h._graphName,
            schema: 2,
        });
        const anchorSha = await h._persistence.commitNode({ message, parents });
        const coverageRef = buildCoverageRef(h._graphName);
        await h._persistence.updateRef(coverageRef, anchorSha);
    }
    async _loadLatestCheckpoint() {
        const h = this._host;
        const stateCache = h._stateCache ?? null;
        if (stateCache !== null) {
            const snapshotHead = await stateCache.resolveCheckpointHead(h._graphName);
            if (snapshotHead !== null && snapshotHead.state !== undefined) {
                return {
                    state: snapshotHead.state,
                    frontier: snapshotHead.coordinate.frontier,
                    stateHash: snapshotHead.stateHash,
                    schema: CURRENT_CHECKPOINT_SCHEMA,
                    appliedVV: null,
                    indexShardOids: null,
                };
            }
        }
        const checkpointRef = buildCheckpointRef(h._graphName);
        const checkpointSha = await h._persistence.readRef(checkpointRef);
        if (typeof checkpointSha !== 'string' || checkpointSha.length === 0) {
            return null;
        }
        try {
            const checkpointStore = h._checkpointStore ?? undefined;
            this._assertLoadPersistence(h._persistence);
            return await loadCheckpoint(h._persistence, checkpointSha, {
                codec: h._codec,
                ...(checkpointStore ? { checkpointStore } : {}),
                commitMessageCodec: h._commitMessageCodec,
            });
        }
        catch (err) {
            const msg = err instanceof Error ? err.message : '';
            if (msg.includes('missing') ||
                msg.includes('not found') ||
                msg.includes('ENOENT') ||
                msg.includes('non-empty string')) {
                return null;
            }
            throw err;
        }
    }
    async _loadPatchesSince(checkpoint) {
        const h = this._host;
        assertPatchLoaderSurface(h);
        const writerIds = await h.discoverWriters();
        const allPatches = [];
        for (const writerId of writerIds) {
            const checkpointSha = checkpoint.frontier?.get(writerId) ?? null;
            const patches = await h._loadWriterPatches(writerId, checkpointSha);
            const lastPatch = patches[patches.length - 1];
            if (lastPatch !== undefined) {
                const tipSha = lastPatch.sha;
                await h._validatePatchAgainstCheckpoint(writerId, tipSha, checkpoint);
            }
            for (const p of patches) {
                allPatches.push(p);
            }
        }
        return allPatches;
    }
    async _validateMigrationBoundary() {
        const checkpoint = await this._loadLatestCheckpoint();
        if (isCurrentCheckpointSchema(checkpoint?.schema)) {
            return;
        }
        const hasSchema1History = await this._hasSchema1Patches();
        if (hasSchema1History) {
            throw new SchemaUnsupportedError('Cannot open graph with retired patch history. Run `npm run upgrade -- --graph <name>` first.');
        }
    }
    async _hasSchema1Patches() {
        const h = this._host;
        const writerIds = await h.discoverWriters();
        for (const writerId of writerIds) {
            const writerRef = buildWriterRef(h._graphName, writerId);
            const tipSha = await h._persistence.readRef(writerRef);
            if (typeof tipSha !== 'string' || tipSha.length === 0) {
                continue;
            }
            const nodeInfo = await h._persistence.getNodeInfo(tipSha);
            const kind = h._commitMessageCodec.detectKind(nodeInfo.message);
            if (kind === 'patch') {
                const patchMeta = h._commitMessageCodec.decodePatch(nodeInfo.message);
                // Runtime-narrow the decoded patch shape here rather than trusting
                // the loose CodecPort return surface.
                const patchBuffer = await h._readPatchBlob(patchMeta);
                const decoded = decodePatchSchema(codecDecodeAsObject(h._codec, patchBuffer));
                if (decoded.schema === 1 || decoded.schema === undefined) {
                    return true;
                }
            }
        }
        return false;
    }
    _maybeRunGC(state) {
        const h = this._host;
        try {
            const metrics = GCMetrics.fromState(state);
            const policy = h._gcPolicy;
            const { shouldRun, reasons } = policy.evaluate({
                tombstoneRatio: metrics.tombstoneRatio,
                totalEntries: metrics.totalEntries,
                patchesSinceCompaction: h._patchesSinceGC,
                ticksSinceCompaction: h._lastGCLamport > 0 ? h._maxObservedLamport - h._lastGCLamport : 0,
            });
            if (!shouldRun) {
                return;
            }
            if (policy.enabled) {
                const preGcFingerprint = h._lastFrontier
                    ? frontierFingerprint(h._lastFrontier)
                    : null;
                const clonedState = cloneState(state);
                const appliedVV = computeAppliedVV(clonedState);
                const result = executeGC(clonedState, appliedVV);
                const postGcFingerprint = h._lastFrontier
                    ? frontierFingerprint(h._lastFrontier)
                    : null;
                if (preGcFingerprint !== postGcFingerprint) {
                    h._stateDirty = true;
                    h._cachedViewHash = null;
                    if (h._logger) {
                        h._logger.warn('Auto-GC discarded: frontier changed during compaction (concurrent write)', { reasons, preGcFingerprint, postGcFingerprint });
                    }
                    return;
                }
                this._installCompactedState(clonedState);
                h._lastGCLamport = h._maxObservedLamport;
                h._patchesSinceGC = 0;
                if (h._logger) {
                    h._logger.info('Auto-GC completed', { ...result, reasons });
                }
            }
            else if (h._logger) {
                h._logger.warn('GC thresholds exceeded but auto-GC is disabled. Set gcPolicy: { enabled: true } to auto-compact.', { reasons });
            }
        }
        catch (err) {
            if (h._logger) {
                let error = 'non-Error thrown value';
                if (err instanceof Error) {
                    error = err.message;
                }
                else if (err === null) {
                    error = 'null';
                }
                else if (err === undefined) {
                    error = 'undefined';
                }
                else if (typeof err === 'string') {
                    error = err;
                }
                else if (typeof err === 'number'
                    || typeof err === 'boolean'
                    || typeof err === 'bigint'
                    || typeof err === 'symbol') {
                    error = String(err);
                }
                h._logger.warn('Auto-GC failed; materialize will continue.', { error });
            }
            // GC failure never breaks materialize
        }
    }
    maybeRunGC() {
        const h = this._host;
        if (!h._cachedState) {
            return { ran: false, result: null, reasons: [] };
        }
        const rawMetrics = GCMetrics.fromState(h._cachedState);
        const policy = h._gcPolicy;
        const { shouldRun, reasons } = policy.evaluate({
            tombstoneRatio: rawMetrics.tombstoneRatio,
            totalEntries: rawMetrics.totalEntries,
            patchesSinceCompaction: h._patchesSinceGC,
            ticksSinceCompaction: h._lastGCLamport > 0 ? h._maxObservedLamport - h._lastGCLamport : 0,
        });
        if (!shouldRun) {
            return { ran: false, result: null, reasons: [] };
        }
        const result = this.runGC();
        return { ran: true, result, reasons: [...reasons] };
    }
    runGC() {
        const h = this._host;
        if (!h._cachedState) {
            throw new QueryError(E_NO_STATE_MSG, { code: 'E_NO_STATE' });
        }
        const preGcFingerprint = h._lastFrontier
            ? frontierFingerprint(h._lastFrontier)
            : null;
        const clonedState = cloneState(h._cachedState);
        const appliedVV = computeAppliedVV(clonedState);
        const result = executeGC(clonedState, appliedVV);
        const postGcFingerprint = h._lastFrontier
            ? frontierFingerprint(h._lastFrontier)
            : null;
        if (preGcFingerprint !== postGcFingerprint) {
            throw new QueryError('GC aborted: frontier changed during compaction (concurrent write detected)', { code: 'E_GC_STALE' });
        }
        this._installCompactedState(clonedState);
        h._lastGCLamport = h._maxObservedLamport;
        h._patchesSinceGC = 0;
        return result;
    }
    _installCompactedState(state) {
        const h = this._host;
        h._cachedState = state;
        h._materializedGraph = null;
        h._logicalIndex = null;
        h._propertyReader = null;
        h._cachedIndexTree = null;
        h._cachedViewHash = null;
    }
    getGCMetrics() {
        const h = this._host;
        if (!h._cachedState) {
            return null;
        }
        const rawMetrics = GCMetrics.fromState(h._cachedState);
        return {
            nodeCount: rawMetrics.nodeLiveDots,
            edgeCount: rawMetrics.edgeLiveDots,
            tombstoneCount: rawMetrics.totalTombstones,
            tombstoneRatio: rawMetrics.tombstoneRatio,
            patchesSinceCompaction: h._patchesSinceGC,
            lastCompactionLamport: h._lastGCLamport,
        };
    }
}
