import type { LanguageModelV3DataContent, LanguageModelV3Prompt } from "@ai-sdk/provider";
import { UnsupportedFunctionalityError } from "@ai-sdk/provider";
import { toWorkersAIToolCallId } from "./utils";
import type { WorkersAIContentPart, WorkersAIChatPrompt } from "./workersai-chat-prompt";

/**
 * Normalise any LanguageModelV3DataContent value to a Uint8Array.
 *
 * Handles:
 *   - Uint8Array  → returned as-is
 *   - string      → decoded from base64 (with or without data-URL prefix)
 *   - URL         → not supported (Workers AI needs raw bytes, not a reference)
 */
function toUint8Array(data: LanguageModelV3DataContent): Uint8Array | null {
	if (data instanceof Uint8Array) {
		return data;
	}

	if (typeof data === "string") {
		let base64 = data;
		if (base64.startsWith("data:")) {
			const commaIndex = base64.indexOf(",");
			if (commaIndex >= 0) {
				base64 = base64.slice(commaIndex + 1);
			}
		}
		const binaryString = atob(base64);
		const bytes = new Uint8Array(binaryString.length);
		for (let i = 0; i < binaryString.length; i++) {
			bytes[i] = binaryString.charCodeAt(i);
		}
		return bytes;
	}

	if (data instanceof URL) {
		throw new Error(
			"URL image sources are not supported by Workers AI. " +
				"Provide image data as a Uint8Array or base64 string instead.",
		);
	}

	return null;
}

function assertImageMediaType(mediaType: string | undefined): string {
	if (!mediaType) {
		throw new UnsupportedFunctionalityError({
			functionality: "file-part-without-media-type",
			message:
				"Workers AI chat only supports image file parts with an image/* mediaType. " +
				"Received a file part without a mediaType.",
		});
	}

	// Media types are case-insensitive (RFC 2045), so compare against a
	// lower-cased copy while preserving the caller's original casing on output.
	if (!mediaType.toLowerCase().startsWith("image/")) {
		throw new UnsupportedFunctionalityError({
			functionality: "non-image-file-part",
			message:
				"Workers AI chat only supports image file parts with an image/* mediaType. " +
				`Received mediaType "${mediaType}".`,
		});
	}

	return mediaType;
}

function uint8ArrayToBase64(bytes: Uint8Array): string {
	let binary = "";
	const chunkSize = 8192;
	for (let i = 0; i < bytes.length; i += chunkSize) {
		const chunk = bytes.subarray(i, Math.min(i + chunkSize, bytes.length));
		binary += String.fromCharCode(...chunk);
	}
	return btoa(binary);
}

export function convertToWorkersAIChatMessages(prompt: LanguageModelV3Prompt): {
	messages: WorkersAIChatPrompt;
} {
	const messages: WorkersAIChatPrompt = [];

	for (const { role, content } of prompt) {
		switch (role) {
			case "system": {
				messages.push({ content, role: "system" });
				break;
			}

			case "user": {
				const textParts: string[] = [];
				const imageParts: { image: Uint8Array; mediaType: string }[] = [];

				for (const part of content) {
					switch (part.type) {
						case "text": {
							textParts.push(part.text);
							break;
						}
						case "file": {
							const mediaType = assertImageMediaType(part.mediaType);
							const imageBytes = toUint8Array(part.data);
							if (imageBytes) {
								imageParts.push({
									image: imageBytes,
									mediaType,
								});
							}
							break;
						}
					}
				}

				if (imageParts.length > 0) {
					const contentArray: WorkersAIContentPart[] = [];
					if (textParts.length > 0) {
						contentArray.push({ type: "text", text: textParts.join("\n") });
					}
					for (const img of imageParts) {
						const base64 = uint8ArrayToBase64(img.image);
						contentArray.push({
							type: "image_url",
							image_url: { url: `data:${img.mediaType};base64,${base64}` },
						});
					}
					messages.push({ content: contentArray, role: "user" });
				} else {
					messages.push({ content: textParts.join("\n"), role: "user" });
				}

				break;
			}

			case "assistant": {
				let text = "";
				let reasoning = "";
				const toolCalls: Array<{
					id: string;
					type: "function";
					function: { name: string; arguments: string };
				}> = [];

				for (const part of content) {
					switch (part.type) {
						case "text": {
							text += part.text;
							break;
						}

						case "reasoning": {
							// Reasoning is accumulated separately and sent as the `reasoning`
							// field on the message object. This is the field name vLLM expects
							// on input for reasoning models (kimi-k2.7-code, glm-4.7-flash).
							// Concatenating it into `content` corrupts the conversation history
							// and causes models to produce empty or garbled responses on the
							// next turn.
							reasoning += part.text;
							break;
						}

						case "file": {
							// File parts in assistant messages - no action needed
							break;
						}

						case "tool-call": {
							toolCalls.push({
								function: {
									arguments: JSON.stringify(part.input),
									name: part.toolName,
								},
								id: toWorkersAIToolCallId(part.toolCallId),
								type: "function",
							});
							break;
						}

						case "tool-result": {
							// Tool results in assistant messages - no action needed
							break;
						}

						default: {
							const exhaustiveCheck = part satisfies never;
							throw new Error(
								`Unsupported part type: ${(exhaustiveCheck as { type: string }).type}`,
							);
						}
					}
				}

				messages.push({
					content: text,
					role: "assistant",
					...(reasoning ? { reasoning } : {}),
					tool_calls:
						toolCalls.length > 0
							? toolCalls.map(({ function: { name, arguments: args }, id }) => ({
									function: { arguments: args, name },
									id,
									type: "function" as const,
								}))
							: undefined,
				});

				break;
			}

			case "tool": {
				for (const toolResponse of content) {
					if (toolResponse.type === "tool-result") {
						const output = toolResponse.output;
						let content: string;
						switch (output.type) {
							case "text":
							case "error-text":
								content = output.value;
								break;
							case "json":
							case "error-json":
								content = JSON.stringify(output.value);
								break;
							case "execution-denied":
								content = output.reason
									? `Tool execution denied: ${output.reason}`
									: "Tool execution was denied.";
								break;
							case "content":
								content = output.value
									.filter(
										(p): p is { type: "text"; text: string } =>
											p.type === "text",
									)
									.map((p) => p.text)
									.join("\n");
								break;
							default:
								content = "";
								break;
						}
						messages.push({
							content,
							name: toolResponse.toolName,
							tool_call_id: toWorkersAIToolCallId(toolResponse.toolCallId),
							role: "tool",
						});
					}
					// Skip tool-approval-response parts as they're not supported by Workers AI
				}
				break;
			}

			default: {
				const exhaustiveCheck = role satisfies never;
				throw new Error(`Unsupported role: ${exhaustiveCheck}`);
			}
		}
	}

	return { messages };
}
