import { useEditor, EditorContent } from "@tiptap/react";
import { Editor as TiptapEditor } from "@tiptap/core";
import StarterKit from "@tiptap/starter-kit";
import Collaboration from "@tiptap/extension-collaboration";
import { CollaborationCursorV3 } from "./extensions/collaboration-cursor-v3";
import Placeholder from "@tiptap/extension-placeholder";
// Link is included in StarterKit v3, configured via StarterKit options
import Mathematics from "@tiptap/extension-mathematics";
import "katex/dist/katex.min.css";
import { CodeBlockShiki } from "./extensions/code-block-shiki";
import * as Y from "yjs";
import { UndoManager } from "yjs";
import { HocuspocusProvider } from "@hocuspocus/provider";
import { IndexeddbPersistence } from "y-indexeddb";
import { createRefocusSync } from "./refocus-sync";
import { useEffect, useMemo, useRef, useState, MutableRefObject } from "react";
import { BubbleToolbar } from "./BubbleToolbar";
import { BlockHandle } from "./BlockHandle";
import { Image } from "@tiptap/extension-image";
import { Table } from "@tiptap/extension-table";
import { TableRow } from "@tiptap/extension-table-row";
import { TableCell } from "@tiptap/extension-table-cell";
import { TableHeader } from "@tiptap/extension-table-header";
import { Comment } from "./extensions/comment";
import { CollaborationUndo } from "./extensions/collaboration-undo";
import { SlashCommands } from "./extensions/slash-commands";
import { ImageUpload } from "./extensions/image-upload";
import { Citation, citationLabelKey } from "./extensions/citation";
import { Bibliography } from "./extensions/bibliography";
import { Glossary } from "./extensions/glossary";
import { Footnote } from "./extensions/footnote";
import { Stack, StackColumn } from "./extensions/stack";
import { ScrollGuard } from "./extensions/scroll-guard";
import { AgentHighlight } from "./extensions/agent-highlight";
import { AgentFocus } from "./extensions/agent-focus";
import { AgentRewrite } from "./extensions/agent-rewrite";
import { CitationPanel } from "./CitationPanel";
import { createCommentStore, CommentStore } from "./comments";
import { createFrontmatterStore, FrontmatterStore } from "./frontmatter/frontmatter-store";
import { createEmbedStore, EmbedStore } from "./embeds/embed-store";
import { createEmbedDataStore, EmbedDataStore } from "./embeds/embed-data-store";
import { COMPONENTS, createComponentExtension } from "./components";
import { uploadImage } from "./upload";
import { loadDemoContent } from "./load-demo";

interface EditorProps {
  docName: string;
  user: { name: string; color: string; avatarUrl?: string };
  editorRef: MutableRefObject<TiptapEditor | null>;
  onCommentStoreReady: (store: CommentStore) => void;
  onFrontmatterStoreReady: (store: FrontmatterStore) => void;
  onEmbedStoreReady?: (store: EmbedStore) => void;
  onEmbedDataStoreReady?: (store: EmbedDataStore) => void;
  onSettingsMapReady?: (map: Y.Map<any>) => void;
  onEditorReady: (editor: TiptapEditor | null) => void;
  onUndoManagerReady?: (manager: UndoManager) => void;
  onProviderReady?: (provider: HocuspocusProvider) => void;
  onAddComment?: () => void;
}

export function Editor({
  docName,
  user,
  editorRef,
  onCommentStoreReady,
  onFrontmatterStoreReady,
  onEmbedStoreReady,
  onEmbedDataStoreReady,
  onProviderReady,
  onSettingsMapReady,
  onEditorReady,
  onUndoManagerReady,
  onAddComment,
}: EditorProps) {
  const ydocRef = useRef<Y.Doc | null>(null);
  if (!ydocRef.current) ydocRef.current = new Y.Doc();
  const ydoc = ydocRef.current;

  const undoManagerCallbackRef = useRef(onUndoManagerReady);
  undoManagerCallbackRef.current = onUndoManagerReady;

  // Local-first persistence layer. Mesh an IndexedDB provider onto the
  // same Y.Doc as the network provider: edits survive a refresh / closed
  // tab / offline window, and - critically - if the server ever loses or
  // resurrects a stale document, this client re-syncs its locally-cached
  // state back up. Yjs providers are meshable and dedupe automatically.
  const idbRef = useRef<IndexeddbPersistence | null>(null);
  if (!idbRef.current) {
    idbRef.current = new IndexeddbPersistence(`collab-editor:${docName}`, ydoc);
  }

  const providerRef = useRef<HocuspocusProvider | null>(null);
  if (!providerRef.current) {
    const wsUrl =
      window.location.protocol === "https:"
        ? `wss://${window.location.host}/collab`
        : `ws://${window.location.host}/collab`;

    const tokenMatch = document.cookie.match(/(?:^|;\s*)hf_access_token=([^;]*)/);
    const token = tokenMatch ? decodeURIComponent(tokenMatch[1]) : undefined;

    providerRef.current = new HocuspocusProvider({
      url: wsUrl,
      name: docName,
      document: ydoc,
      token: token || "",
    });
  }
  const provider = providerRef.current;

  const citationsMap = useMemo(() => ydoc.getMap("citations"), [ydoc]);
  const settingsMap = useMemo(() => ydoc.getMap("settings"), [ydoc]);
  const commentStore = useMemo(() => createCommentStore(ydoc), [ydoc]);
  const frontmatterStore = useMemo(() => createFrontmatterStore(ydoc), [ydoc]);
  const embedStore = useMemo(() => createEmbedStore(ydoc), [ydoc]);
  const embedDataStore = useMemo(() => createEmbedDataStore(ydoc), [ydoc]);
  const [showCitationPanel, setShowCitationPanel] = useState(false);

  useEffect(() => {
    onCommentStoreReady(commentStore);
  }, [commentStore, onCommentStoreReady]);

  useEffect(() => {
    onFrontmatterStoreReady(frontmatterStore);
  }, [frontmatterStore, onFrontmatterStoreReady]);

  useEffect(() => {
    onEmbedStoreReady?.(embedStore);
  }, [embedStore, onEmbedStoreReady]);

  useEffect(() => {
    onEmbedDataStoreReady?.(embedDataStore);
  }, [embedDataStore, onEmbedDataStoreReady]);

  useEffect(() => {
    onSettingsMapReady?.(settingsMap);
  }, [settingsMap, onSettingsMapReady]);

  useEffect(() => {
    onProviderReady?.(provider);
  }, [provider, onProviderReady]);

  useEffect(() => {
    return () => {
      providerRef.current?.destroy();
      providerRef.current = null;
      idbRef.current?.destroy();
      idbRef.current = null;
      ydocRef.current?.destroy();
      ydocRef.current = null;
    };
  }, []);

  const editor = useEditor(
    {
      editorProps: {
        handleDrop: (view, event, _slice, moved) => {
          if (moved || !event.dataTransfer?.files?.length) return false;
          const images = Array.from(event.dataTransfer.files).filter((f) =>
            f.type.startsWith("image/"),
          );
          if (!images.length) return false;
          event.preventDefault();
          const insertPos = view.posAtCoords({
            left: event.clientX,
            top: event.clientY,
          });
          images.forEach((file) => {
            uploadImage(file).then((url) => {
              const node = view.state.schema.nodes.image.create({ src: url });
              const tr = view.state.tr.insert(
                insertPos?.pos ?? view.state.doc.content.size,
                node,
              );
              view.dispatch(tr);
            });
          });
          return true;
        },
        handlePaste: (view, event) => {
          const items = Array.from(event.clipboardData?.items ?? []);
          const imageItems = items.filter((i) => i.type.startsWith("image/"));
          if (!imageItems.length) return false;
          event.preventDefault();
          imageItems.forEach((item) => {
            const file = item.getAsFile();
            if (!file) return;
            uploadImage(file).then((url) => {
              const node = view.state.schema.nodes.image.create({ src: url });
              const tr = view.state.tr.replaceSelectionWith(node);
              view.dispatch(tr);
            });
          });
          return true;
        },
      },
      extensions: [
        StarterKit.configure({
          codeBlock: false,
          undoRedo: false,
          link: {
            openOnClick: false,
            HTMLAttributes: { class: "editor-link" },
          },
        } as any),
        CodeBlockShiki,
        Placeholder.configure({
          placeholder: 'Type "/" for commands, or "/demo" to load sample content...',
        }),
        Collaboration.configure({
          document: ydoc,
        }),
        CollaborationCursorV3.configure({
          provider,
          user,
        }),
        Mathematics.configure({
          katexOptions: { throwOnError: false },
        }),
        Image.configure({
          allowBase64: true,
          HTMLAttributes: { class: "editor-image" },
        }),
        Table.configure({ resizable: false }),
        TableRow,
        TableCell,
        TableHeader,
        SlashCommands,
        ImageUpload,
        Citation,
        Bibliography,
        Glossary,
        Footnote,
        Stack,
        StackColumn,
        ...COMPONENTS.map(createComponentExtension),
        CollaborationUndo.configure({
          onUndoManagerReady: (um) => undoManagerCallbackRef.current?.(um),
        }),
        Comment,
        ScrollGuard,
        AgentHighlight,
        AgentFocus.configure({
          provider,
          user,
        }),
        AgentRewrite,
      ],
    },
    [ydoc, provider],
  );

  useEffect(() => {
    if (!editor) return;
    const handler = () => {
      loadDemoContent(editor, frontmatterStore, citationsMap, settingsMap, embedStore);
    };
    window.addEventListener("load-demo-content", handler);
    return () => window.removeEventListener("load-demo-content", handler);
  }, [editor, frontmatterStore, citationsMap, settingsMap, embedStore]);

  useEffect(() => {
    if (!editor) return;
    const handler = () => {
      // Wipe body
      editor.commands.setContent("<p></p>");

      // Wipe frontmatter: keep template sane, empty everything else.
      frontmatterStore.setAll({
        title: "",
        subtitle: "",
        description: "",
        authors: [],
        affiliations: [],
        published: "",
        template: "article",
        banner: "",
        doi: "",
        showPdf: true,
        tableOfContentsAutoCollapse: false,
        licence: "",
        pdfProOnly: false,
        seoThumbImage: "",
        links: [],
      });

      // Wipe citations and settings
      ydoc.transact(() => {
        citationsMap.forEach((_, key) => citationsMap.delete(key));
        settingsMap.forEach((_, key) => settingsMap.delete(key));
      });

      // Wipe all embed HTML (banner + charts)
      for (const key of embedStore.keys()) {
        embedStore.remove(key);
      }
    };
    window.addEventListener("reset-article", handler);
    return () => window.removeEventListener("reset-article", handler);
  }, [editor, frontmatterStore, citationsMap, settingsMap, embedStore, ydoc]);

  // Dev-only hook used by the demo recording script (backend/demo/showcase.ts).
  // Lets the script swap the banner HTML deterministically, simulating a
  // successful Embed Studio agent run without depending on the LLM backend.
  useEffect(() => {
    if (!import.meta.env.DEV) return;
    const handler = (event: Event) => {
      const detail = (event as CustomEvent).detail as
        | { html?: string; key?: string }
        | undefined;
      const html = detail?.html;
      if (!html || !embedStore) return;
      const key = detail?.key || "banner.html";
      embedStore.set(key, html);
      frontmatterStore.set("banner", key);
    };
    window.addEventListener("__demo-set-banner", handler);
    return () => window.removeEventListener("__demo-set-banner", handler);
  }, [embedStore, frontmatterStore]);

  useEffect(() => {
    if (!editor) return;
    (editor.commands as any).updateUser(user);
    editor.commands.updateAgentFocusUser(user);
  }, [editor, user]);

  useEffect(() => {
    if (!editor) return;
    if (!editor.storage.citation) {
      editor.storage.citation = { citationsMap: null, settingsMap: null };
    }
    editor.storage.citation!.citationsMap = citationsMap;
    editor.storage.citation!.settingsMap = settingsMap;

    // When Yjs citation data or style changes, signal the citation-label
    // plugin to recompute all labels in a single batched transaction.
    const signal = () => {
      if (editor.isDestroyed) return;
      editor.view.dispatch(editor.state.tr.setMeta(citationLabelKey, true));
    };
    citationsMap.observe(signal);
    settingsMap.observe(signal);
    return () => {
      citationsMap.unobserve(signal);
      settingsMap.unobserve(signal);
    };
  }, [editor, citationsMap, settingsMap]);

  useEffect(() => {
    if (!editor) return;
    if (!editor.storage.htmlEmbed) {
      editor.storage.htmlEmbed = {} as any;
    }
    (editor.storage.htmlEmbed as any).embedStore = embedStore;
  }, [editor, embedStore]);

  useEffect(() => {
    const handler = () => setShowCitationPanel(true);
    window.addEventListener("open-citation-panel", handler);
    return () => window.removeEventListener("open-citation-panel", handler);
  }, []);

  useEffect(() => {
    editorRef.current = editor;
    onEditorReady(editor);
  }, [editor, editorRef, onEditorReady]);

  // Re-sync on tab refocus and hold the editor read-only until the
  // round-trip completes, so a stale never-closed tab can't clobber a
  // collaborator's newer content. The logic lives in createRefocusSync
  // (unit-tested); here we only wire the DOM trigger.
  useEffect(() => {
    if (!editor) return;

    const { onVisibility, dispose } = createRefocusSync({ provider, editor });
    const handler = () => {
      if (document.visibilityState === "visible") onVisibility();
    };

    document.addEventListener("visibilitychange", handler);
    return () => {
      document.removeEventListener("visibilitychange", handler);
      dispose();
    };
  }, [editor, provider]);

  const [containerEl, setContainerEl] = useState<HTMLElement | null>(null);

  if (!editor) return null;

  return (
    <>
      <BubbleToolbar editor={editor} onAddComment={onAddComment} />
      <div ref={setContainerEl} style={{ position: "relative" }}>
        <BlockHandle editor={editor} containerEl={containerEl} />
        <EditorContent editor={editor} />
      </div>
      {showCitationPanel && (
        <CitationPanel
          editor={editor}
          citationsMap={citationsMap}
          onClose={() => setShowCitationPanel(false)}
        />
      )}
    </>
  );
}
