'use client';

import { useEditor, EditorContent, type Editor } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import Placeholder from '@tiptap/extension-placeholder';
import Mention from '@tiptap/extension-mention';
import { Markdown } from '@tiptap/markdown';
import type { AnyExtension } from '@tiptap/core';
import { useEffect, useRef, useMemo } from 'react';
import {
  Bold, Italic, Strikethrough, Heading1, Heading2, Heading3,
  List, ListOrdered, Quote, Minus, Code, type LucideIcon,
} from 'lucide-react';
import { createMentionSuggestion } from './createMentionSuggestion';
import { mentionPresets } from './mentionPresets';
import type { MentionAttrs, MentionConfig } from './types';
import './styles.css';

// ── Helpers ──

interface MarkdownManager {
  serialize: (json: Record<string, unknown>) => string;
}

function getMarkdown(editor: Editor): string {
  const storage = editor.storage.markdown as { manager?: MarkdownManager } | undefined;
  if (!storage?.manager) return editor.getText();
  return storage.manager.serialize(editor.getJSON());
}

function extractMentionIds(editor: Editor): string[] {
  const ids: string[] = [];
  editor.state.doc.descendants((node) => {
    if (node.type.name === 'mention' && node.attrs.id) {
      ids.push(node.attrs.id as string);
    }
  });
  return [...new Set(ids)];
}

// ── Types ──

export interface MarkdownEditorProps {
  value: string;
  onChange: (value: string) => void;
  placeholder?: string;
  minHeight?: number;
  className?: string;
  disabled?: boolean;
  showToolbar?: boolean;
  /**
   * `@`-mention autocomplete config.
   *
   * IMPORTANT: Tiptap's `useEditor` initialises the editor exactly once.
   * The `Mention` extension is only registered when `mentions` is truthy
   * on the FIRST render — handing in a real config later (e.g. after an
   * async items fetch) silently does nothing, and typing `@` will not
   * open the popover.
   *
   * If you want mentions even with async-loaded items, pass
   * `{ items: [] }` from the very first render and update the array
   * when data arrives. Either keep the `MentionConfig` object identity
   * stable across renders and mutate `items` in place (the suggestion
   * plugin captures the config by closure and reads `items` on each
   * query), or accept that swapping the whole object reference is a
   * no-op for the live editor.
   */
  mentions?: MentionConfig;
  /** Called when mentioned IDs change */
  onMentionIdsChange?: (ids: string[]) => void;
}

// ── Component ──

export function MarkdownEditor({
  value,
  onChange,
  placeholder = 'Write markdown...',
  minHeight = 120,
  className = '',
  disabled = false,
  showToolbar = true,
  mentions,
  onMentionIdsChange,
}: MarkdownEditorProps) {
  const isExternalUpdate = useRef(false);

  // ── Dev-mode trap detector ──
  // Tiptap initialises the editor once with the extensions array from
  // first render. If `mentions` is undefined on mount and becomes
  // truthy later, the Mention extension is never installed and the
  // user-visible @-trigger silently does nothing. Catch this early so
  // future consumers don't spend hours debugging why @ does nothing.
  const initialMentionsDefinedRef = useRef<boolean>(mentions !== undefined);
  const warnedRef = useRef(false);
  if (
    process.env.NODE_ENV !== 'production' &&
    !initialMentionsDefinedRef.current &&
    mentions !== undefined &&
    !warnedRef.current
  ) {
    warnedRef.current = true;
    // eslint-disable-next-line no-console
    console.warn(
      '[MarkdownEditor] `mentions` flipped from undefined to a config ' +
        'after mount. Tiptap only installs the Mention extension on first ' +
        'render — the @-popover will NOT work for this editor instance. ' +
        'Pass `{ items: [] }` from the very first render and mutate `.items` ' +
        'in place instead.',
    );
  }

  const extensions = useMemo(() => {
    const exts: AnyExtension[] = [
      StarterKit.configure({ heading: { levels: [1, 2, 3] } }),
      Placeholder.configure({ placeholder }),
      Markdown,
    ];

    if (mentions) {
      // ── Why .extend() with renderMarkdown ──
      //
      // Tiptap's `Mention` extension ships a default markdown serializer
      // (via `createInlineMarkdownSpec`, see @tiptap/extension-mention)
      // that emits a shortcode like `[@ id="..." label="..."]`. That's
      // round-trippable but useless for most consumers: a chat composer
      // feeding an LLM wants `@<label>`, a deep-linking app wants a
      // markdown link, etc.
      //
      // `renderText` only affects `editor.getText()` and the rendered
      // node — it does NOT influence `@tiptap/markdown`'s serializer.
      // The serializer reads `renderMarkdown` off the extension config
      // (see MarkdownManager.registerExtension → getExtensionField).
      // Overriding it via `.extend({ renderMarkdown })` replaces the
      // shortcode output with whatever the consumer supplied (or the
      // `plainAt` default — see ./mentionPresets.ts).
      //
      // Renderer-choice capture: useEditor only initialises once, so the
      // chosen renderer is captured by closure on first render. This is
      // intentional — swapping renderers per render would mean tearing
      // the editor down anyway, which we don't do for any other prop.
      //
      // Round-trip note: we intentionally do NOT also override
      // `parseMarkdown` / `markdownTokenizer`. Once a mention is
      // serialized, parsing it back would need the original mention
      // items list to look up the id, which we don't have at parse time.
      // Mentions are write-only by design here — `setContent(value)`
      // after submit gets back a plain string, which is fine for the
      // chat use case.
      const renderMarkdown = mentions.renderMarkdown ?? mentionPresets.plainAt;
      exts.push(
        Mention.extend({
          renderMarkdown(node) {
            const raw = node.attrs as { label?: string | null; id?: string | null };
            const attrs: MentionAttrs = {
              id: raw?.id ?? '',
              label: raw?.label ?? '',
            };
            // Defensive: if both are empty (shouldn't happen for a real
            // mention node, but `setContent` of malformed data could),
            // emit nothing rather than a stray "@" or invalid link.
            if (!attrs.id && !attrs.label) return '';
            return renderMarkdown(attrs);
          },
        }).configure({
          HTMLAttributes: { class: 'markdown-mention' },
          suggestion: createMentionSuggestion(mentions),
          renderText: ({ node }) => `@${node.attrs.label}`,
        }),
      );
    }

    return exts;
  }, [placeholder, mentions]);

  const editor = useEditor({
    immediatelyRender: false,
    editable: !disabled,
    extensions,
    content: value,
    onUpdate: ({ editor }) => {
      if (isExternalUpdate.current) return;
      onChange(getMarkdown(editor));

      if (onMentionIdsChange) {
        onMentionIdsChange(extractMentionIds(editor));
      }
    },
    editorProps: {
      attributes: {
        class: 'markdown-editor-content focus:outline-none text-sm',
        style: `min-height: ${minHeight}px`,
      },
    },
  });

  useEffect(() => {
    if (!editor) return;
    const current = getMarkdown(editor);
    if (current !== value) {
      isExternalUpdate.current = true;
      editor.commands.setContent(value);
      isExternalUpdate.current = false;
    }
  }, [value, editor]);

  const wrapperClass = `markdown-editor rounded-md border border-input bg-background ${disabled ? 'opacity-60' : ''} ${className}`.trim();

  return (
    <div className={wrapperClass}>
      {showToolbar && editor && <MarkdownToolbar editor={editor} />}
      <div className="px-3 py-2">
        <EditorContent editor={editor} />
      </div>
    </div>
  );
}

// ── Toolbar ──

interface ToolbarItem {
  icon: LucideIcon;
  action: () => void;
  active: boolean;
  title: string;
}

function MarkdownToolbar({ editor }: { editor: Editor }) {
  const items = useMemo<(ToolbarItem | null)[]>(() => [
    { icon: Bold, title: 'Bold', action: () => editor.chain().focus().toggleBold().run(), active: editor.isActive('bold') },
    { icon: Italic, title: 'Italic', action: () => editor.chain().focus().toggleItalic().run(), active: editor.isActive('italic') },
    { icon: Strikethrough, title: 'Strike', action: () => editor.chain().focus().toggleStrike().run(), active: editor.isActive('strike') },
    { icon: Code, title: 'Code', action: () => editor.chain().focus().toggleCode().run(), active: editor.isActive('code') },
    null,
    { icon: Heading1, title: 'H1', action: () => editor.chain().focus().toggleHeading({ level: 1 }).run(), active: editor.isActive('heading', { level: 1 }) },
    { icon: Heading2, title: 'H2', action: () => editor.chain().focus().toggleHeading({ level: 2 }).run(), active: editor.isActive('heading', { level: 2 }) },
    { icon: Heading3, title: 'H3', action: () => editor.chain().focus().toggleHeading({ level: 3 }).run(), active: editor.isActive('heading', { level: 3 }) },
    null,
    { icon: List, title: 'Bullet list', action: () => editor.chain().focus().toggleBulletList().run(), active: editor.isActive('bulletList') },
    { icon: ListOrdered, title: 'Ordered list', action: () => editor.chain().focus().toggleOrderedList().run(), active: editor.isActive('orderedList') },
    { icon: Quote, title: 'Quote', action: () => editor.chain().focus().toggleBlockquote().run(), active: editor.isActive('blockquote') },
    { icon: Minus, title: 'Divider', action: () => editor.chain().focus().setHorizontalRule().run(), active: false },
  ], [editor]);

  return (
    <div className="flex items-center gap-0.5 px-2 py-1.5 border-b border-border">
      {items.map((item, i) => {
        if (!item) return <div key={i} className="w-px h-4 bg-border mx-1" />;
        const Icon = item.icon;
        const btnClass = `markdown-toolbar-btn ${item.active ? 'active' : ''}`;
        return (
          <button key={i} type="button" onClick={item.action} title={item.title} className={btnClass}>
            <Icon style={{ width: 14, height: 14 }} />
          </button>
        );
      })}
    </div>
  );
}
