import { basename, extname } from "node:path";
import { readFile, stat } from "node:fs/promises";
import type { FilePart, ImagePart, ModelMessage, TextPart, ToolCallPart, ToolResultPart } from "ai";
import type { PromptContentPart } from "@best-agent/protocol";
import type { ConversationMessage } from "./provider-conversation.js";
import {
  isPersistableProviderToolName,
  toProviderHistoryToolName,
  type ProviderToolNameBinding,
} from "./provider-tool-names.js";

type AttachmentPromptContentPart = Extract<PromptContentPart, { type: "file" | "image" }>;

const EXTENSION_MEDIA_TYPES: Record<string, string> = {
  ".avi": "video/x-msvideo",
  ".bmp": "image/bmp",
  ".gif": "image/gif",
  ".ico": "image/x-icon",
  ".jpeg": "image/jpeg",
  ".jpg": "image/jpeg",
  ".json": "application/json",
  ".m4a": "audio/mp4",
  ".md": "text/markdown",
  ".mov": "video/quicktime",
  ".mp3": "audio/mpeg",
  ".mp4": "video/mp4",
  ".pdf": "application/pdf",
  ".png": "image/png",
  ".svg": "image/svg+xml",
  ".txt": "text/plain",
  ".wav": "audio/wav",
  ".webm": "video/webm",
  ".webp": "image/webp",
  ".xml": "application/xml",
  ".yaml": "text/yaml",
  ".yml": "text/yaml",
};

function guessMediaTypeFromPath(filePath: string): string | undefined {
  return EXTENSION_MEDIA_TYPES[extname(filePath).toLowerCase()];
}

async function readPromptPartPath(
  part: AttachmentPromptContentPart,
): Promise<{ data: Uint8Array } | { errorText: string }> {
  const filePath = String(part.path ?? "").trim();
  if (!filePath) {
    return { errorText: renderAttachmentReadError(part, "path is missing") };
  }
  let fileInfo;
  try {
    fileInfo = await stat(filePath);
  } catch {
    return { errorText: renderAttachmentReadError(part, `path does not exist: ${filePath}`) };
  }
  if (!fileInfo.isFile() || fileInfo.size <= 0) {
    return {
      errorText: renderAttachmentReadError(part, `path is not a readable file: ${filePath}`),
    };
  }
  try {
    const data = await readFile(filePath);
    return { data: new Uint8Array(data.buffer, data.byteOffset, data.byteLength) };
  } catch {
    return { errorText: renderAttachmentReadError(part, `path cannot be read: ${filePath}`) };
  }
}

function renderAttachmentReadError(part: AttachmentPromptContentPart, reason: string): string {
  const label = part.filename ?? (part.path ? basename(part.path) : part.type);
  return `ERROR: Cannot read ${part.type} attachment "${label}" (${reason}).`;
}

function isEmptyBase64DataUrl(value: string): boolean {
  const match = /^data:[^;]+;base64,(.*)$/u.exec(value.trim());
  return Boolean(match && match[1]!.trim().length === 0);
}

async function materializePromptContentPart(
  part: PromptContentPart,
): Promise<FilePart | ImagePart | TextPart> {
  if (part.type === "text") {
    return {
      type: "text",
      text: part.text,
    };
  }

  if (part.type === "image") {
    if (!part.path && !part.image) {
      return {
        type: "text",
        text: renderAttachmentReadError(part, "image data is missing"),
      };
    }
    if (part.image && isEmptyBase64DataUrl(part.image)) {
      return {
        type: "text",
        text: renderAttachmentReadError(part, "image data is empty or corrupted"),
      };
    }
    const mediaType = part.mediaType ?? (part.path ? guessMediaTypeFromPath(part.path) : undefined);
    const image = part.path ? await readPromptPartPath(part) : { data: String(part.image ?? "") };
    if ("errorText" in image) {
      return {
        type: "text",
        text: image.errorText,
      };
    }
    return {
      type: "image",
      image: image.data,
      ...(mediaType ? { mediaType } : {}),
    };
  }

  if (!part.path && !part.data) {
    return {
      type: "text",
      text: renderAttachmentReadError(part, "file data is missing"),
    };
  }
  const mediaType = part.mediaType ?? (part.path ? guessMediaTypeFromPath(part.path) : undefined);
  const data = part.path ? await readPromptPartPath(part) : { data: String(part.data ?? "") };
  if ("errorText" in data) {
    return {
      type: "text",
      text: data.errorText,
    };
  }
  return {
    type: "file",
    data: data.data,
    filename: part.filename ?? (part.path ? basename(part.path) : undefined),
    mediaType: mediaType ?? "application/octet-stream",
  };
}

export function conversationMessageHasAttachments(message: ConversationMessage): boolean {
  return (
    message.role === "user" && Boolean(message.contentParts?.some((part) => part.type !== "text"))
  );
}

export async function mapConversationMessagesToAiSdk(
  messages: ConversationMessage[],
  toolNameBindings: readonly ProviderToolNameBinding[] = [],
): Promise<ModelMessage[]> {
  const output: ModelMessage[] = [];
  for (const message of messages) {
    if (message.role === "tool") {
      output.push({
        role: "tool",
        content: [
          {
            type: "tool-result",
            toolCallId: message.toolCallId,
            toolName: resolveProviderHistoryToolName(
              message.toolName,
              message.providerToolName,
              toolNameBindings,
            ),
            output:
              message.outcome === "error"
                ? {
                    type: "error-text",
                    value: renderToolResultText(message),
                  }
                : {
                    type: "text",
                    value: message.content,
                  },
          } satisfies ToolResultPart,
        ],
      });
      continue;
    }

    if (message.role === "assistant") {
      if (!message.toolCalls || message.toolCalls.length === 0) {
        output.push({
          role: "assistant",
          content: message.content,
        });
        continue;
      }

      const content: Array<TextPart | ToolCallPart> = [];
      if (message.content.trim()) {
        content.push({
          type: "text",
          text: message.content,
        });
      }
      for (const toolCall of message.toolCalls) {
        content.push({
          type: "tool-call",
          input: toolCall.input,
          toolCallId: toolCall.toolCallId,
          toolName: resolveProviderHistoryToolName(
            toolCall.toolName,
            toolCall.providerToolName,
            toolNameBindings,
          ),
        });
      }
      output.push({
        role: "assistant",
        content,
      });
      continue;
    }

    if (message.role === "runtime") {
      output.push({
        role: "user",
        content: message.content,
      });
      continue;
    }

    if (!message.contentParts || message.contentParts.length === 0) {
      output.push({
        role: "user",
        content: message.content,
      });
      continue;
    }

    output.push({
      role: "user",
      content: await Promise.all(
        message.contentParts.map((part) => materializePromptContentPart(part)),
      ),
    });
  }
  return output;
}

function resolveProviderHistoryToolName(
  runtimeToolName: string,
  persistedProviderToolName: string | undefined,
  bindings: readonly ProviderToolNameBinding[],
): string {
  if (persistedProviderToolName && isPersistableProviderToolName(persistedProviderToolName)) {
    return persistedProviderToolName;
  }
  return toProviderHistoryToolName(runtimeToolName, bindings);
}

function renderToolResultText(message: Extract<ConversationMessage, { role: "tool" }>): string {
  return message.content;
}
