/**
 * Google Gemini CLI provider.
 *
 * Delegates auth, project discovery, and the Code Assist transport setup to
 * the dedicated google-gemini-cli-provider package.
 * Request retry/fallback stays in the caller so SF can move to the next model.
 */

import { retryWithBackoff } from "@google/gemini-cli-core";
import type {
	Content,
	GenerateContentParameters,
	ThinkingConfig,
} from "@google/genai";
import { createGeminiCliContentGenerator } from "@singularity-forge/google-gemini-cli-provider";
import { resolveWireModelId } from "../model-identity.js";
import { calculateCost } from "../models.js";
import type {
	Api,
	AssistantMessage,
	Context,
	Model,
	SimpleStreamOptions,
	StreamFunction,
	StreamOptions,
	TextContent,
	ThinkingBudgets,
	ThinkingContent,
	ThinkingLevel,
	ToolCall,
} from "../types.js";
import { AssistantMessageEventStream } from "../utils/event-stream.js";
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
import {
	convertMessages,
	convertTools,
	isThinkingPart,
	mapStopReasonString,
	mapToolChoice,
	retainThoughtSignature,
} from "./google-shared.js";
import {
	buildBaseOptions,
	clampReasoning,
	isAutoReasoning,
	resolveReasoningLevel,
} from "./simple-options.js";

/**
 * Thinking level for Gemini 3 models.
 *
 * Gemini 3 Pro supports LOW/HIGH; Gemini 3 Flash supports MINIMAL/LOW/MEDIUM/HIGH.
 * These are the wire format values for `ThinkingConfig.thinkingLevel` sent to cli-core's
 * `CodeAssistServer.generateContentStream()`.
 */
export type GoogleThinkingLevel =
	| "THINKING_LEVEL_UNSPECIFIED"
	| "MINIMAL"
	| "LOW"
	| "MEDIUM"
	| "HIGH";

/**
 * Options for `streamGoogleGeminiCli()`.
 *
 * Delegates auth to the helper package (reads ~/.gemini/oauth_creds.json via
 * Gemini CLI Core's transport setup);
 * `projectId` is auto-discovered and not used by this provider (apiKey is ignored).
 * Thinking is configured separately from base `StreamOptions` because Gemini 2 and 3
 * models use incompatible enum formats (budgetTokens vs. level).
 */
export interface GoogleGeminiCliOptions extends StreamOptions {
	toolChoice?: "auto" | "none" | "any";
	/**
	 * Thinking/reasoning configuration.
	 * - Gemini 2.x models: use `budgetTokens` to set the thinking budget
	 * - Gemini 3 models (gemini-3-pro-*, gemini-3-flash-*): use `level` instead
	 *
	 * When using `streamSimple`, this is handled automatically based on the model.
	 */
	thinking?: {
		enabled: boolean;
		/** Thinking budget in tokens. Use for Gemini 2.x models. */
		budgetTokens?: number;
		/** Thinking level. Use for Gemini 3 models (LOW/HIGH for Pro, MINIMAL/LOW/MEDIUM/HIGH for Flash). */
		level?: GoogleThinkingLevel;
	};
	projectId?: string;
}

// Counter for generating unique tool call IDs
let toolCallCounter = 0;

function parseDurationMs(value: string): number | undefined {
	const match = value.match(/(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?/i);
	if (!match || !match[0]) return undefined;
	const hours = Number(match[1] ?? 0);
	const minutes = Number(match[2] ?? 0);
	const seconds = Number(match[3] ?? 0);
	const totalMs = ((hours * 60 + minutes) * 60 + seconds) * 1000;
	return totalMs > 0 ? totalMs : undefined;
}

function extractRetryAfterMs(error: unknown): number | undefined {
	if (typeof error === "object" && error !== null && "retryDelayMs" in error) {
		const retryDelayMs = (error as { retryDelayMs?: unknown }).retryDelayMs;
		if (
			typeof retryDelayMs === "number" &&
			Number.isFinite(retryDelayMs) &&
			retryDelayMs > 0
		) {
			return retryDelayMs;
		}
	}
	const message =
		error instanceof Error ? error.message : JSON.stringify(error);
	const resetMatch = message.match(
		/(?:quota will reset|reset) after ([0-9hms]+)/i,
	);
	return resetMatch?.[1] ? parseDurationMs(resetMatch[1]) : undefined;
}

/**
 * Check if the model is a Gemini 3 Pro variant (gemini-3*-pro).
 * Used to determine which thinking config enum to use (thinkingLevel vs. budgetTokens).
 */
function isGemini3ProModel(modelId: string): boolean {
	return /gemini-3(?:\.1)?-pro/.test(modelId.toLowerCase());
}

/**
 * Check if the model is a Gemini 3 Flash variant (gemini-3*-flash).
 * Used to determine which thinking config enum to use (thinkingLevel vs. budgetTokens).
 */
function isGemini3FlashModel(modelId: string): boolean {
	return /gemini-3(?:\.1)?-flash/.test(modelId.toLowerCase());
}

/**
 * Check if the model is any Gemini 3 variant (Pro or Flash).
 * Determines whether to use thinkingLevel enum (Gemini 3) vs. budgetTokens (Gemini 2.x).
 */
function isGemini3Model(modelId: string): boolean {
	return isGemini3ProModel(modelId) || isGemini3FlashModel(modelId);
}

/**
 * Stream a chat completion from Google Gemini via the helper package and cli-core transport.
 *
 * The helper package owns the OAuth/bootstrap path against `@google/gemini-cli-core`, including
 * `~/.gemini/oauth_creds.json` and Gemini Code Assist project discovery. `apiKey` is ignored.
 * Casting the request as `any` works around the fact that cli-core bundles its own nested
 * `@google/genai` copy (nominal type split at packaging time; runtime shapes are byte-identical).
 * Returns a real-time stream emitting start, delta, end, and error events that accumulate into
 * an `AssistantMessage`.
 */
export const streamGoogleGeminiCli: StreamFunction<
	"google-gemini-cli",
	GoogleGeminiCliOptions
> = (
	model: Model<"google-gemini-cli">,
	context: Context,
	options?: GoogleGeminiCliOptions,
): AssistantMessageEventStream => {
	const stream = new AssistantMessageEventStream();

	(async () => {
		const output: AssistantMessage = {
			role: "assistant",
			content: [],
			api: "google-gemini-cli" as Api,
			provider: model.provider,
			model: model.id,
			usage: {
				input: 0,
				output: 0,
				cacheRead: 0,
				cacheWrite: 0,
				totalTokens: 0,
				cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
			},
			stopReason: "stop",
			timestamp: Date.now(),
		};

		try {
			let req = buildRequest(model, context, options);
			const nextReq = await options?.onPayload?.(req, model);
			if (nextReq !== undefined) {
				req = nextReq as GenerateContentParameters;
			}
			// cli-core handles auth + project discovery through the helper package.
			const server = await createGeminiCliContentGenerator({
				modelId: req.model,
			});
			const promptId = `pi-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
			// Cast through `any` — cli-core bundles its own nested @google/genai copy,
			// so TypeScript sees two structurally-identical-but-distinct Content types.
			// The runtime shapes are byte-identical; the nominal split is a packaging
			// artefact.
			const streamGen = await retryWithBackoff(
				() => server.generateContentStream(req as any, promptId, "USER" as any),
				{
					// SF owns cross-model fallback. Let cli-core classify quota errors,
					// but do not let it hold the turn through its 10-attempt retry loop.
					maxAttempts: 1,
					signal: options?.signal,
				},
			);

			let started = false;
			const ensureStarted = () => {
				if (!started) {
					stream.push({ type: "start", partial: output });
					started = true;
				}
			};

			let currentBlock: TextContent | ThinkingContent | null = null;
			const blocks = output.content;
			const blockIndex = () => blocks.length - 1;

			for await (const chunk of streamGen) {
				if (options?.signal?.aborted) {
					throw new Error("Request was aborted");
				}

				const candidate = chunk?.candidates?.[0];
				if (candidate?.content?.parts) {
					for (const part of candidate.content.parts) {
						// Text / thinking block handling
						if (part.text !== undefined) {
							const isThinking = isThinkingPart(part);
							if (
								!currentBlock ||
								(isThinking && currentBlock.type !== "thinking") ||
								(!isThinking && currentBlock.type !== "text")
							) {
								if (currentBlock) {
									if (currentBlock.type === "text") {
										stream.push({
											type: "text_end",
											contentIndex: blockIndex(),
											content: currentBlock.text,
											partial: output,
										});
									} else {
										stream.push({
											type: "thinking_end",
											contentIndex: blockIndex(),
											content: currentBlock.thinking,
											partial: output,
										});
									}
								}
								if (isThinking) {
									currentBlock = {
										type: "thinking",
										thinking: "",
										thinkingSignature: undefined,
									};
									output.content.push(currentBlock);
									ensureStarted();
									stream.push({
										type: "thinking_start",
										contentIndex: blockIndex(),
										partial: output,
									});
								} else {
									currentBlock = { type: "text", text: "" };
									output.content.push(currentBlock);
									ensureStarted();
									stream.push({
										type: "text_start",
										contentIndex: blockIndex(),
										partial: output,
									});
								}
							}
							if (currentBlock.type === "thinking") {
								currentBlock.thinking += part.text;
								currentBlock.thinkingSignature = retainThoughtSignature(
									currentBlock.thinkingSignature,
									part.thoughtSignature,
								);
								stream.push({
									type: "thinking_delta",
									contentIndex: blockIndex(),
									delta: part.text,
									partial: output,
								});
							} else {
								currentBlock.text += part.text;
								currentBlock.textSignature = retainThoughtSignature(
									currentBlock.textSignature,
									part.thoughtSignature,
								);
								stream.push({
									type: "text_delta",
									contentIndex: blockIndex(),
									delta: part.text,
									partial: output,
								});
							}
						}

						// Tool-call part
						if (part.functionCall) {
							if (currentBlock) {
								if (currentBlock.type === "text") {
									stream.push({
										type: "text_end",
										contentIndex: blockIndex(),
										content: currentBlock.text,
										partial: output,
									});
								} else {
									stream.push({
										type: "thinking_end",
										contentIndex: blockIndex(),
										content: currentBlock.thinking,
										partial: output,
									});
								}
								currentBlock = null;
							}

							const providedId = part.functionCall.id;
							const needsNewId =
								!providedId ||
								output.content.some(
									(b) => b.type === "toolCall" && b.id === providedId,
								);
							const toolCallId = needsNewId
								? `${part.functionCall.name}_${Date.now()}_${++toolCallCounter}`
								: providedId;

							const toolCall: ToolCall = {
								type: "toolCall",
								id: toolCallId,
								name: part.functionCall.name || "",
								arguments:
									(part.functionCall.args as Record<string, unknown>) ?? {},
								...(part.thoughtSignature && {
									thoughtSignature: part.thoughtSignature,
								}),
							};

							output.content.push(toolCall);
							ensureStarted();
							stream.push({
								type: "toolcall_start",
								contentIndex: blockIndex(),
								partial: output,
							});
							stream.push({
								type: "toolcall_delta",
								contentIndex: blockIndex(),
								delta: JSON.stringify(toolCall.arguments),
								partial: output,
							});
							stream.push({
								type: "toolcall_end",
								contentIndex: blockIndex(),
								toolCall,
								partial: output,
							});
						}
					}
				}

				if (candidate?.finishReason) {
					output.stopReason = mapStopReasonString(candidate.finishReason);
					if (output.content.some((b) => b.type === "toolCall")) {
						output.stopReason = "toolUse";
					}
				}

				if (chunk?.usageMetadata) {
					const promptTokens = chunk.usageMetadata.promptTokenCount || 0;
					const cacheReadTokens =
						chunk.usageMetadata.cachedContentTokenCount || 0;
					output.usage = {
						input: promptTokens - cacheReadTokens,
						output:
							(chunk.usageMetadata.candidatesTokenCount || 0) +
							(chunk.usageMetadata.thoughtsTokenCount || 0),
						cacheRead: cacheReadTokens,
						cacheWrite: 0,
						totalTokens: chunk.usageMetadata.totalTokenCount || 0,
						cost: {
							input: 0,
							output: 0,
							cacheRead: 0,
							cacheWrite: 0,
							total: 0,
						},
					};
					calculateCost(model, output.usage);
				}
			}

			// Close any open text/thinking block after stream ends
			if (currentBlock) {
				if (currentBlock.type === "text") {
					stream.push({
						type: "text_end",
						contentIndex: blockIndex(),
						content: currentBlock.text,
						partial: output,
					});
				} else {
					stream.push({
						type: "thinking_end",
						contentIndex: blockIndex(),
						content: currentBlock.thinking,
						partial: output,
					});
				}
			}

			if (options?.signal?.aborted) {
				throw new Error("Request was aborted");
			}

			if (output.stopReason === "aborted" || output.stopReason === "error") {
				throw new Error("An unknown error occurred");
			}

			stream.push({ type: "done", reason: output.stopReason, message: output });
			stream.end();
		} catch (error) {
			for (const block of output.content) {
				if ("index" in block) {
					delete (block as { index?: number }).index;
				}
			}
			output.stopReason = options?.signal?.aborted ? "aborted" : "error";
			output.errorMessage =
				error instanceof Error ? error.message : JSON.stringify(error);
			const retryAfterMs = extractRetryAfterMs(error);
			if (retryAfterMs !== undefined) {
				output.retryAfterMs = retryAfterMs;
			}
			stream.push({ type: "error", reason: output.stopReason, error: output });
			stream.end();
		}
	})();

	return stream;
};

/**
 * Simplified stream wrapper that auto-configures thinking based on model and reasoning level.
 *
 * Reasoning intent is resolved via `buildBaseOptions()` and the `reasoning` flag in `SimpleStreamOptions`.
 * For Gemini 3 models, uses the thinkingLevel enum (LOW/HIGH for Pro, MINIMAL/LOW/MEDIUM/HIGH for Flash).
 * For Gemini 2.x, maps the requested level to token budgets (default: minimal=1K, low=2K, medium=8K, high=16K).
 * Auth is still handled by cli-core (apiKey is ignored). Returns the same `AssistantMessageEventStream`
 * as `streamGoogleGeminiCli()` after delegating with appropriate `thinking` config.
 */
export const streamSimpleGoogleGeminiCli: StreamFunction<
	"google-gemini-cli",
	SimpleStreamOptions
> = (
	model: Model<"google-gemini-cli">,
	context: Context,
	options?: SimpleStreamOptions,
): AssistantMessageEventStream => {
	// cli-core sources auth from ~/.gemini/ — apiKey not required.
	const base = buildBaseOptions(model, options, options?.apiKey ?? "");
	if (!options?.reasoning) {
		return streamGoogleGeminiCli(model, context, {
			...base,
			thinking: { enabled: false },
		} satisfies GoogleGeminiCliOptions);
	}

	if (isAutoReasoning(options.reasoning)) {
		if (isGemini3Model(model.id)) {
			return streamGoogleGeminiCli(model, context, {
				...base,
				thinking: {
					enabled: true,
					level: "THINKING_LEVEL_UNSPECIFIED",
				},
			} satisfies GoogleGeminiCliOptions);
		}

		return streamGoogleGeminiCli(model, context, {
			...base,
			thinking: {
				enabled: true,
				budgetTokens: -1,
			},
		} satisfies GoogleGeminiCliOptions);
	}

	const effort = clampReasoning(
		resolveReasoningLevel(model, options.reasoning),
	)!;
	if (isGemini3Model(model.id)) {
		return streamGoogleGeminiCli(model, context, {
			...base,
			thinking: {
				enabled: true,
				level: getGeminiCliThinkingLevel(effort, model.id),
			},
		} satisfies GoogleGeminiCliOptions);
	}

	const defaultBudgets: ThinkingBudgets = {
		minimal: 1024,
		low: 2048,
		medium: 8192,
		high: 16384,
	};
	const budgets = { ...defaultBudgets, ...options.thinkingBudgets };

	const minOutputTokens = 1024;
	let thinkingBudget = budgets[effort]!;
	const maxTokens = Math.min(
		(base.maxTokens || 0) + thinkingBudget,
		model.maxTokens,
	);

	if (maxTokens <= thinkingBudget) {
		thinkingBudget = Math.max(0, maxTokens - minOutputTokens);
	}

	return streamGoogleGeminiCli(model, context, {
		...base,
		maxTokens,
		thinking: {
			enabled: true,
			budgetTokens: thinkingBudget,
		},
	} satisfies GoogleGeminiCliOptions);
};

/**
 * Build a `GenerateContentParameters` payload for cli-core's `CodeAssistServer.generateContentStream()`.
 *
 * This is the raw genai Content/Config shape (`@google/genai`), not the legacy Cloud Code Assist envelope.
 * cli-core wraps it with project, requestId, User-Agent, and retry logic; we only provide content/tools/config.
 * Unlike the old path, we do NOT need to set `project` or `requestId` — cli-core infers project from `setupUser()`.
 * Returns the exact shape the server's `generateContentStream()` method expects (casting through `any`
 * at the call site handles the vendored `@google/genai` type split).
 */
function buildRequest(
	model: Model<"google-gemini-cli">,
	context: Context,
	options: GoogleGeminiCliOptions = {},
): GenerateContentParameters {
	const contents = convertMessages(model, context);

	const config: NonNullable<GenerateContentParameters["config"]> = {};
	if (options.temperature !== undefined)
		config.temperature = options.temperature;
	if (options.maxTokens !== undefined)
		config.maxOutputTokens = options.maxTokens;

	// Thinking config
	if (options.thinking?.enabled && model.reasoning) {
		const thinkingConfig: ThinkingConfig = { includeThoughts: true };
		// Gemini 3 models use thinkingLevel, older models use thinkingBudget
		if (options.thinking.level !== undefined) {
			thinkingConfig.thinkingLevel = options.thinking
				.level as ThinkingConfig["thinkingLevel"];
		} else if (options.thinking.budgetTokens !== undefined) {
			thinkingConfig.thinkingBudget = options.thinking.budgetTokens;
		}
		config.thinkingConfig = thinkingConfig;
	}

	if (context.systemPrompt) {
		config.systemInstruction = {
			parts: [{ text: sanitizeSurrogates(context.systemPrompt) }],
		} as Content;
	}

	if (context.tools && context.tools.length > 0) {
		// Claude models historically needed the legacy `parameters` field, but
		// Claude via gemini-cli is no longer supported (Antigravity was the
		// only path). Keep the useParameters=false default.
		const useParameters = false;
		config.tools = convertTools(context.tools, useParameters) as NonNullable<
			GenerateContentParameters["config"]
		>["tools"];
		if (options.toolChoice) {
			config.toolConfig = {
				functionCallingConfig: {
					mode: mapToolChoice(options.toolChoice),
				},
			};
		}
	}

	return {
		model: resolveWireModelId(model),
		contents,
		config,
	};
}

type ClampedThinkingLevel = Exclude<ThinkingLevel, "xhigh">;

/**
 * Map a normalized thinking level (minimal/low/medium/high) to the Gemini 3 wire format.
 *
 * Gemini 3 Pro only supports LOW/HIGH (maps minimal/low -> LOW, medium/high -> HIGH).
 * Gemini 3 Flash supports all four (MINIMAL/LOW/MEDIUM/HIGH one-to-one).
 * Used when `options.thinking.level` is set for Gemini 3 models.
 */
function getGeminiCliThinkingLevel(
	effort: ClampedThinkingLevel,
	modelId: string,
): GoogleThinkingLevel {
	if (isGemini3ProModel(modelId)) {
		switch (effort) {
			case "minimal":
			case "low":
				return "LOW";
			case "medium":
			case "high":
				return "HIGH";
		}
	}
	switch (effort) {
		case "minimal":
			return "MINIMAL";
		case "low":
			return "LOW";
		case "medium":
			return "MEDIUM";
		case "high":
			return "HIGH";
	}
}
