import { debugLog, withDebugLogContext } from "../debug-log.js";
import { resolveTurnLimits } from "../config.js";
import { firstRawIdAfter, getMemoryState, lastObservationCoverEndIdx, rawTailEntriesBetween, rawTokensSinceLastBound, } from "../branch.js";
import { observationsToPromptLines, runObserver } from "../observer.js";
import { serializeSourceAddressedBranchEntries } from "../serialize.js";
import { estimateEntryTokens, estimateStringTokens } from "../tokens.js";
import { OBSERVATION_CUSTOM_TYPE, reflectionToPromptLine } from "../types.js";
export function registerObserverTrigger(ext, runtime) {
    ext.on("turn_end", (_event, ctx) => {
        process.stderr.write("[obs-mem] turn_end fired\n");
        runtime.ensureConfig(ctx.cwd);
        if (runtime.config.passive === true) {
            process.stderr.write("[obs-mem] passive=true → skipping\n");
            return;
        }
        if (runtime.observerInFlight) {
            process.stderr.write("[obs-mem] observer already in flight → skipping\n");
            return;
        }
        const entries = ctx.sessionManager.getBranch();
        const tokens = rawTokensSinceLastBound(entries);
        process.stderr.write(`[obs-mem] tokens since last bound: ${tokens} (threshold: ${runtime.config.observationThresholdTokens})\n`);
        if (tokens < runtime.config.observationThresholdTokens) {
            process.stderr.write("[obs-mem] below threshold → skipping observer\n");
            return;
        }
        const lastBoundIdx = lastObservationCoverEndIdx(entries);
        const coversFromId = firstRawIdAfter(entries, lastBoundIdx);
        if (!coversFromId)
            return;
        const leafId = ctx.sessionManager.getLeafId();
        if (!leafId)
            return;
        const coversUpToId = leafId;
        const { reflections, committedObs, pendingObs } = getMemoryState(entries);
        const priorObservationLines = observationsToPromptLines([...committedObs, ...pendingObs]);
        const turnLimits = resolveTurnLimits(runtime.config);
        let chunkEntries = rawTailEntriesBetween(entries, coversFromId, coversUpToId);
        if (chunkEntries.length === 0)
            return;
        // Cap chunk size to prevent observer overload on large sessions.
        // Without capping, returning to an old big session can produce a 159K+ token
        // chunk that the observer cannot process in 16 turns, causing it to return
        // empty. The bound never advances, so future attempts retry an even larger
        // chunk → deadlock.
        const chunkTokenCap = runtime.config.observerMaxChunkTokens ?? 30_000;
        let effectiveCoversUpToId = coversUpToId;
        if (chunkTokenCap > 0) {
            let accumulated = 0;
            let capIdx = chunkEntries.length;
            for (let i = 0; i < chunkEntries.length; i++) {
                accumulated += estimateEntryTokens(chunkEntries[i]);
                if (accumulated > chunkTokenCap) {
                    capIdx = i;
                    break;
                }
            }
            if (capIdx < chunkEntries.length) {
                chunkEntries = chunkEntries.slice(0, capIdx);
                effectiveCoversUpToId = chunkEntries[capIdx - 1]?.id ?? coversUpToId;
                process.stderr.write(`[obs-mem] chunk capped at ~${accumulated.toLocaleString()} tokens ` +
                    `(${chunkEntries.length} source entries); remaining unobserved entries deferred\n`);
                debugLog("observer.chunk_capped", {
                    originalCount: rawTailEntriesBetween(entries, coversFromId, coversUpToId).length,
                    cappedCount: chunkEntries.length,
                    cappedTokens: accumulated,
                    cap: chunkTokenCap,
                    effectiveCoversUpToId,
                });
            }
        }
        const { text: chunk, sourceEntryIds } = serializeSourceAddressedBranchEntries(chunkEntries);
        if (!chunk.trim() || sourceEntryIds.length === 0)
            return;
        if (ctx.hasUI)
            ctx.ui.notify(`Observational memory: observer running on ~${tokens.toLocaleString()}-token chunk`, "info");
        const runId = `observer-${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 8)}`;
        // Capture ctx properties synchronously — the async work below may outlive
        // the extension ctx (stale after session replacement/reload).
        const hasUI = ctx.hasUI;
        const ui = ctx.ui;
        const model = ctx.model;
        const modelRegistry = ctx.modelRegistry;
        const cwd = ctx.cwd;
        void runtime.launchObserverTask(ctx, "observer", async () => withDebugLogContext({ enabled: runtime.config.debugLog === true, cwd, runId }, async () => {
            try {
                debugLog("observer.start", {
                    tokens,
                    coversFromId,
                    coversUpToId,
                    sourceEntryIds,
                    sourceEntryCount: sourceEntryIds.length,
                    priorReflections: reflections.length,
                    priorObservations: priorObservationLines.length,
                });
                const resolved = await runtime.resolveModel({ model, modelRegistry, hasUI, ui });
                if (!resolved.ok) {
                    debugLog("observer.model_unavailable", { reason: resolved.reason });
                    if (!runtime.resolveFailureNotified && hasUI && ui) {
                        ui.notify(`Observational memory: observer skipped — ${resolved.reason}`, "warning");
                        runtime.resolveFailureNotified = true;
                    }
                    return;
                }
                runtime.resolveFailureNotified = false;
                const records = await runObserver({
                    model: resolved.model,
                    apiKey: resolved.apiKey,
                    headers: resolved.headers,
                    priorReflections: reflections.map(reflectionToPromptLine),
                    priorObservations: priorObservationLines,
                    chunk,
                    allowedSourceEntryIds: sourceEntryIds,
                    maxTurns: turnLimits.observerMaxTurnsPerRun,
                });
                if (!records || records.length === 0) {
                    debugLog("observer.empty", { coversFromId, coversUpToId: effectiveCoversUpToId });
                    // Advance the bound even on empty results so we don't retry the same
                    // chunk. Without this, returning to an old session can deadlock:
                    // observer runs on 159K tokens → returns empty → bound not advanced
                    // → next run retries an even larger chunk → repeat forever.
                    const placeholderData = {
                        records: [],
                        coversFromId,
                        coversUpToId: effectiveCoversUpToId,
                        tokenCount: 0,
                    };
                    ext.appendEntry(OBSERVATION_CUSTOM_TYPE, placeholderData);
                    debugLog("observer.empty_placeholder", { coversFromId, coversUpToId: effectiveCoversUpToId });
                    if (hasUI && ui)
                        ui.notify("Observational memory: observer returned no observations; bound advanced to prevent re-processing", "warning");
                    return;
                }
                const observationTokens = records.reduce((sum, r) => sum + estimateStringTokens(r.content), 0);
                const data = {
                    records,
                    coversFromId,
                    coversUpToId: effectiveCoversUpToId,
                    tokenCount: observationTokens,
                };
                debugLog("observer.records", {
                    count: records.length,
                    observationTokens,
                    coversFromId,
                    coversUpToId: effectiveCoversUpToId,
                    records,
                });
                ext.appendEntry(OBSERVATION_CUSTOM_TYPE, data);
                debugLog("observer.appended", { count: records.length, tokenCount: observationTokens, coversFromId, coversUpToId: effectiveCoversUpToId });
                if (hasUI && ui)
                    ui.notify(`Observational memory: ${records.length} observation${records.length === 1 ? "" : "s"} recorded (~${observationTokens.toLocaleString()} tokens)`, "info");
            }
            catch (error) {
                debugLog("observer.error", { errorMessage: error instanceof Error ? error.message : String(error) });
                throw error;
            }
        }));
    });
}
