// Lazy-loaded: OpenAI SDK (AzureOpenAI) is imported on first use, not at startup.
// This avoids penalizing users who don't use Azure OpenAI models.
import type { AzureOpenAI } from "openai";
import type { ResponseCreateParamsStreaming } from "openai/resources/responses/responses.js";
import { getEnvApiKey } from "../env-api-keys.js";
import { resolveWireModelId } from "../model-identity.js";
import { supportsXhigh } from "../models.js";
import type {
	Context,
	Model,
	SimpleStreamOptions,
	StreamFunction,
	StreamOptions,
} from "../types.js";
import { AssistantMessageEventStream } from "../utils/event-stream.js";
import {
	convertResponsesMessages,
	convertResponsesTools,
	processResponsesStream,
} from "./openai-responses-shared.js";
import {
	assertStreamSuccess,
	buildInitialOutput,
	clampReasoningForModel,
	finalizeStream,
	handleStreamError,
} from "./openai-shared.js";
import {
	buildBaseOptions,
	clampReasoning,
	resolveReasoningLevel,
} from "./simple-options.js";

let _AzureOpenAIClass: typeof AzureOpenAI | undefined;
async function getAzureOpenAIClass(): Promise<typeof AzureOpenAI> {
	if (!_AzureOpenAIClass) {
		const mod = await import("openai");
		_AzureOpenAIClass = mod.AzureOpenAI;
	}
	return _AzureOpenAIClass;
}

const DEFAULT_AZURE_API_VERSION = "v1";
const AZURE_TOOL_CALL_PROVIDERS = new Set([
	"openai",
	"openai-codex",
	"opencode",
	"azure-openai-responses",
]);

function parseDeploymentNameMap(
	value: string | undefined,
): Map<string, string> {
	const map = new Map<string, string>();
	if (!value) return map;
	for (const entry of value.split(",")) {
		const trimmed = entry.trim();
		if (!trimmed) continue;
		const [modelId, deploymentName] = trimmed.split("=", 2);
		if (!modelId || !deploymentName) continue;
		map.set(modelId.trim(), deploymentName.trim());
	}
	return map;
}

function resolveDeploymentName(
	model: Model<"azure-openai-responses">,
	options?: AzureOpenAIResponsesOptions,
): string {
	if (options?.azureDeploymentName) {
		return options.azureDeploymentName;
	}
	const mappedDeployment = parseDeploymentNameMap(
		process.env.AZURE_OPENAI_DEPLOYMENT_NAME_MAP,
	).get(model.id);
	return mappedDeployment || resolveWireModelId(model);
}

// Azure OpenAI Responses-specific options
export interface AzureOpenAIResponsesOptions extends StreamOptions {
	reasoningEffort?: "minimal" | "low" | "medium" | "high" | "xhigh";
	reasoningSummary?: "auto" | "detailed" | "concise" | null;
	azureApiVersion?: string;
	azureResourceName?: string;
	azureBaseUrl?: string;
	azureDeploymentName?: string;
}

/**
 * Generate function for Azure OpenAI Responses API
 */
export const streamAzureOpenAIResponses: StreamFunction<
	"azure-openai-responses",
	AzureOpenAIResponsesOptions
> = (
	model: Model<"azure-openai-responses">,
	context: Context,
	options?: AzureOpenAIResponsesOptions,
): AssistantMessageEventStream => {
	const stream = new AssistantMessageEventStream();

	// Start async processing
	(async () => {
		const deploymentName = resolveDeploymentName(model, options);
		const output = buildInitialOutput(model);

		try {
			// Create Azure OpenAI client
			const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
			const client = await createClient(model, apiKey, options);
			let params = buildParams(model, context, options, deploymentName);
			const nextParams = await options?.onPayload?.(params, model);
			if (nextParams !== undefined) {
				params = nextParams as ResponseCreateParamsStreaming;
			}
			const openaiStream = await client.responses.create(
				params,
				options?.signal ? { signal: options.signal } : undefined,
			);
			stream.push({ type: "start", partial: output });

			await processResponsesStream(openaiStream, output, stream, model);

			assertStreamSuccess(output, options?.signal);
			finalizeStream(stream, output);
		} catch (error) {
			handleStreamError(stream, output, error, options?.signal);
		}
	})();

	return stream;
};

export const streamSimpleAzureOpenAIResponses: StreamFunction<
	"azure-openai-responses",
	SimpleStreamOptions
> = (
	model: Model<"azure-openai-responses">,
	context: Context,
	options?: SimpleStreamOptions,
): AssistantMessageEventStream => {
	const apiKey = options?.apiKey || getEnvApiKey(model.provider);
	if (!apiKey) {
		throw new Error(`No API key for provider: ${model.provider}`);
	}

	const base = buildBaseOptions(model, options, apiKey);
	const effectiveReasoning = resolveReasoningLevel(model, options?.reasoning);
	const reasoningEffort = supportsXhigh(model)
		? effectiveReasoning
		: clampReasoning(effectiveReasoning);

	return streamAzureOpenAIResponses(model, context, {
		...base,
		reasoningEffort,
	} satisfies AzureOpenAIResponsesOptions);
};

function normalizeAzureBaseUrl(baseUrl: string): string {
	return baseUrl.replace(/\/+$/, "");
}

function buildDefaultBaseUrl(resourceName: string): string {
	return `https://${resourceName}.openai.azure.com/openai/v1`;
}

function isCognitiveServicesDomain(url: string): boolean {
	try {
		const hostname = new URL(url).hostname;
		return hostname.endsWith(".cognitiveservices.azure.com");
	} catch {
		return false;
	}
}

function normalizeCognitiveServicesUrl(url: string): string {
	// Azure Cognitive Services endpoints use /openai/deployments/{deployment}/chat/completions
	// We need to normalize to the OpenAI-compatible base path
	if (url.includes("/openai/deployments/")) {
		return url.split("/openai/deployments/")[0]!;
	}
	return url;
}

function resolveAzureConfig(
	model: Model<"azure-openai-responses">,
	options?: AzureOpenAIResponsesOptions,
): { baseUrl: string; apiVersion: string } {
	const apiVersion =
		options?.azureApiVersion ||
		process.env.AZURE_OPENAI_API_VERSION ||
		DEFAULT_AZURE_API_VERSION;

	const baseUrl =
		options?.azureBaseUrl?.trim() ||
		process.env.AZURE_OPENAI_BASE_URL?.trim() ||
		undefined;
	const resourceName =
		options?.azureResourceName || process.env.AZURE_OPENAI_RESOURCE_NAME;

	let resolvedBaseUrl = baseUrl;

	if (!resolvedBaseUrl && resourceName) {
		resolvedBaseUrl = buildDefaultBaseUrl(resourceName);
	}

	if (!resolvedBaseUrl && model.baseUrl) {
		resolvedBaseUrl = model.baseUrl;
	}

	if (!resolvedBaseUrl) {
		throw new Error(
			"Azure OpenAI base URL is required. Set AZURE_OPENAI_BASE_URL or AZURE_OPENAI_RESOURCE_NAME, or pass azureBaseUrl, azureResourceName, or model.baseUrl.",
		);
	}

	// Normalize Cognitive Services endpoints (e.g., .cognitiveservices.azure.com)
	if (isCognitiveServicesDomain(resolvedBaseUrl)) {
		resolvedBaseUrl = normalizeCognitiveServicesUrl(resolvedBaseUrl);
	}

	return {
		baseUrl: normalizeAzureBaseUrl(resolvedBaseUrl),
		apiVersion,
	};
}

async function createClient(
	model: Model<"azure-openai-responses">,
	apiKey: string,
	options?: AzureOpenAIResponsesOptions,
) {
	if (!apiKey) {
		if (!process.env.AZURE_OPENAI_API_KEY) {
			throw new Error(
				"Azure OpenAI API key is required. Set AZURE_OPENAI_API_KEY environment variable or pass it as an argument.",
			);
		}
		apiKey = process.env.AZURE_OPENAI_API_KEY;
	}

	const headers = { ...model.headers };

	if (options?.headers) {
		Object.assign(headers, options.headers);
	}

	const { baseUrl, apiVersion } = resolveAzureConfig(model, options);
	const AzureOpenAIClass = await getAzureOpenAIClass();

	return new AzureOpenAIClass({
		apiKey,
		apiVersion,
		dangerouslyAllowBrowser: true,
		defaultHeaders: headers,
		baseURL: baseUrl,
	});
}

function buildParams(
	model: Model<"azure-openai-responses">,
	context: Context,
	options: AzureOpenAIResponsesOptions | undefined,
	deploymentName: string,
) {
	const messages = convertResponsesMessages(
		model,
		context,
		AZURE_TOOL_CALL_PROVIDERS,
	);

	const params: ResponseCreateParamsStreaming = {
		model: deploymentName,
		input: messages,
		stream: true,
		prompt_cache_key: options?.sessionId,
	};

	if (options?.maxTokens) {
		params.max_output_tokens = options?.maxTokens;
	}

	if (options?.temperature !== undefined) {
		params.temperature = options?.temperature;
	}

	if (context.tools && context.tools.length > 0) {
		params.tools = convertResponsesTools(context.tools);
	}

	if (model.reasoning) {
		if (options?.reasoningEffort || options?.reasoningSummary) {
			const effort = clampReasoningForModel(
				model.name,
				options?.reasoningEffort || "medium",
			) as typeof options.reasoningEffort;
			params.reasoning = {
				effort: effort || "medium",
				summary: options?.reasoningSummary || "auto",
			};
			params.include = ["reasoning.encrypted_content"];
		} else {
			if (model.name.toLowerCase().startsWith("gpt-5")) {
				// Jesus Christ, see https://community.openai.com/t/need-reasoning-false-option-for-gpt-5/1351588/7
				messages.push({
					role: "developer",
					content: [
						{
							type: "input_text",
							text: "# Juice: 0 !important",
						},
					],
				});
			}
		}
	}

	return params;
}
