/**
 * Google Gemini CLI provider.
 * Uses the Cloud Code Assist API endpoint (cloudcode-pa.googleapis.com) to
 * access Gemini models with the user's OAuth credentials from the real
 * `gemini` CLI. Follow-up work will re-platform this on top of
 * @google/gemini-cli-core so our requests are indistinguishable from the
 * official CLI's. See google-gemini-cli-core-plan.md.
 */

import type { Content, GenerateContentParameters, GenerateContentResponse, ThinkingConfig } from "@google/genai";
import { CodeAssistServer, makeFakeConfig } from "@google/gemini-cli-core";
import { OAuth2Client } from "google-auth-library";
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.
 * Mirrors Google's ThinkingLevel enum values.
 */
export type GoogleThinkingLevel = "THINKING_LEVEL_UNSPECIFIED" | "MINIMAL" | "LOW" | "MEDIUM" | "HIGH";

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;

/**
 * Build a CodeAssistServer from the SF-stored auth blob.
 *
 * SF caches Google OAuth as `apiKey` = JSON-encoded `{ token, projectId }`.
 * We unpack it and hand the access token to a google-auth-library
 * OAuth2Client, which is what cli-core's CodeAssistServer wants.
 *
 * The transport itself (User-Agent, Client-Metadata, request path,
 * retry/backoff on 429/5xx) is cli-core's job from here — byte-identical
 * to what the real `gemini` CLI sends, so subject to the same free-tier
 * treatment Google gives its own client.
 */
function buildCodeAssistServer(token: string, projectId: string): CodeAssistServer {
	const authClient = new OAuth2Client();
	authClient.setCredentials({ access_token: token });
	// httpOptions is an empty-headers pass-through — cli-core sets the
	// correct User-Agent / Client-Metadata / X-Goog-Api-Client itself.
	// cli-core vendors its own google-auth-library copy, so TypeScript sees
	// a package-identity mismatch even though the runtime object shape is
	// compatible with the constructor's AuthClient contract.
	return new CodeAssistServer(
		authClient as unknown as ConstructorParameters<typeof CodeAssistServer>[0],
		projectId,
		{ headers: {} },
	);
}


function isGemini3ProModel(modelId: string): boolean {
	return /gemini-3(?:\.1)?-pro/.test(modelId.toLowerCase());
}

function isGemini3FlashModel(modelId: string): boolean {
	return /gemini-3(?:\.1)?-flash/.test(modelId.toLowerCase());
}

function isGemini3Model(modelId: string): boolean {
	return isGemini3ProModel(modelId) || isGemini3FlashModel(modelId);
}

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 {
			// apiKey is JSON-encoded: { token, projectId }
			const apiKeyRaw = options?.apiKey;
			if (!apiKeyRaw) {
				throw new Error("Google Cloud Code Assist requires OAuth authentication. Use /login to authenticate.");
			}
			let accessToken: string;
			let projectId: string;
			try {
				const parsed = JSON.parse(apiKeyRaw) as { token: string; projectId: string };
				accessToken = parsed.token;
				projectId = parsed.projectId;
			} catch {
				throw new Error("Invalid Google Cloud Code Assist credentials. Use /login to re-authenticate.");
			}
			if (!accessToken || !projectId) {
				throw new Error("Missing token or projectId in Google Cloud credentials. Use /login to re-authenticate.");
			}

			const server = buildCodeAssistServer(accessToken, projectId);
			let req = buildRequest(model, context, options);
			const nextReq = await options?.onPayload?.(req, model);
			if (nextReq !== undefined) {
				req = nextReq as GenerateContentParameters;
			}
			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.
			// biome-ignore lint/suspicious/noExplicitAny: see above
			const streamGen = await server.generateContentStream(req as any, promptId, "USER" as any);

			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);
			stream.push({ type: "error", reason: output.stopReason, error: output });
			stream.end();
		}
	})();

	return stream;
};

export const streamSimpleGoogleGeminiCli: StreamFunction<"google-gemini-cli", SimpleStreamOptions> = (
	model: Model<"google-gemini-cli">,
	context: Context,
	options?: SimpleStreamOptions,
): AssistantMessageEventStream => {
	const apiKey = options?.apiKey;
	if (!apiKey) {
		throw new Error("Google Cloud Code Assist requires OAuth authentication. Use /login to authenticate.");
	}

	const base = buildBaseOptions(model, 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 shape
 * `@google/genai` defines — different from the old Cloud-Code-Assist
 * request envelope (cli-core does the envelope wrapping itself).
 *
 * Note: the old handwritten envelope included `project`, `requestId`,
 * `userAgent` fields. cli-core manages all of those — we only supply
 * the content shape.
 */
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: model.id,
		contents,
		config,
	};
}

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

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";
	}
}
