import type { Message } from "@langchain/langgraph-sdk";
import { FileIcon, Loader2Icon } from "lucide-react";
import { memo, useMemo, useState, type ImgHTMLAttributes } from "react";
import rehypeKatex from "rehype-katex";

import { Loader } from "@/components/ai-elements/loader";
import {
  Message as AIElementMessage,
  MessageContent as AIElementMessageContent,
  MessageResponse as AIElementMessageResponse,
  MessageToolbar,
} from "@/components/ai-elements/message";
import {
  Reasoning,
  ReasoningContent,
  ReasoningTrigger,
} from "@/components/ai-elements/reasoning";
import { Task, TaskTrigger } from "@/components/ai-elements/task";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuTrigger,
} from "@/components/ui/context-menu";
import { resolveArtifactURL } from "@/core/artifacts/utils";
import { useI18n } from "@/core/i18n/hooks";
import {
  extractSummaryTemplateBody,
  extractContentFromMessage,
  normalizeHumanMessageDisplayText,
  extractReasoningContentFromMessage,
  isSummaryTemplateMessage,
  parseUploadedFiles,
  stripPriorityHintSuffix,
  stripUploadedFilesTag,
  type FileInMessage,
} from "@/core/messages/utils";
import { useRehypeSplitWordsIntoSpans } from "@/core/rehype";
import { materializeSkillYaml } from "@/core/skills";
import { dispatchMentionReference } from "@/core/threads/reference-events";
import { cn } from "@/lib/utils";

import { CopyButton } from "../copy-button";

import { MarkdownContent } from "./markdown-content";

function localizeAssistantFixedCopy(content: string, localized: string): string {
  if (content.includes("The account balance is insufficient for this model call.")) {
    return localized;
  }
  return content;
}

function buildClipboardData(message: Message): string {
  const raw =
    extractContentFromMessage(message) ??
    extractReasoningContentFromMessage(message) ??
    "";
  if (!raw) {
    return "";
  }

  const cleaned = stripUploadedFilesTag(raw);
  if (message.type === "human") {
    return normalizeHumanMessageDisplayText(stripPriorityHintSuffix(cleaned));
  }
  return cleaned;
}

export function MessageListItem({
  className,
  message,
  isLoading,
  threadId,
  isFirstInSession = false,
}: {
  className?: string;
  message: Message;
  isLoading?: boolean;
  threadId: string;
  isFirstInSession?: boolean;
}) {
  const isHuman = message.type === "human";
  return (
    <AIElementMessage
      className={cn(
        "group/conversation-message relative mb-1 w-full",
        className,
      )}
      from={isHuman ? "user" : "assistant"}
      isFirstInSession={isFirstInSession}
    >
      <MessageContent
        className={isHuman ? "w-fit" : "w-full"}
        message={message}
        isLoading={isLoading}
        threadId={threadId}
      />
      {!isLoading && (
        <MessageToolbar
          className={cn(
            isHuman ? "-bottom-8 justify-end" : "-bottom-8",
            "absolute right-0 left-0 z-20 opacity-0 transition-opacity delay-200 duration-300 group-hover/conversation-message:opacity-100",
          )}
        >
          <div className="flex gap-1">
            <CopyButton clipboardData={buildClipboardData(message)} />
          </div>
        </MessageToolbar>
      )}
    </AIElementMessage>
  );
}

/**
 * Custom image component that handles artifact URLs
 */
function MessageImage({
  src,
  alt,
  threadId,
  maxWidth = "90%",
  ...props
}: React.ImgHTMLAttributes<HTMLImageElement> & {
  threadId: string;
  maxWidth?: string;
}) {
  if (!src) return null;

  const imgClassName = cn("overflow-hidden rounded-lg", `max-w-[${maxWidth}]`);

  if (typeof src !== "string") {
    return <img className={imgClassName} src={src} alt={alt} {...props} />;
  }

  const url =
    src.startsWith("/mnt/") && threadId
      ? resolveArtifactURL(src, threadId)
      : src;

  return (
    <a href={url} target="_blank" rel="noopener noreferrer">
      <img className={imgClassName} src={url} alt={alt} {...props} />
    </a>
  );
}

function MessageContent_({
  className,
  message,
  isLoading = false,
  threadId,
}: {
  className?: string;
  message: Message;
  isLoading?: boolean;
  threadId: string;
}) {
  const { t } = useI18n();
  const rehypePlugins = useRehypeSplitWordsIntoSpans(isLoading);
  const isHuman = message.type === "human";
  const components = useMemo(
    () => ({
      img: (props: ImgHTMLAttributes<HTMLImageElement>) => (
        <MessageImage {...props} threadId={threadId} maxWidth="90%" />
      ),
    }),
    [threadId],
  );

  const rawContent = extractContentFromMessage(message);
  const reasoningContent = extractReasoningContentFromMessage(message);

  const files = useMemo(() => {
    const files = message.additional_kwargs?.files;
    if (!Array.isArray(files) || files.length === 0) {
      if (rawContent.includes("<uploaded_files>")) {
        // If the content contains the <uploaded_files> tag, we return the parsed files from the content for backward compatibility.
        return parseUploadedFiles(rawContent);
      }
      return null;
    }
    return files as FileInMessage[];
  }, [message.additional_kwargs?.files, rawContent]);

  const contentToDisplay = useMemo(() => {
    if (isHuman) {
      if (!rawContent) {
        return "";
      }
      const cleaned = stripPriorityHintSuffix(stripUploadedFilesTag(rawContent));
      return normalizeHumanMessageDisplayText(cleaned);
    }
    if (!rawContent) {
      return "";
    }
    return localizeAssistantFixedCopy(
      rawContent,
      t.threads.billingInsufficientBalance,
    );
  }, [rawContent, isHuman, t.threads.billingInsufficientBalance]);
  const isSummaryMessage = useMemo(
    () => isHuman && isSummaryTemplateMessage(message),
    [isHuman, message],
  );
  const summaryBody = useMemo(
    () => (isSummaryMessage ? extractSummaryTemplateBody(message) : ""),
    [isSummaryMessage, message],
  );
  const [isSummaryExpanded, setIsSummaryExpanded] = useState(false);

  const filesList =
    files && files.length > 0 && threadId ? (
      <RichFilesList files={files} threadId={threadId} />
    ) : null;

  // Uploading state: mock AI message shown while files upload
  if (message.additional_kwargs?.element === "task") {
    return (
      <AIElementMessageContent className={className}>
        <Task defaultOpen={false}>
          <TaskTrigger title="">
            <div className="text-muted-foreground flex w-full cursor-default items-center gap-2 text-sm select-none">
              <Loader className="size-4" />
              <span>{contentToDisplay}</span>
            </div>
          </TaskTrigger>
        </Task>
      </AIElementMessageContent>
    );
  }

  // Reasoning-only AI message (no main response content yet)
  if (!isHuman && reasoningContent && !rawContent) {
    return (
      <AIElementMessageContent className={className}>
        <Reasoning isStreaming={isLoading}>
          <ReasoningTrigger />
          <ReasoningContent>{reasoningContent}</ReasoningContent>
        </Reasoning>
      </AIElementMessageContent>
    );
  }

  if (isHuman) {
    const shouldRenderSummaryCollapse = isSummaryMessage && summaryBody;
    const messageResponse = contentToDisplay ? (
      <div className="whitespace-break-spaces break-words">
        {contentToDisplay}
      </div>
    ) : null;
    return (
      <div className={cn("ml-auto flex flex-col gap-2", className)}>
        {filesList}
        {shouldRenderSummaryCollapse && (
          <details
            className="w-fit max-w-full rounded-lg border"
            open={isSummaryExpanded}
            onToggle={(event) => {
              setIsSummaryExpanded(event.currentTarget.open);
            }}
          >
            <summary className="text-muted-foreground cursor-pointer px-3 py-2 text-xs select-none">
              {isSummaryExpanded
                ? t.toolCalls.collapseContent
                : t.toolCalls.expandContent}
            </summary>
            <AIElementMessageContent className="w-fit border-t">
              <div className="whitespace-break-spaces break-words">
                {summaryBody}
              </div>
            </AIElementMessageContent>
          </details>
        )}
        {messageResponse && (
          <AIElementMessageContent
            className={cn(
              "w-fit",
              shouldRenderSummaryCollapse ? "hidden" : undefined,
            )}
          >
            {messageResponse}
          </AIElementMessageContent>
        )}
      </div>
    );
  }

  return (
    <AIElementMessageContent className={className}>
      {filesList}
      <MarkdownContent
        content={contentToDisplay}
        isLoading={isLoading}
        rehypePlugins={[...rehypePlugins, [rehypeKatex, { output: "html" }]]}
        // className="my-3"
        components={components}
      />
    </AIElementMessageContent>
  );
}

/**
 * Get file extension and check helpers
 */
const getFileExt = (filename: string) =>
  filename.split(".").pop()?.toLowerCase() ?? "";

const FILE_TYPE_MAP: Record<string, string> = {
  json: "JSON",
  csv: "CSV",
  txt: "TXT",
  md: "Markdown",
  py: "Python",
  js: "JavaScript",
  ts: "TypeScript",
  tsx: "TSX",
  jsx: "JSX",
  html: "HTML",
  css: "CSS",
  xml: "XML",
  yaml: "YAML",
  yml: "YAML",
  pdf: "PDF",
  png: "PNG",
  jpg: "JPG",
  jpeg: "JPEG",
  gif: "GIF",
  svg: "SVG",
  zip: "ZIP",
  tar: "TAR",
  gz: "GZ",
};

const IMAGE_EXTENSIONS = ["png", "jpg", "jpeg", "gif", "webp", "svg", "bmp"];

function getFileTypeLabel(filename: string): string {
  const ext = getFileExt(filename);
  return FILE_TYPE_MAP[ext] ?? (ext.toUpperCase() || "FILE");
}

function isImageFile(filename: string): boolean {
  return IMAGE_EXTENSIONS.includes(getFileExt(filename));
}

function isYamlFile(filename: string): boolean {
  const ext = getFileExt(filename);
  return ext === "yaml" || ext === "yml";
}

/**
 * Format bytes to human-readable size string
 */
function formatBytes(bytes: number | string): string {
  const numericBytes = typeof bytes === "string" ? Number(bytes) : bytes;
  if (!Number.isFinite(numericBytes)) return "—";
  const safeBytes = Math.max(0, numericBytes);
  if (safeBytes === 0) return "—";
  const kb = safeBytes / 1024;
  if (kb < 1024) return `${kb.toFixed(1)} KB`;
  return `${(kb / 1024).toFixed(1)} MB`;
}

/**
 * List of files from additional_kwargs.files (with optional upload status)
 */
function RichFilesList({
  files,
  threadId,
}: {
  files: FileInMessage[];
  threadId: string;
}) {
  if (files.length === 0) return null;
  return (
    <div className="mb-2 flex flex-wrap justify-end gap-2">
      {files.map((file, index) => (
        <RichFileCard
          key={`${file.filename}-${index}`}
          file={file}
          threadId={threadId}
        />
      ))}
    </div>
  );
}

/**
 * Single file card that handles FileInMessage (supports uploading state)
 */
function RichFileCard({
  file,
  threadId,
}: {
  file: FileInMessage;
  threadId: string;
}) {
  const { t } = useI18n();
  const isUploading = file.status === "uploading";
  const isImage = isImageFile(file.filename);
  const isYaml = isYamlFile(file.filename);
  const [isMaterializing, setIsMaterializing] = useState(false);
  const [materializeMessage, setMaterializeMessage] = useState<string | null>(
    null,
  );

  if (isUploading) {
    return (
      <div className="bg-background border-border/40 flex max-w-[200px] min-w-[120px] flex-col gap-1 rounded-lg border p-3 opacity-60 shadow-sm">
        <div className="flex items-start gap-2">
          <Loader2Icon className="text-muted-foreground mt-0.5 size-4 shrink-0 animate-spin" />
          <span
            className="text-foreground truncate text-sm font-medium"
            title={file.filename}
          >
            {file.filename}
          </span>
        </div>
        <div className="flex items-center justify-between gap-2">
          <Badge
            variant="secondary"
            className="rounded px-1.5 py-0.5 text-[10px] font-normal"
          >
            {getFileTypeLabel(file.filename)}
          </Badge>
          <span className="text-muted-foreground text-[10px]">
            {t.uploads.uploading}
          </span>
        </div>
      </div>
    );
  }

  if (!file.path) return null;

  const fileUrl = resolveArtifactURL(file.path, threadId);

  const handleMaterializeYaml = async () => {
    if (!isYaml || isMaterializing) return;
    setIsMaterializing(true);
    setMaterializeMessage(null);
    try {
      const result = await materializeSkillYaml({
        thread_id: threadId,
        path: file.path!,
        target_dir: "/mnt/user-data/uploads/skill",
        clear_target: true,
      });
      setMaterializeMessage(
        t.messageListItem.materializeSuccess(
          result.created_files,
          result.created_directories,
        ),
      );
    } catch (error) {
      const message =
        error instanceof Error ? error.message : t.messageListItem.parseFailed;
      setMaterializeMessage(t.messageListItem.materializeFailed(message));
    } finally {
      setIsMaterializing(false);
    }
  };

  if (isImage) {
    const refSource = file.ref_source ?? "upload";
    const canReference = Boolean(file.path);

    return (
      <ContextMenu>
        <ContextMenuTrigger asChild>
          <a
            href={fileUrl}
            target="_blank"
            rel="noopener noreferrer"
            className="group border-border/40 relative block overflow-hidden rounded-lg border"
          >
            <img
              src={fileUrl}
              alt={file.filename}
              className="h-32 w-auto max-w-[240px] object-cover transition-transform group-hover:scale-105"
            />
          </a>
        </ContextMenuTrigger>
        <ContextMenuContent className="min-w-[120px]">
          <ContextMenuItem
            disabled={!canReference}
            onClick={() => {
              if (!file.path) return;
              dispatchMentionReference({
                threadId,
                filename: file.filename,
                path: file.path,
                ref_source: refSource,
              });
            }}
          >
            {t.common.reference}
          </ContextMenuItem>
        </ContextMenuContent>
      </ContextMenu>
    );
  }

  return (
    <div className="bg-background border-border/40 flex max-w-[200px] min-w-[120px] flex-col gap-1 rounded-lg border p-3 shadow-sm">
      <div className="flex items-start gap-2">
        <FileIcon className="text-muted-foreground mt-0.5 size-4 shrink-0" />
        <span
          className="text-foreground truncate text-sm font-medium"
          title={file.filename}
        >
          {file.filename}
        </span>
      </div>
      <div className="flex items-center justify-between gap-2">
        <Badge
          variant="secondary"
          className="rounded px-1.5 py-0.5 text-[10px] font-normal"
        >
          {getFileTypeLabel(file.filename)}
        </Badge>
        <span className="text-muted-foreground text-[10px]">
          {formatBytes(file.size)}
        </span>
      </div>
      {isYaml && (
        <div className="mt-1 flex flex-col gap-1">
          <Button
            size="sm"
            variant="secondary"
            className="h-7 text-xs"
            onClick={() => {
              void handleMaterializeYaml();
            }}
            disabled={isMaterializing}
          >
            {isMaterializing
              ? t.messageListItem.materializing
              : t.messageListItem.importAsSkillDir}
          </Button>
          {materializeMessage && (
            <span className="text-muted-foreground text-[10px] leading-tight">
              {materializeMessage}
            </span>
          )}
        </div>
      )}
    </div>
  );
}

const MessageContent = memo(MessageContent_);
