import { Button } from "@decocms/ui/components/button.tsx";
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
} from "@decocms/ui/components/dialog.tsx";
import { Plugin, PluginKey } from "@tiptap/pm/state";
import type { Editor } from "@tiptap/react";
import { useEffect, useRef, useState } from "react";
import { AlertTriangle, Attachment01 } from "@untitledui/icons";
import { toast } from "sonner";
import { useT, type TFunction } from "@/i18n/use-t.ts";
import {
  getSupportedFileTypesLabel,
  isFileTypeSupportedByModel,
  modelSupportsFiles,
} from "../../select-model";
import { insertFile, type FileAttrs } from "./node.tsx";
import { AiProviderModel } from "@/hooks/collections/use-ai-providers.ts";

export interface UnsupportedFileInfo {
  fileName: string;
  modelName: string;
  accepted: string;
}

interface FileUploaderProps {
  editor: Editor;
  selectedModel: AiProviderModel | null;
  onUnsupportedFile?: (info: UnsupportedFileInfo) => void;
}

/**
 * Processes a file by converting it to base64 and inserting it into the editor.
 */
export async function processFile(
  editor: Editor,
  selectedModel: AiProviderModel | null,
  file: File,
  position: number,
  onUnsupportedFile: ((info: UnsupportedFileInfo) => void) | undefined,
  t: TFunction,
): Promise<void> {
  // Check if model supports files
  if (!modelSupportsFiles(selectedModel)) {
    // ponytail: toast.error has no i18n context here (not a React component).
    // Caller will use UnsupportedFileDialog for user-facing messages.
    toast.error("Selected model does not support file uploads");
    return;
  }

  const fileMimeType = file.type || "application/octet-stream";
  if (!isFileTypeSupportedByModel(fileMimeType, selectedModel)) {
    const accepted = getSupportedFileTypesLabel(selectedModel)
      .parts.map((key) => t(key))
      .join(", ");
    const modelName = selectedModel?.title ?? "This model";
    if (onUnsupportedFile) {
      onUnsupportedFile({ fileName: file.name, modelName, accepted });
    } else {
      // ponytail: toast.error has no i18n context here (not a React component).
      // Caller will use UnsupportedFileDialog for user-facing messages.
      toast.error(`"${file.name}" can't be attached`, {
        description: `${modelName} accepts ${accepted}.`,
      });
    }
    return;
  }

  const MAX_SIZE = 10 * 1024 * 1024; // 10MB

  if (file.size > MAX_SIZE) {
    // ponytail: toast.error has no i18n context here (not a React component).
    toast.error(`File "${file.name}" exceeds 10MB limit`);
    return;
  }

  try {
    const reader = new FileReader();
    const base64Promise = new Promise<string>((resolve, reject) => {
      reader.onload = () => {
        const result = reader.result;
        if (typeof result !== "string") {
          reject(new Error("Failed to read file as data URL"));
          return;
        }
        // Remove data URL prefix (e.g., "data:image/png;base64,")
        const base64 = result.includes(",")
          ? (result.split(",")[1] ?? result)
          : result;
        resolve(base64);
      };
      reader.onerror = reject;
    });

    reader.readAsDataURL(file);

    const base64Data = await base64Promise;

    const fileAttrs: FileAttrs = {
      id: crypto.randomUUID(),
      name: file.name,
      mimeType: file.type || "application/octet-stream",
      size: file.size,
      data: base64Data,
    };

    insertFile(editor, { from: position, to: position }, fileAttrs);
  } catch (error) {
    console.error("Failed to process file:", error);
    // ponytail: toast.error has no i18n context here (not a React component).
    toast.error(`Failed to load file "${file.name}"`);
  }
}

/**
 * FileUploader component that registers a ProseMirror plugin to handle file drops.
 * Uses a ref to keep the latest selectedModel in sync for file processing.
 */
export function FileUploader({
  editor,
  selectedModel,
  onUnsupportedFile,
}: FileUploaderProps) {
  const t = useT();
  // Use a ref to store the latest processFile handler
  // This ensures we always use the latest selectedModel when processing files
  const processFileRef = useRef<
    (file: File, position: number) => Promise<void>
  >(() => Promise.resolve());

  // Keep the processFile handler in sync with selectedModel
  // eslint-disable-next-line ban-use-effect/ban-use-effect
  useEffect(() => {
    processFileRef.current = async (file: File, position: number) => {
      await processFile(
        editor,
        selectedModel,
        file,
        position,
        onUnsupportedFile,
        t,
      );
    };
  }, [editor, selectedModel, onUnsupportedFile, t]);

  // Register the file drop plugin once per editor instance
  // eslint-disable-next-line ban-use-effect/ban-use-effect
  useEffect(() => {
    if (editor?.isDestroyed) {
      return;
    }

    const pluginKey = new PluginKey("fileDropHandler");

    // Remove existing plugin if present
    const existingPlugin = editor.state.plugins.find(
      (plugin) => plugin.spec.key === pluginKey,
    );
    if (existingPlugin) {
      editor.unregisterPlugin(pluginKey);
    }

    const fileDropPlugin = new Plugin({
      key: pluginKey,
      props: {
        handleDrop: (view, event, _slice, moved) => {
          // Don't handle if it's a move within the editor
          if (moved) return false;

          const files = event.dataTransfer?.files;
          if (!files || files.length === 0) return false;

          event.preventDefault();

          // Get drop position
          const coordinates = view.posAtCoords({
            left: event.clientX,
            top: event.clientY,
          });

          if (!coordinates) return false;

          // Process dropped files one at a time, re-reading the selection after
          // each insert so multiple files land in order instead of racing to
          // insert at the same stale position.
          const fileArray = Array.from(files);
          void (async () => {
            let insertPos = coordinates.pos;
            for (const file of fileArray) {
              await processFileRef.current?.(file, insertPos);
              insertPos = view.state.selection.to;
            }
          })();

          return true;
        },

        handlePaste: (view, event) => {
          const items = event.clipboardData?.items;
          if (!items) return false;

          const fileItems = Array.from(items).filter(
            (item) => item.kind === "file",
          );
          if (fileItems.length === 0) return false;

          event.preventDefault();

          void (async () => {
            let insertPos = view.state.selection.from;
            for (const item of fileItems) {
              const file = item.getAsFile();
              if (!file) continue;
              await processFileRef.current?.(file, insertPos);
              insertPos = view.state.selection.to;
            }
          })();

          return true;
        },
      },
    });

    editor.registerPlugin(fileDropPlugin);

    return () => {
      if (!editor?.isDestroyed) {
        editor.unregisterPlugin(pluginKey);
      }
    };
  }, [editor]);

  // This component doesn't render anything
  return null;
}

/**
 * Dialog shown when the user tries to attach a file whose MIME type the
 * selected model doesn't support.
 */
export function UnsupportedFileDialog({
  info,
  onClose,
}: {
  info: UnsupportedFileInfo | null;
  onClose: () => void;
}) {
  const t = useT();
  return (
    <Dialog open={info !== null} onOpenChange={(open) => !open && onClose()}>
      <DialogContent
        className="sm:max-w-[480px] gap-0 p-0 overflow-hidden"
        onOpenAutoFocus={(e) => e.preventDefault()}
      >
        {/* Header with subtle amber warning gradient */}
        <div className="relative px-8 pt-8 pb-2 overflow-hidden">
          <div
            className="absolute inset-x-0 top-0 h-36 pointer-events-none"
            style={{
              backgroundImage: [
                "radial-gradient(ellipse 40% 200% at -5% 100%, rgba(251,191,36,0.35) 0%, transparent 100%)",
                "radial-gradient(ellipse 40% 200% at 105% -10%, rgba(244,114,182,0.25) 0%, transparent 100%)",
              ].join(", "),
              maskImage:
                "linear-gradient(to bottom, black 0%, transparent 100%)",
              WebkitMaskImage:
                "linear-gradient(to bottom, black 0%, transparent 100%)",
            }}
          />
          <DialogHeader className="relative gap-4">
            <div className="flex items-center justify-center size-9 rounded-lg bg-warning/15 text-warning">
              <AlertTriangle size={18} />
            </div>
            <div>
              <DialogTitle className="text-xl font-semibold tracking-tight">
                {t("chat.uploader.fileTypeNotSupported")}
              </DialogTitle>
              <DialogDescription className="mt-1.5 text-sm leading-relaxed">
                <span className="font-medium text-foreground">
                  &ldquo;{info?.fileName}&rdquo;
                </span>{" "}
                {t("chat.uploader.cantAttachToChat")}
              </DialogDescription>
            </div>
          </DialogHeader>
        </div>

        {/* Body */}
        <div className="px-8 pt-3 pb-6 space-y-3">
          <div className="rounded-xl border border-border p-4">
            <div className="text-xs font-medium uppercase tracking-wide text-muted-foreground mb-2">
              {t("chat.uploader.accepts", { modelName: info?.modelName ?? "" })}
            </div>
            <div className="flex items-center gap-2 text-sm text-foreground">
              <Attachment01 size={14} className="text-muted-foreground" />
              <span className="capitalize">{info?.accepted}</span>
            </div>
          </div>
        </div>

        {/* Footer */}
        <div className="px-8 py-4 border-t border-border bg-muted/30 flex items-center justify-end">
          <Button onClick={onClose}>{t("chat.uploader.gotIt")}</Button>
        </div>
      </DialogContent>
    </Dialog>
  );
}

export function useUnsupportedFileDialog() {
  const [unsupportedFile, setUnsupportedFile] =
    useState<UnsupportedFileInfo | null>(null);
  return {
    unsupportedFile,
    onUnsupportedFile: setUnsupportedFile,
    clearUnsupportedFile: () => setUnsupportedFile(null),
  };
}
