/**
 * ForkController — fork creation, wormhole compression, and
 * backfill-rejection helpers.
 *
 * Extracted from fork.methods.js.
 *
 * @module domain/services/controllers/ForkController
 */
import ForkError from "../../errors/ForkError.js";
import { isCurrentCheckpointSchema } from "../state/checkpointHelpers.js";
import { validateGraphName, validateWriterId, buildWriterRef, buildWritersPrefix } from "../../utils/RefLayout.js";
import { generateWriterId } from "../../utils/WriterId.js";
import { createWormhole as createWormholeImpl } from "../WormholeService.js";
import { openRuntimeHostProduct, } from "../../warp/RuntimeHostProduct.js";
const DEFAULT_ADJACENCY_CACHE_SIZE = 3;
const HEX_CHARS = '0123456789abcdef';
/** Generates an 8-char hex suffix using crypto-grade randomness. */
function randomSuffix() {
    const bytes = new Uint8Array(4);
    globalThis.crypto.getRandomValues(bytes);
    let out = '';
    for (const b of bytes) {
        out += HEX_CHARS.charAt(b >>> 4) + HEX_CHARS.charAt(b & 0x0f);
    }
    return out;
}
export default class ForkController {
    _host;
    constructor(host) {
        this._host = host;
    }
    async fork({ from, at, forkName, forkWriterId }) {
        const host = this._host;
        if (!from || typeof from !== 'string') {
            throw new ForkError("Required parameter 'from' is missing or not a string", {
                code: 'E_FORK_INVALID_ARGS',
                context: { from },
            });
        }
        if (!at || typeof at !== 'string') {
            throw new ForkError("Required parameter 'at' is missing or not a string", {
                code: 'E_FORK_INVALID_ARGS',
                context: { at },
            });
        }
        const writers = await host.discoverWriters();
        if (!writers.includes(from)) {
            throw new ForkError(`Writer '${from}' does not exist in graph '${host._graphName}'`, {
                code: 'E_FORK_WRITER_NOT_FOUND',
                context: { writerId: from, graphName: host._graphName, existingWriters: writers },
            });
        }
        const nodeExists = await host._persistence.nodeExists(at);
        if (!nodeExists) {
            throw new ForkError(`Patch SHA '${at}' does not exist`, {
                code: 'E_FORK_PATCH_NOT_FOUND',
                context: { patchSha: at, writerId: from },
            });
        }
        const writerRef = buildWriterRef(host._graphName, from);
        const tipSha = await host._persistence.readRef(writerRef);
        if (tipSha === null || tipSha === undefined || tipSha === '') {
            throw new ForkError(`Writer '${from}' has no commits`, {
                code: 'E_FORK_WRITER_NOT_FOUND',
                context: { writerId: from },
            });
        }
        const isInChain = await this._isAncestor(at, tipSha);
        if (!isInChain) {
            throw new ForkError(`Patch SHA '${at}' is not in writer '${from}' chain`, {
                code: 'E_FORK_PATCH_NOT_IN_CHAIN',
                context: { patchSha: at, writerId: from, tipSha },
            });
        }
        const resolvedForkName = forkName ?? `${host._graphName}-fork-${randomSuffix()}`;
        try {
            validateGraphName(resolvedForkName);
        }
        catch (err) {
            throw new ForkError(`Invalid fork name: ${err.message}`, {
                code: 'E_FORK_NAME_INVALID',
                context: { forkName: resolvedForkName, originalError: err.message },
            });
        }
        const forkWritersPrefix = buildWritersPrefix(resolvedForkName);
        const existingForkRefs = await host._persistence.listRefs(forkWritersPrefix);
        if (existingForkRefs.length > 0) {
            throw new ForkError(`Graph '${resolvedForkName}' already exists`, {
                code: 'E_FORK_ALREADY_EXISTS',
                context: { forkName: resolvedForkName, existingRefs: existingForkRefs },
            });
        }
        const resolvedForkWriterId = (forkWriterId !== undefined && forkWriterId !== null && forkWriterId !== '') ? forkWriterId : generateWriterId();
        try {
            validateWriterId(resolvedForkWriterId);
        }
        catch (err) {
            throw new ForkError(`Invalid fork writer ID: ${err.message}`, {
                code: 'E_FORK_WRITER_ID_INVALID',
                context: { forkWriterId: resolvedForkWriterId, originalError: err.message },
            });
        }
        const forkWriterRef = buildWriterRef(resolvedForkName, resolvedForkWriterId);
        await host._persistence.updateRef(forkWriterRef, at);
        let forkGraph;
        try {
            forkGraph = await openRuntimeHostProduct({
                persistence: host._persistence,
                graphName: resolvedForkName,
                writerId: resolvedForkWriterId,
                gcPolicy: host._gcPolicy,
                adjacencyCacheSize: host._adjacencyCache?.maxSize ?? DEFAULT_ADJACENCY_CACHE_SIZE,
                ...(host._checkpointPolicy ? { checkpointPolicy: host._checkpointPolicy } : {}),
                autoMaterialize: host._autoMaterialize,
                onDeleteWithData: host._onDeleteWithData,
                ...(host._logger ? { logger: host._logger } : {}),
                crypto: host._crypto,
                codec: host._codec,
            });
        }
        catch (openErr) {
            try {
                await host._persistence.deleteRef(forkWriterRef);
            }
            catch {
                // Best-effort rollback
            }
            throw openErr;
        }
        return forkGraph;
    }
    async createWormhole(fromSha, toSha) {
        const host = this._host;
        return await createWormholeImpl({
            persistence: host._persistence,
            graphName: host._graphName,
            fromSha,
            toSha,
            commitMessageCodec: host._commitMessageCodec,
            codec: host._codec,
            ...(host._blobStorage ? { blobStorage: host._blobStorage } : {}),
            ...(host._patchBlobStorage ? { patchBlobStorage: host._patchBlobStorage } : {}),
        });
    }
    async _isAncestor(ancestorSha, descendantSha) {
        if (!ancestorSha || !descendantSha) {
            return false;
        }
        if (ancestorSha === descendantSha) {
            return true;
        }
        let cur = descendantSha;
        const visited = new Set();
        while (cur !== null) {
            if (visited.has(cur)) {
                throw new ForkError('Cycle detected in commit graph', {
                    code: 'E_FORK_CYCLE_DETECTED',
                    context: { sha: cur },
                });
            }
            visited.add(cur);
            const nodeInfo = await this._host._persistence.getNodeInfo(cur);
            const parent = nodeInfo.parents?.[0] ?? null;
            if (parent === ancestorSha) {
                return true;
            }
            cur = parent;
        }
        return false;
    }
    async _relationToCheckpointHead(ckHead, incomingSha) {
        if (incomingSha === ckHead) {
            return 'same';
        }
        if (await this._isAncestor(ckHead, incomingSha)) {
            return 'ahead';
        }
        if (await this._isAncestor(incomingSha, ckHead)) {
            return 'behind';
        }
        return 'diverged';
    }
    async _validatePatchAgainstCheckpoint(writerId, incomingSha, checkpoint) {
        if (checkpoint === null || checkpoint === undefined || !isCurrentCheckpointSchema(checkpoint.schema)) {
            return;
        }
        const ckHead = checkpoint.frontier?.get(writerId);
        if (ckHead === undefined || ckHead === null || ckHead === '') {
            return;
        }
        const relation = await this._relationToCheckpointHead(ckHead, incomingSha);
        if (relation === 'same' || relation === 'behind') {
            throw new ForkError(`Backfill rejected for writer ${writerId}: incoming patch is ${relation} checkpoint frontier`, { code: 'E_FORK_BACKFILL_REJECTED', context: { writerId, incomingSha, relation, ckHead } });
        }
        if (relation === 'diverged') {
            throw new ForkError(`Writer fork detected for ${writerId}: incoming patch does not extend checkpoint head`, { code: 'E_FORK_WRITER_DIVERGED', context: { writerId, incomingSha, ckHead } });
        }
    }
}
