import { debugLog, withDebugLogContext } from "../debug-log.js";
import { resolveTurnLimits } from "../config.js";
import { getProjectObsStore } from "../project-observations-store.js";
import { collectObservationsByCoverage, findLastCompactionIndex, gapRawEntries, getMemoryState, } from "../branch.js";
import { coverageTagCounts, migrateLegacyReflections, observationPoolTokens, renderSummary, runPruner, runReflector, } from "../compaction.js";
import { observationsToPromptLines, runObserver } from "../observer.js";
import { serializeSourceAddressedBranchEntries } from "../serialize.js";
import { estimateStringTokens } from "../tokens.js";
import { OBSERVATION_CUSTOM_TYPE, reflectionToPromptLine, } from "../types.js";
function plural(count, singular, pluralForm = `${singular}s`) {
    return `${count.toLocaleString()} ${count === 1 ? singular : pluralForm}`;
}
function formatCoverageCounts(counts) {
    return `${counts.uncited.toLocaleString()}/${counts.cited.toLocaleString()}/${counts.reinforced.toLocaleString()} uncited/cited/reinforced`;
}
function formatReflectorStats(stats) {
    const failed = stats.failedPass === undefined ? "" : `, failed pass ${stats.failedPass}`;
    return `reflector ${plural(stats.toolCalls, "tool call")}, +${stats.added.toLocaleString()} added, ${stats.merged.toLocaleString()} merged, ${stats.promoted.toLocaleString()} promoted, ${stats.duplicates.toLocaleString()} duplicate/no-op, ${stats.unsupported.toLocaleString()} unsupported${failed}`;
}
function formatPrunerStats(result) {
    return `pruner dropped ${plural(result.droppedIds.length, "observation")} in ${plural(result.passes.length, "pass", "passes")}, stop: ${result.stopReason}`;
}
export function registerCompactionHook(ext, runtime) {
    ext.on("session_before_compact", async (event, ctx) => {
        if (runtime.compactHookInFlight) {
            if (ctx.hasUI)
                ctx.ui.notify("Observational memory: another compaction is already in progress; cancelling duplicate", "warning");
            return { cancel: true };
        }
        runtime.compactHookInFlight = true;
        try {
            runtime.ensureConfig(ctx.cwd);
            const runId = `compaction-${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 8)}`;
            return await withDebugLogContext({ enabled: runtime.config.debugLog === true, cwd: ctx.cwd, runId }, async () => {
                const { preparation, branchEntries, signal } = event;
                const { firstKeptEntryId, tokensBefore } = preparation;
                // Capture ctx properties synchronously — after multiple awaits below,
                // the extension ctx may become stale (e.g. after session replacement/reload).
                const hasUI = ctx.hasUI;
                const ui = ctx.ui;
                const turnLimits = resolveTurnLimits(runtime.config);
                debugLog("compaction.start", {
                    firstKeptEntryId,
                    tokensBefore,
                    branchEntryCount: branchEntries.length,
                    reflectionThresholdTokens: runtime.config.reflectionThresholdTokens,
                    turnLimits,
                    legacyCompactionMaxToolCalls: runtime.config.compactionMaxToolCalls,
                });
                const resolved = await runtime.resolveModel(ctx);
                if (!resolved.ok) {
                    debugLog("compaction.model_unavailable", { reason: resolved.reason });
                    if (hasUI)
                        ui?.notify(`Observational memory: cannot compact — ${resolved.reason}. ` +
                            "Fix the model/API key and try /compact manually.", "error");
                    return { cancel: true };
                }
                runtime.resolveFailureNotified = false;
                let entries = branchEntries;
                if (runtime.observerPromise) {
                    try {
                        await runtime.observerPromise;
                    }
                    catch { /* already notified via launchObserverTask */ }
                    // In-flight observer may have appended a new observation entry during the await;
                    // refresh from sessionManager so gap computation and coverage collection see it
                    entries = ctx.sessionManager.getBranch();
                }
                const memoryState = getMemoryState(entries);
                debugLog("compaction.memory_state", {
                    committedObservations: memoryState.committedObs.length,
                    pendingObservations: memoryState.pendingObs.length,
                    reflections: memoryState.reflections.length,
                });
                let gapObservationData = null;
                const gap = gapRawEntries(entries, firstKeptEntryId);
                if (gap.length > 0) {
                    const { text: gapChunk, sourceEntryIds } = serializeSourceAddressedBranchEntries(gap);
                    if (gapChunk.trim() && sourceEntryIds.length > 0) {
                        const gapFromId = gap[0].id;
                        const gapUpToId = gap[gap.length - 1].id;
                        const priorObservationLines = observationsToPromptLines([
                            ...memoryState.committedObs,
                            ...memoryState.pendingObs,
                        ]);
                        const gapTokenEstimate = estimateStringTokens(gapChunk);
                        debugLog("compaction.sync_catchup.start", {
                            gapEntryCount: gap.length,
                            sourceEntryIds,
                            gapFromId,
                            gapUpToId,
                            tokenEstimate: gapTokenEstimate,
                        });
                        if (hasUI)
                            ui?.notify(`Observational memory: sync catch-up observer running on ~${gapTokenEstimate.toLocaleString()}-token gap`, "info");
                        runtime.observerInFlight = true;
                        const gapCall = runObserver({
                            model: resolved.model,
                            apiKey: resolved.apiKey,
                            headers: resolved.headers,
                            priorReflections: memoryState.reflections.map(reflectionToPromptLine),
                            priorObservations: priorObservationLines,
                            chunk: gapChunk,
                            allowedSourceEntryIds: sourceEntryIds,
                            signal,
                            maxTurns: turnLimits.observerMaxTurnsPerRun,
                        });
                        const gapPromise = gapCall.then(() => undefined, () => undefined);
                        runtime.observerPromise = gapPromise;
                        try {
                            const records = await gapCall;
                            if (records && records.length > 0) {
                                const observationTokens = records.reduce((sum, r) => sum + estimateStringTokens(r.content), 0);
                                gapObservationData = {
                                    records,
                                    coversFromId: gapFromId,
                                    coversUpToId: gapUpToId,
                                    tokenCount: observationTokens,
                                };
                                debugLog("compaction.sync_catchup.records", {
                                    count: records.length,
                                    observationTokens,
                                    coversFromId: gapFromId,
                                    coversUpToId: gapUpToId,
                                    records,
                                });
                                ext.appendEntry(OBSERVATION_CUSTOM_TYPE, gapObservationData);
                                if (hasUI && ui)
                                    ui.notify(`Observational memory: sync catch-up recorded ${records.length} observation${records.length === 1 ? "" : "s"} (~${observationTokens.toLocaleString()} tokens)`, "info");
                            }
                            else if (hasUI && ui) {
                                debugLog("compaction.sync_catchup.empty", { gapEntryCount: gap.length });
                                ui.notify("Observational memory: sync catch-up observer returned empty — proceeding with compaction", "warning");
                            }
                        }
                        catch (error) {
                            const msg = error instanceof Error ? error.message : String(error);
                            debugLog("compaction.sync_catchup.error", { gapEntryCount: gap.length, errorMessage: msg });
                            if (hasUI && ui)
                                ui.notify(`Observational memory: sync catch-up observer failed: ${msg}. Cancelling compaction — ${gap.length} unobserved raw entries would be pruned without coverage. Try /compact again.`, "warning");
                            return { cancel: true };
                        }
                        finally {
                            runtime.observerInFlight = false;
                            if (runtime.observerPromise === gapPromise)
                                runtime.observerPromise = null;
                        }
                    }
                }
                const priorCompactionIdx = findLastCompactionIndex(entries);
                const priorFirstKeptEntryId = priorCompactionIdx >= 0 ? entries[priorCompactionIdx].firstKeptEntryId : undefined;
                const deltaObservationData = collectObservationsByCoverage(entries, priorFirstKeptEntryId, firstKeptEntryId);
                if (gapObservationData)
                    deltaObservationData.push(gapObservationData);
                debugLog("compaction.delta", {
                    priorFirstKeptEntryId,
                    firstKeptEntryId,
                    deltaObservationEntries: deltaObservationData.length,
                    deltaObservationRecords: deltaObservationData.reduce((sum, data) => sum + data.records.length, 0),
                    gapObservationRecords: gapObservationData?.records.length ?? 0,
                });
                if (deltaObservationData.length === 0) {
                    // No new observations since last compaction. If we have existing memory,
                    // carry it forward in a no-op compaction so it survives spectral's compaction.
                    // If there is truly nothing (no prior memory either), cancel.
                    if (memoryState.committedObs.length === 0 && memoryState.reflections.length === 0) {
                        debugLog("compaction.no_delta_cancel", {
                            committedObservations: memoryState.committedObs.length,
                            pendingObservations: memoryState.pendingObs.length,
                            reflections: memoryState.reflections.length,
                        });
                        if (hasUI) {
                            ui?.notify(`Observational memory: nothing to compact yet — ${plural(memoryState.committedObs.length, "committed observation")} and ${plural(memoryState.pendingObs.length, "pending observation")}; no eligible delta before compact boundary`, "warning");
                        }
                        return { cancel: true };
                    }
                    // Carry forward existing memory without running reflector/pruner
                    const workingReflections = migrateLegacyReflections(memoryState.reflections);
                    debugLog("compaction.no_delta_carry_forward", {
                        observations: memoryState.committedObs.length,
                        reflections: workingReflections.length,
                    });
                    const summary = renderSummary(workingReflections, memoryState.committedObs);
                    const details = {
                        type: "observational-memory",
                        version: 4,
                        observations: memoryState.committedObs,
                        reflections: workingReflections,
                    };
                    if (hasUI)
                        ui?.notify(`Observational memory: no new observations — carrying forward ${memoryState.committedObs.length} observation${memoryState.committedObs.length === 1 ? "" : "s"}, ${workingReflections.length} reflection${workingReflections.length === 1 ? "" : "s"}`, "info");
                    // Promote reflections as project observations (cross-session durable memory).
                    persistProjectObservations(ctx, workingReflections);
                    return {
                        compaction: {
                            summary,
                            firstKeptEntryId,
                            tokensBefore,
                            details,
                        },
                    };
                }
                const workingReflections = migrateLegacyReflections(memoryState.reflections);
                const workingObservations = [
                    ...memoryState.committedObs,
                    ...deltaObservationData.flatMap((d) => d.records),
                ];
                const observationTokens = observationPoolTokens(workingObservations);
                debugLog("compaction.reflect_prune.gate", {
                    observationTokens,
                    reflectionThresholdTokens: runtime.config.reflectionThresholdTokens,
                    willRun: observationTokens >= runtime.config.reflectionThresholdTokens,
                    workingObservations: workingObservations.length,
                    workingReflections: workingReflections.length,
                });
                let finalReflections = workingReflections;
                let finalObservations = workingObservations;
                if (observationTokens >= runtime.config.reflectionThresholdTokens) {
                    try {
                        debugLog("compaction.reflect_prune.start", {
                            workingObservations: workingObservations.length,
                            workingReflections: workingReflections.length,
                            observationTokens,
                        });
                        if (hasUI)
                            ui?.notify("Observational memory: running reflector + pruner...", "info");
                        const coverageBefore = coverageTagCounts(workingReflections, workingObservations);
                        const reflectorResult = await runReflector({ model: resolved.model, apiKey: resolved.apiKey, headers: resolved.headers, signal, maxTurns: turnLimits.reflectorMaxTurnsPerPass }, workingReflections, workingObservations);
                        finalReflections = reflectorResult.reflections;
                        const coverageAfter = coverageTagCounts(finalReflections, workingObservations);
                        debugLog("compaction.reflector.result", {
                            stats: reflectorResult.stats,
                            coverageBefore,
                            coverageAfter,
                            beforeReflections: workingReflections.length,
                            afterReflections: finalReflections.length,
                        });
                        const prunerResult = await runPruner({ model: resolved.model, apiKey: resolved.apiKey, headers: resolved.headers, signal, maxTurns: turnLimits.prunerMaxTurnsPerPass }, finalReflections, workingObservations, runtime.config.reflectionThresholdTokens);
                        finalObservations = prunerResult.observations;
                        debugLog("compaction.pruner.result", {
                            stopReason: prunerResult.stopReason,
                            fellBack: prunerResult.fellBack,
                            droppedIds: prunerResult.droppedIds,
                            passes: prunerResult.passes,
                            beforeObservations: workingObservations.length,
                            afterObservations: finalObservations.length,
                        });
                        if (hasUI) {
                            ui?.notify(`Observational memory: diagnostics — ${formatReflectorStats(reflectorResult.stats)}; coverage ${formatCoverageCounts(coverageBefore)} → ${formatCoverageCounts(coverageAfter)}; ${formatPrunerStats(prunerResult)}`, "info");
                        }
                        if (prunerResult.fellBack && hasUI) {
                            ui?.notify("Observational memory: pruner run failed; kept observation set unchanged", "warning");
                        }
                    }
                    catch (error) {
                        const msg = error instanceof Error ? error.message : String(error);
                        debugLog("compaction.reflect_prune.error", { errorMessage: msg });
                        if (hasUI)
                            ui?.notify(`Observational memory: reflect/prune failed: ${msg}`, "warning");
                    }
                }
                const summary = renderSummary(finalReflections, finalObservations);
                if (finalObservations.length === 0) {
                    throw new Error("invariant violated: finalObservations empty after delta guard");
                }
                const details = {
                    type: "observational-memory",
                    version: 4,
                    observations: finalObservations,
                    reflections: finalReflections,
                };
                debugLog("compaction.result", {
                    finalObservations: finalObservations.length,
                    finalReflections: finalReflections.length,
                    firstKeptEntryId,
                    tokensBefore,
                });
                if (hasUI)
                    ui?.notify(`Observational memory: compaction assembled — ${finalObservations.length} observation${finalObservations.length === 1 ? "" : "s"}, ${finalReflections.length} reflection${finalReflections.length === 1 ? "" : "s"}`, "info");
                // Promote reflections as project observations (cross-session durable memory).
                persistProjectObservations(ctx, finalReflections);
                return {
                    compaction: {
                        summary,
                        firstKeptEntryId,
                        tokensBefore,
                        details,
                    },
                };
            });
        }
        finally {
            runtime.compactHookInFlight = false;
        }
    });
    function persistProjectObservations(ctx, reflections) {
        const store = getProjectObsStore();
        if (!store)
            return; // Not available in standalone spectral mode.
        const projectId = store.getProjectByCwd(ctx.cwd);
        if (!projectId)
            return; // No project registered for this cwd.
        const sessionId = ctx.sessionManager.getSessionId?.();
        if (!sessionId)
            return;
        if (reflections.length === 0)
            return;
        const projectObs = reflections.map((r) => ({
            id: typeof r === "string" ? r : r.id ?? r.content.slice(0, 64),
            content: typeof r === "string" ? r : r.content,
            relevance: "high",
        }));
        store.insertProjectObservations(projectId, sessionId, projectObs, Date.now());
        debugLog("compaction.project_obs", {
            count: projectObs.length,
            projectId,
            sessionId,
        });
    }
}
