import { blockModel, isModelBlocked } from "../blocked-models.js";
import {
	classifyError,
	createRetryState,
	isTransient,
	resetRetryState,
} from "../error-classifier.js";
import { getErrorMessage } from "../error-utils.js";
import { resolveNextModelRoute } from "../model-route-failure.js";
import {
	resolveModelWithFallbacksForUnit,
	resolvePersistModelChanges,
} from "../preferences.js";
import { pauseAutoForProviderError } from "../provider-error-pause.js";
import { logWarning } from "../workflow-logger.js";
import { clearDiscussionFlowState } from "./write-gate.js";

const retryState = createRetryState();
const GEMINI_CAPACITY_COOLDOWN_MS = 2 * 60_000;
const GEMINI_CAPACITY_MAX_COOLDOWN_MS = 30 * 60_000;

function temporaryRouteBlockMs(provider, cls) {
	if (provider !== "google-gemini-cli") return undefined;
	if (cls.kind !== "rate-limit" && cls.kind !== "server") return undefined;
	const retryAfterMs =
		"retryAfterMs" in cls && typeof cls.retryAfterMs === "number"
			? cls.retryAfterMs
			: undefined;
	const base = Math.max(retryAfterMs ?? 0, GEMINI_CAPACITY_COOLDOWN_MS);
	return Math.min(base, GEMINI_CAPACITY_MAX_COOLDOWN_MS);
}
/**
 * Reset the module-level retry state so a resumed auto-session starts fresh.
 * Called by provider-error-resume.ts before startAuto() so legacy paused
 * provider recovery does not inherit stale transient counters.
 */
export function resetTransientRetryState() {
	resetRetryState(retryState);
}
function getCurrentRouteFromMessage(lastMsg, ctx) {
	const msg = lastMsg;
	const provider =
		typeof msg?.provider === "string" ? msg.provider : ctx.model?.provider;
	const id = typeof msg?.model === "string" ? msg.model : ctx.model?.id;
	return provider && id ? { provider, id } : undefined;
}
function isModelRouteFailure(cls) {
	return (
		cls.kind === "rate-limit" ||
		cls.kind === "network" ||
		cls.kind === "server" ||
		cls.kind === "connection" ||
		cls.kind === "stream"
	);
}
async function trySwitchToFallbackModel(args) {
	const {
		getCurrentUnitModelFailures,
		recordCurrentModelFailure,
		setCurrentUnitModel,
	} = await import("../auto.js");
	const modelConfig = resolveModelWithFallbacksForUnit(args.unitType, {
		autoBenchmark: true,
	});
	if (args.current) {
		recordCurrentModelFailure({
			provider: args.current.provider,
			modelId: args.current.id,
			reason: args.reason,
		});
	}
	const availableModels = args.ctx.modelRegistry.getAvailable();
	const isBlocked = args.basePath
		? (model) => isModelBlocked(args.basePath, model.provider, model.id)
		: undefined;
	for (
		let attempt = 0;
		attempt < availableModels.length + (modelConfig?.fallbacks.length ?? 0) + 1;
		attempt++
	) {
		const nextRoute = resolveNextModelRoute({
			current: args.current,
			modelConfig,
			availableModels,
			failedRoutes: getCurrentUnitModelFailures(),
			isBlocked,
			unitType: args.unitType,
		});
		if (!nextRoute) return false;
		const ok = await args.pi.setModel(nextRoute.model, {
			persist: args.persistModelChanges,
		});
		if (!ok) {
			recordCurrentModelFailure({
				provider: nextRoute.model.provider,
				modelId: nextRoute.model.id,
				reason: "setModel failed during provider recovery",
			});
			continue;
		}
		resetRetryState(retryState);
		setCurrentUnitModel(nextRoute.model);
		args.ctx.ui.notify(
			`Model route failed${args.errorDetail}. Switched to ${nextRoute.source === "configured" ? "configured fallback" : "available fallback"}: ${nextRoute.model.provider}/${nextRoute.model.id}.`,
			"warning",
		);
		args.pi.sendMessage(
			{
				customType: "sf-auto-timeout-recovery",
				content: "Continue execution.",
				display: false,
			},
			{ triggerTurn: true },
		);
		return true;
	}
	return false;
}
export async function handleAgentEnd(pi, event, ctx) {
	const { checkAutoStartAfterDiscuss } = await import("../guided-flow.js");
	const { getAutoDashboardData, isAutoActive, pauseAuto } = await import(
		"../auto.js"
	);
	const { isSessionSwitchInFlight, resolveAgentEnd } = await import(
		"../auto-loop.js"
	);
	const persistModelChanges = resolvePersistModelChanges();
	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") {
		// Empty content with aborted stopReason is a non-fatal agent stop (the LLM
		// chose to end without producing output). Only pause on genuine fatal aborts
		// that carry error context — e.g. errorMessage field or non-empty content
		// indicating a mid-stream failure. (#2695)
		const content = "content" in lastMsg ? lastMsg.content : undefined;
		const hasEmptyContent = Array.isArray(content) && content.length === 0;
		const hasErrorMessage = "errorMessage" in lastMsg && !!lastMsg.errorMessage;
		if (hasEmptyContent && !hasErrorMessage) {
			// Non-fatal: treat as a normal agent end so the loop can continue
			// instead of entering a stuck re-dispatch cycle.
			try {
				resetRetryState(retryState);
				resolveAgentEnd(event);
			} catch (err) {
				const message = getErrorMessage(err);
				ctx.ui.notify(
					`Autonomous mode error after empty-content abort: ${message}. Stopping autonomous mode.`,
					"error",
				);
				try {
					await pauseAuto(ctx, pi);
				} catch (e) {
					logWarning(
						"bootstrap",
						`pauseAuto failed after empty-content abort: ${e.message}`,
					);
				}
			}
			return;
		}
		await pauseAuto(ctx, pi);
		return;
	}
	if (lastMsg && "stopReason" in lastMsg && lastMsg.stopReason === "error") {
		// #3588: errorMessage can be useless (e.g. "success") while the real error
		// is in the assistant message text content. Fall back to content when
		// errorMessage looks uninformative.
		const rawErrorMsg =
			"errorMessage" in lastMsg && lastMsg.errorMessage
				? String(lastMsg.errorMessage)
				: "";
		const isUseless =
			!rawErrorMsg ||
			/^(success|ok|true|error|unknown)$/i.test(rawErrorMsg.trim());
		// #3588: When errorMessage is uninformative, extract the real error from
		// the assistant message text content for display purposes only.
		// Classification still uses rawErrorMsg to avoid false positives from prose.
		let displayMsg = rawErrorMsg;
		if (isUseless && "content" in lastMsg && Array.isArray(lastMsg.content)) {
			const textBlock = lastMsg.content.find(
				(b) => b.type === "text" && b.text,
			);
			if (textBlock) displayMsg = textBlock.text.slice(0, 300);
		}
		const errorDetail = displayMsg ? `: ${displayMsg}` : "";
		const explicitRetryAfterMs =
			"retryAfterMs" in lastMsg && typeof lastMsg.retryAfterMs === "number"
				? lastMsg.retryAfterMs
				: undefined;
		// ── 1. Classify using rawErrorMsg to avoid prose false-positives ────
		const cls = classifyError(rawErrorMsg, explicitRetryAfterMs);
		const currentRoute = getCurrentRouteFromMessage(lastMsg, ctx);
		const dash = getAutoDashboardData();
		// SF owns provider-route recovery in autonomous mode. Quota/rate-limit/server/
		// stream/connection failures must leave the failed provider/model route
		// immediately instead of sleeping or waiting for same-model retry loops.
		// Cap rate-limit backoff for CLI-style providers (openai-codex, google-gemini-cli)
		// which use per-user quotas with shorter windows (#2922).
		if (cls.kind === "rate-limit") {
			const currentProvider = ctx.model?.provider;
			if (
				currentProvider === "openai-codex" ||
				currentProvider === "google-gemini-cli"
			) {
				cls.retryAfterMs = Math.min(cls.retryAfterMs, 30_000);
			}
		}
		// ── 1c. Unsupported-model: provider rejected this model for the current
		//        account/plan at request time (#4513).  Persist a block so the
		//        same dead model isn't reselected on the next /autonomous restart,
		//        then try a fallback before pausing.
		if (cls.kind === "unsupported-model") {
			const rejectedProvider = currentRoute?.provider;
			const rejectedId = currentRoute?.id;
			if (dash.basePath && rejectedProvider && rejectedId) {
				try {
					blockModel(
						dash.basePath,
						rejectedProvider,
						rejectedId,
						rawErrorMsg || "unsupported for account",
					);
					ctx.ui.notify(
						`Blocked ${rejectedProvider}/${rejectedId} for this project — provider rejected it for the current account.`,
						"warning",
					);
				} catch (err) {
					const m = getErrorMessage(err);
					logWarning("bootstrap", `Failed to persist blocked model: ${m}`);
				}
			}
			if (dash.currentUnit && dash.basePath) {
				const switched = await trySwitchToFallbackModel({
					pi,
					ctx,
					current: currentRoute,
					reason: rawErrorMsg || "unsupported for account",
					unitType: dash.currentUnit.type,
					basePath: dash.basePath,
					errorDetail,
					persistModelChanges,
				});
				if (switched) return;
			}
			// No usable fallback — pause
			await pauseAutoForProviderError(
				ctx.ui,
				`Model unsupported for this account${errorDetail}`,
				() =>
					pauseAuto(ctx, pi, {
						message: `Model unsupported for this account${errorDetail}`,
						category: "provider",
					}),
			);
			return;
		}
		// ── 2. Decide & Act ──────────────────────────────────────────────────
		// --- Route failures: try configured fallback first, then any available route ---
		if (isModelRouteFailure(cls) && dash.currentUnit) {
			const blockMs = temporaryRouteBlockMs(currentRoute?.provider, cls);
			if (
				blockMs &&
				dash.basePath &&
				currentRoute?.provider &&
				currentRoute?.id
			) {
				try {
					blockModel(
						dash.basePath,
						currentRoute.provider,
						currentRoute.id,
						rawErrorMsg || cls.kind,
						{ expiresAt: Date.now() + blockMs },
					);
					ctx.ui.notify(
						`Cooling down ${currentRoute.provider}/${currentRoute.id} for ${Math.ceil(blockMs / 1000)}s after provider capacity rejection.`,
						"warning",
					);
				} catch (err) {
					const m = getErrorMessage(err);
					logWarning("bootstrap", `Failed to persist model cooldown: ${m}`);
				}
			}
			const switched = await trySwitchToFallbackModel({
				pi,
				ctx,
				current: currentRoute,
				reason: rawErrorMsg || cls.kind,
				unitType: dash.currentUnit.type,
				basePath: dash.basePath,
				errorDetail,
				persistModelChanges,
			});
			if (switched) return;
		}
		// --- Transient fallback exhausted: pause without same-route scheduled resume ---
		if (isTransient(cls)) {
			const message =
				isModelRouteFailure(cls) && dash.currentUnit
					? `Provider route failed and no usable fallback model remains${errorDetail}`
					: `Provider error${errorDetail}`;
			await pauseAutoForProviderError(
				ctx.ui,
				errorDetail,
				() =>
					pauseAuto(ctx, pi, {
						message,
						category: "provider",
						isTransient: false,
						retryAfterMs: "retryAfterMs" in cls ? cls.retryAfterMs : undefined,
					}),
				{
					isRateLimit: cls.kind === "rate-limit",
					isTransient: false,
					retryAfterMs: "retryAfterMs" in cls ? cls.retryAfterMs : 0,
				},
			);
			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 {
		resetRetryState(retryState);
		resolveAgentEnd(event);
	} catch (err) {
		const message = getErrorMessage(err);
		ctx.ui.notify(
			`Autonomous mode error in agent_end handler: ${message}. Stopping autonomous mode.`,
			"error",
		);
		try {
			await pauseAuto(ctx, pi);
		} catch (e) {
			logWarning(
				"bootstrap",
				`pauseAuto failed in agent_end handler: ${e.message}`,
			);
		}
	}
}
