import { checkAutoStartAfterDiscuss } from "../guided-flow.js";
import { getAutoDashboardData, getAutoModeStartModel, isAutoActive, pauseAuto, setFallbackActive, clearFallbackActive, isFallbackActive } from "../auto.js";
import { getNextFallbackModel, resolveModelWithFallbacksForUnit } from "../preferences.js";
import { pauseAutoForProviderError } from "../provider-error-pause.js";
import { isSessionSwitchInFlight, resolveAgentEnd } from "../auto-loop.js";
import { resolveModelId } from "../auto-model-selection.js";
import { clearDiscussionFlowState } from "./write-gate.js";
import { resumeAutoAfterProviderDelay } from "./provider-error-resume.js";
import { classifyError, createRetryState, resetRetryState, isTransient, } from "../error-classifier.js";
import { emitFallbackEvent, buildFallbackEvent, buildRestoredEvent, buildExhaustedEvent, } from "../fallback-events.js";
const retryState = createRetryState();
const MAX_NETWORK_RETRIES = 2;
const MAX_TRANSIENT_AUTO_RESUMES = 3;
async function pauseTransientWithBackoff(cls, pi, ctx, errorDetail, isRateLimit) {
    retryState.consecutiveTransientCount += 1;
    const baseRetryAfterMs = "retryAfterMs" in cls ? cls.retryAfterMs : 15_000;
    const retryAfterMs = baseRetryAfterMs * 2 ** Math.max(0, retryState.consecutiveTransientCount - 1);
    const allowAutoResume = retryState.consecutiveTransientCount <= MAX_TRANSIENT_AUTO_RESUMES;
    if (!allowAutoResume) {
        ctx.ui.notify(`Transient provider errors persisted after ${MAX_TRANSIENT_AUTO_RESUMES} auto-resume attempts. Pausing for manual review.`, "warning");
    }
    await pauseAutoForProviderError(ctx.ui, errorDetail, () => pauseAuto(ctx, pi, {
        message: `Provider error: ${errorDetail}`,
        category: "provider",
        isTransient: allowAutoResume,
        retryAfterMs,
    }), {
        isRateLimit,
        isTransient: allowAutoResume,
        retryAfterMs,
        resume: allowAutoResume
            ? () => {
                void resumeAutoAfterProviderDelay(pi, ctx).catch((err) => {
                    const message = err instanceof Error ? err.message : String(err);
                    ctx.ui.notify(`Provider error recovery delay elapsed, but auto-mode failed to resume: ${message}`, "error");
                });
            }
            : undefined,
    });
}
export async function handleAgentEnd(pi, event, ctx) {
    if (checkAutoStartAfterDiscuss()) {
        clearDiscussionFlowState();
        return;
    }
    if (!isAutoActive())
        return;
    if (isSessionSwitchInFlight())
        return;
    const lastMsg = event.messages[event.messages.length - 1];
    if (lastMsg && "stopReason" in lastMsg && lastMsg.stopReason === "aborted") {
        await pauseAuto(ctx, pi);
        return;
    }
    if (lastMsg && "stopReason" in lastMsg && lastMsg.stopReason === "error") {
        const errorDetail = "errorMessage" in lastMsg && lastMsg.errorMessage ? `: ${lastMsg.errorMessage}` : "";
        const errorMsg = ("errorMessage" in lastMsg && lastMsg.errorMessage) ? String(lastMsg.errorMessage) : "";
        const explicitRetryAfterMs = ("retryAfterMs" in lastMsg && typeof lastMsg.retryAfterMs === "number") ? lastMsg.retryAfterMs : undefined;
        // ── 1. Classify ──────────────────────────────────────────────────────
        const cls = classifyError(errorMsg, explicitRetryAfterMs);
        // ── 2. Decide & Act ──────────────────────────────────────────────────
        // --- Network errors: same-model retry with backoff ---
        if (cls.kind === "network") {
            const currentModelId = ctx.model?.id ?? "unknown";
            if (retryState.currentRetryModelId !== currentModelId) {
                retryState.networkRetryCount = 0;
                retryState.currentRetryModelId = currentModelId;
            }
            if (retryState.networkRetryCount < MAX_NETWORK_RETRIES) {
                retryState.networkRetryCount += 1;
                retryState.consecutiveTransientCount += 1;
                const attempt = retryState.networkRetryCount;
                const delayMs = Math.min(cls.retryAfterMs * Math.pow(2, attempt - 1), 120_000) * (0.7 + Math.random() * 0.6);
                ctx.ui.notify(`Network error on ${currentModelId}${errorDetail}. Retry ${attempt}/${MAX_NETWORK_RETRIES} in ${delayMs / 1000}s...`, "warning");
                setTimeout(() => {
                    pi.sendMessage({ customType: "nerf-auto-timeout-recovery", content: "Continue execution — retrying after transient network error.", display: false }, { triggerTurn: true });
                }, delayMs);
                return;
            }
            // Network retries exhausted — fall through to model fallback
            retryState.networkRetryCount = 0;
            retryState.currentRetryModelId = undefined;
            ctx.ui.notify(`Network retries exhausted for ${currentModelId}. Attempting model fallback.`, "warning");
        }
        // --- Transient errors: try model fallback first, then pause ---
        // Rate limits are often per-model, so switching models can bypass them.
        if (cls.kind === "rate-limit" || cls.kind === "network" || cls.kind === "server" || cls.kind === "connection" || cls.kind === "stream") {
            // Try model fallback
            const dash = getAutoDashboardData();
            if (dash.currentUnit) {
                const modelConfig = resolveModelWithFallbacksForUnit(dash.currentUnit.type);
                if (modelConfig && modelConfig.fallbacks.length > 0) {
                    const availableModels = ctx.modelRegistry.getAvailable();
                    const nextModelId = getNextFallbackModel(ctx.model?.id, modelConfig);
                    if (nextModelId) {
                        retryState.networkRetryCount = 0;
                        retryState.currentRetryModelId = undefined;
                        const modelToSet = resolveModelId(nextModelId, availableModels, ctx.model?.provider);
                        if (modelToSet) {
                            const ok = await pi.setModel(modelToSet, { persist: false });
                            if (ok) {
                                const fromModel = ctx.model?.id ?? "unknown";
                                const unitType = dash.currentUnit?.type ?? "unknown";
                                setFallbackActive(nextModelId);
                                emitFallbackEvent(ctx, buildFallbackEvent(fromModel, nextModelId, cls.kind, unitType));
                                ctx.ui.notify(`Model fallback: ${fromModel} → ${nextModelId} (reason: ${cls.kind}, unit: ${unitType})`, "info");
                                ctx.ui.notify(`Model error${errorDetail}. Switched to fallback: ${nextModelId} and resuming.`, "warning");
                                pi.sendMessage({ customType: "nerf-auto-timeout-recovery", content: "Continue execution.", display: false }, { triggerTurn: true });
                                return;
                            }
                        }
                    }
                }
            }
            // Fallback loop fell through — emit exhausted event
            {
                const unitType = dash?.currentUnit?.type ?? "unknown";
                const currentModel = ctx.model?.id ?? "unknown";
                emitFallbackEvent(ctx, buildExhaustedEvent(currentModel, cls.kind, unitType));
            }
            // Try restoring session model
            const sessionModel = getAutoModeStartModel();
            if (sessionModel) {
                if (ctx.model?.id !== sessionModel.id || ctx.model?.provider !== sessionModel.provider) {
                    const startModel = ctx.modelRegistry.getAvailable().find((m) => m.provider === sessionModel.provider && m.id === sessionModel.id);
                    if (startModel) {
                        const ok = await pi.setModel(startModel, { persist: false });
                        if (ok) {
                            retryState.networkRetryCount = 0;
                            retryState.currentRetryModelId = undefined;
                            ctx.ui.notify(`Model error${errorDetail}. Restored session model: ${sessionModel.provider}/${sessionModel.id} and resuming.`, "warning");
                            pi.sendMessage({ customType: "nerf-auto-timeout-recovery", content: "Continue execution.", display: false }, { triggerTurn: true });
                            return;
                        }
                    }
                }
            }
        }
        // --- Transient fallback: pause with auto-resume ---
        if (isTransient(cls)) {
            await pauseTransientWithBackoff(cls, pi, ctx, errorDetail, cls.kind === "rate-limit");
            return;
        }
        // --- Permanent / unknown: pause indefinitely ---
        await pauseAutoForProviderError(ctx.ui, errorDetail, () => pauseAuto(ctx, pi, {
            message: `Provider error: ${errorDetail}`,
            category: "provider",
            isTransient: false,
        }), {
            isRateLimit: false,
            isTransient: false,
            retryAfterMs: 0,
        });
        return;
    }
    // ── Success path ─────────────────────────────────────────────────────────
    try {
        if (isFallbackActive()) {
            const fromFallback = ctx.model?.id ?? "unknown";
            emitFallbackEvent(ctx, buildRestoredEvent(fromFallback, fromFallback, "success"));
            clearFallbackActive();
        }
        resetRetryState(retryState);
        resolveAgentEnd(event);
    }
    catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        ctx.ui.notify(`Auto-mode error in agent_end handler: ${message}. Stopping auto-mode.`, "error");
        try {
            await pauseAuto(ctx, pi);
        }
        catch {
            // best-effort
        }
    }
}
