import { ReactRenderer } from "@tiptap/react";
import tippy, { type Instance as TippyInstance } from "tippy.js";
import { type SuggestionProps, type SuggestionKeyDownProps } from "@tiptap/suggestion";
import {
  forwardRef,
  useEffect,
  useImperativeHandle,
  useMemo,
  useState,
  useCallback,
} from "react";
import type { Editor } from "@tiptap/core";
import { getComponentSlashItems } from "./components";

type SlashSection =
  | "Insert"
  | "Text"
  | "Academic"
  | "Media"
  | "Layout"
  | "Advanced";

interface SlashItem {
  title: string;
  description: string;
  icon: string;
  section: SlashSection;
  command: (editor: Editor) => void;
}

const BUILT_IN_ITEMS: SlashItem[] = [
  {
    title: "Heading 1",
    description: "Large section heading",
    icon: "H1",
    section: "Text",
    command: (editor) =>
      editor.chain().focus().toggleHeading({ level: 1 }).run(),
  },
  {
    title: "Heading 2",
    description: "Medium section heading",
    icon: "H2",
    section: "Text",
    command: (editor) =>
      editor.chain().focus().toggleHeading({ level: 2 }).run(),
  },
  {
    title: "Heading 3",
    description: "Small section heading",
    icon: "H3",
    section: "Text",
    command: (editor) =>
      editor.chain().focus().toggleHeading({ level: 3 }).run(),
  },
  {
    title: "Bullet list",
    description: "Unordered list",
    icon: "•",
    section: "Text",
    command: (editor) => editor.chain().focus().toggleBulletList().run(),
  },
  {
    title: "Numbered list",
    description: "Ordered list",
    icon: "1.",
    section: "Text",
    command: (editor) => editor.chain().focus().toggleOrderedList().run(),
  },
  {
    title: "Quote",
    description: "Blockquote",
    icon: "❝",
    section: "Text",
    command: (editor) => editor.chain().focus().toggleBlockquote().run(),
  },
  {
    title: "Code block",
    description: "Code with syntax highlighting",
    icon: "<>",
    section: "Text",
    command: (editor) => editor.chain().focus().toggleCodeBlock().run(),
  },
  {
    title: "Divider",
    description: "Horizontal rule",
    icon: "—",
    section: "Text",
    command: (editor) => editor.chain().focus().setHorizontalRule().run(),
  },
  {
    title: "Image",
    description: "Upload or embed an image",
    icon: "🖼",
    section: "Insert",
    command: (editor) => {
      editor.chain().focus().insertImageUpload().run();
    },
  },
  {
    title: "Citation",
    description: "Cite a paper (DOI or BibTeX)",
    icon: "📎",
    section: "Academic",
    command: () => {
      window.dispatchEvent(new CustomEvent("open-citation-panel"));
    },
  },
  {
    title: "Glossary",
    description: "Highlighted term with tooltip definition",
    icon: "📖",
    section: "Academic",
    command: (editor) => {
      editor.chain().focus().insertGlossary("term", "Definition here…").run();
    },
  },
  {
    title: "Footnote",
    description: "Numbered footnote reference",
    icon: "¹",
    section: "Academic",
    command: (editor) => {
      editor.chain().focus().insertFootnote("Footnote content…").run();
    },
  },
  {
    title: "Stack",
    description: "Multi-column layout (2-4 columns)",
    icon: "▥",
    section: "Layout",
    command: (editor) => {
      editor.chain().focus().insertStack(2).run();
    },
  },
  {
    title: "New Chart",
    description: "Create a D3 chart with AI (Embed Studio)",
    icon: "📊",
    section: "Insert",
    command: (editor) => {
      const id = `d3-chart-${Date.now().toString(36)}`;
      const src = `${id}.html`;
      (editor.chain().focus() as any).insertHtmlEmbed().run();
      setTimeout(() => {
        const { doc } = editor.state;
        let targetPos = -1;
        doc.descendants((node, pos) => {
          if (node.type.name === "htmlEmbed" && !node.attrs.src) {
            targetPos = pos;
            return false;
          }
        });
        if (targetPos >= 0) {
          editor.view.dispatch(
            editor.state.tr.setNodeMarkup(targetPos, undefined, {
              ...editor.state.doc.nodeAt(targetPos)?.attrs,
              src,
              title: "New chart",
            }),
          );
        }
        window.dispatchEvent(
          new CustomEvent("open-embed-studio", { detail: { src } }),
        );
      }, 50);
    },
  },
];

/**
 * Component-driven slash items (Accordion, Note, HtmlEmbed, ...) come
 * from the shared component registry. We map each tag to its slash
 * section here so the menu stays categorized when new components are
 * added.
 */
const COMPONENT_SECTION_MAP: Record<string, SlashSection> = {
  Accordion: "Layout",
  Note: "Academic",
  Quote: "Text",
  Wide: "Layout",
  FullWidth: "Layout",
  Sidenote: "Academic",
  Reference: "Media",
  HtmlEmbed: "Media",
  HfUser: "Media",
  RawHtml: "Advanced",
  Mermaid: "Media",
};

function inferSection(title: string): SlashSection {
  // Component slash items don't carry the original tag; match by label.
  const byLabel: Record<string, SlashSection> = {
    Accordion: "Layout",
    "Note / Callout": "Academic",
    "Quote block": "Text",
    Wide: "Layout",
    "Full width": "Layout",
    Sidenote: "Academic",
    "Reference / Figure": "Media",
    "HTML Embed": "Media",
    "HF User card": "Media",
    "Raw HTML": "Advanced",
    "Mermaid diagram": "Media",
  };
  return byLabel[title] ?? "Layout";
}

function withInferredSection(
  items: Array<Omit<SlashItem, "section"> & { section?: SlashSection }>,
): SlashItem[] {
  return items.map((item) => ({
    ...item,
    section: item.section ?? inferSection(item.title),
  }));
}

const ITEMS: SlashItem[] = [
  ...BUILT_IN_ITEMS,
  ...withInferredSection(getComponentSlashItems()),
];

// Display order of section groups in the menu. "Insert" is a tiny
// featured section at the very top holding the two flagship heavy
// inserts (New Chart, Image) - this editor's signature features.
// Text blocks follow because they're 80% of real usage.
const SECTION_ORDER: SlashSection[] = [
  "Insert",
  "Text",
  "Media",
  "Academic",
  "Layout",
  "Advanced",
];

// Used to re-export so we can also feed the section map to anyone who
// wants it (e.g. the TopBar "more" menu could list categories).
export { SECTION_ORDER, COMPONENT_SECTION_MAP };

interface SlashGroup {
  section: SlashSection;
  items: SlashItem[];
}

function groupItems(items: SlashItem[]): SlashGroup[] {
  const map = new Map<SlashSection, SlashItem[]>();
  for (const item of items) {
    const list = map.get(item.section) ?? [];
    list.push(item);
    map.set(item.section, list);
  }
  return SECTION_ORDER.filter((s) => map.has(s)).map((section) => ({
    section,
    items: map.get(section)!,
  }));
}

interface SlashMenuListProps {
  items: SlashItem[];
  command: (item: SlashItem) => void;
}

interface SlashMenuListRef {
  onKeyDown: (props: SuggestionKeyDownProps) => boolean;
}

const SlashMenuList = forwardRef<SlashMenuListRef, SlashMenuListProps>(
  ({ items, command }, ref) => {
    const [selectedIndex, setSelectedIndex] = useState(0);

    // We render items in section groups. Keyboard navigation still
    // operates on the flat `items` array (not on section headers),
    // so ArrowDown moves between actual items regardless of their
    // section.
    const groups = useMemo(() => groupItems(items), [items]);

    useEffect(() => {
      setSelectedIndex(0);
    }, [items]);

    const selectItem = useCallback(
      (index: number) => {
        const item = items[index];
        if (item) command(item);
      },
      [items, command],
    );

    useImperativeHandle(ref, () => ({
      onKeyDown: ({ event }: SuggestionKeyDownProps) => {
        if (event.key === "ArrowUp") {
          setSelectedIndex((i) => (i + items.length - 1) % items.length);
          return true;
        }
        if (event.key === "ArrowDown") {
          setSelectedIndex((i) => (i + 1) % items.length);
          return true;
        }
        if (event.key === "Enter") {
          selectItem(selectedIndex);
          return true;
        }
        return false;
      },
    }));

    if (items.length === 0) return null;

    return (
      <div className="slash-menu">
        {groups.map((group) => (
          <div key={group.section} className="slash-menu-group">
            <div className="slash-menu-section" aria-hidden="true">
              {group.section}
            </div>
            {group.items.map((item) => {
              // Map back to the flat index so keyboard state and
              // click highlight stay in sync.
              const flatIndex = items.indexOf(item);
              return (
                <button
                  key={`${group.section}:${item.title}`}
                  className={`slash-menu-item ${flatIndex === selectedIndex ? "is-selected" : ""}`}
                  onClick={() => selectItem(flatIndex)}
                  onMouseEnter={() => setSelectedIndex(flatIndex)}
                >
                  <span className="slash-menu-item-icon">{item.icon}</span>
                  <span className="slash-menu-item-content">
                    <span className="slash-menu-item-title">{item.title}</span>
                    <span className="slash-menu-item-desc">{item.description}</span>
                  </span>
                </button>
              );
            })}
          </div>
        ))}
      </div>
    );
  },
);

SlashMenuList.displayName = "SlashMenuList";

export function slashMenuSuggestion() {
  return {
    items: ({ query }: { query: string }) => {
      const q = query.toLowerCase();
      if (!q) return ITEMS;
      // Match on title OR description so typing "image" surfaces
      // both "Image" and "HTML Embed", etc.
      return ITEMS.filter(
        (item) =>
          item.title.toLowerCase().includes(q) ||
          item.description.toLowerCase().includes(q),
      );
    },

    render: () => {
      let component: ReactRenderer<SlashMenuListRef> | null = null;
      let popup: TippyInstance[] | null = null;

      return {
        onStart: (props: SuggestionProps<SlashItem>) => {
          component = new ReactRenderer(SlashMenuList, {
            props,
            editor: props.editor,
          });

          if (!props.clientRect) return;

          popup = tippy("body", {
            getReferenceClientRect: props.clientRect as () => DOMRect,
            appendTo: () => document.body,
            content: component.element,
            showOnCreate: true,
            interactive: true,
            trigger: "manual",
            placement: "bottom-start",
          });
        },

        onUpdate(props: SuggestionProps<SlashItem>) {
          component?.updateProps(props);

          if (!props.clientRect || !popup?.[0]) return;
          popup[0].setProps({
            getReferenceClientRect: props.clientRect as () => DOMRect,
          });
        },

        onKeyDown(props: SuggestionKeyDownProps) {
          if (props.event.key === "Escape") {
            popup?.[0]?.hide();
            return true;
          }
          return component?.ref?.onKeyDown(props) ?? false;
        },

        onExit() {
          popup?.[0]?.destroy();
          component?.destroy();
        },
      };
    },
  };
}
