<script lang="ts">
  import { EditorState } from 'prosemirror-state';
  import { EditorView, type NodeView } from 'prosemirror-view';
  import { Node as PmNode } from 'prosemirror-model';
  import {
    SearchQuery,
    findNext as searchFindNext,
    findPrev as searchFindPrev,
    getMatchHighlights,
    getSearchState,
    search,
    setSearchState,
  } from 'prosemirror-search';
  import { tick } from 'svelte';
  import { keymap } from 'prosemirror-keymap';
  import { baseKeymap, selectAll } from 'prosemirror-commands';
  import { history, redo, undo } from 'prosemirror-history';
  import { codeHighlightPlugin } from './prosemirror/code-highlight';
  import { codeBlockNodeView } from './prosemirror/code-block-nodeview';
  import { mathNodeView } from './prosemirror/math';
  import { mermaidNodeView } from './prosemirror/mermaid-nodeview';
  import { tablePlugins } from './prosemirror/tables';
  import { editSave } from './ipc';
  import { markdownParser, markdownSerializer, schema } from './schema';

  interface Props {
    markdown: string;
    editable?: boolean;
    onSave?: () => void;
    onCancel?: () => void;
    onLinkNavigate?: (href: string) => void;
    onCheckboxToggle?: (md: string) => void;
    onDirtyChange?: (dirty: boolean) => void;
  }

  let {
    markdown,
    editable = false,
    onSave,
    onCancel,
    onLinkNavigate,
    onCheckboxToggle,
    onDirtyChange,
  }: Props = $props();
  let editorEl: HTMLElement | undefined = $state(undefined);
  let view: EditorView | undefined;
  let findOpen = $state(false);
  let findQuery = $state('');
  let findMatchCount = $state(0);
  let lastMarkdown = '';
  let dirty = false;
  let findInputEl: HTMLInputElement | undefined = $state(undefined);
  let findBarEl: HTMLFormElement | undefined = $state(undefined);

  let lastSafeModeLength = -1;
  const PARSE_WARN_MS = 120;
  const LARGE_MARKDOWN_CHAR_LIMIT = 350_000;
  const SAFE_MODE_PREVIEW_CHAR_LIMIT = 50_000;
  let pendingLocalSaveNormalized: string | null = null;

  function normalizeMarkdownForCompare(md: string): string {
    return md
      .replace(/\r\n/g, '\n')
      .replace(/\r/g, '\n')
      .replace(/\n$/, '');
  }

  function setDirty(next: boolean): void {
    if (dirty === next) return;
    dirty = next;
    if (onDirtyChange) {
      onDirtyChange(next);
    }
  }

  function emptyDoc(): PmNode {
    return schema.topNodeType.createAndFill()!;
  }

  function buildSafeModeDoc(md: string, reason: string): PmNode {
    const blocks: PmNode[] = [];
    blocks.push(
      schema.nodes.paragraph.create(
        null,
        schema.text(`Large document loaded in safe mode (${reason}) to prevent UI freezes.`),
      ),
    );

    const preview = md.slice(0, SAFE_MODE_PREVIEW_CHAR_LIMIT);
    if (preview) {
      blocks.push(schema.nodes.code_block.create({ params: 'plaintext' }, schema.text(preview)));
    }
    if (md.length > SAFE_MODE_PREVIEW_CHAR_LIMIT) {
      blocks.push(
        schema.nodes.paragraph.create(
          null,
          schema.text(`Preview truncated at ${SAFE_MODE_PREVIEW_CHAR_LIMIT.toLocaleString()} characters.`),
        ),
      );
    }
    return schema.topNodeType.create(null, blocks);
  }

  function parseMarkdownDoc(md: string, phase: 'initial' | 'update'): PmNode {
    if (md.length > LARGE_MARKDOWN_CHAR_LIMIT) {
      if (lastSafeModeLength !== md.length) {
        console.warn(
          `[attn] markdown too large for full ProseMirror parse: ${md.length} chars; `
          + `using safe mode`,
        );
        lastSafeModeLength = md.length;
      }
      return buildSafeModeDoc(md, 'size_limit');
    }

    if (lastSafeModeLength !== -1) {
      lastSafeModeLength = -1;
    }

    const start = performance.now();
    try {
      const doc = markdownParser.parse(md) ?? emptyDoc();
      const elapsed = performance.now() - start;
      if (elapsed > PARSE_WARN_MS) {
        console.warn(
          `[attn] slow markdown parse (${phase}): ${elapsed.toFixed(1)}ms `
          + `for ${md.length} chars`,
        );
      }
      return doc;
    } catch (error) {
      console.error('[attn] markdown parse failed; using safe mode', error);
      return buildSafeModeDoc(md, 'parse_error');
    }
  }

  function buildPlugins(md: string) {
    const plugins = [
      history(),
      search(),
    ];
    if (md.length <= LARGE_MARKDOWN_CHAR_LIMIT) {
      plugins.push(codeHighlightPlugin());
    }
    plugins.push(...tablePlugins());
    plugins.push(
      keymap({
        'Mod-z': undo,
        'Mod-y': redo,
        'Mod-Shift-z': redo,
        'Mod-f': () => {
          void openFindPanel();
          return true;
        },
        'Mod-g': () => {
          if (!findOpen) {
            void openFindPanel();
          } else {
            findNextMatch();
          }
          return true;
        },
        'Shift-Mod-g': () => {
          if (!findOpen) {
            void openFindPanel();
          } else {
            findPrevMatch();
          }
          return true;
        },
        'Mod-s': () => {
          if (view) {
            const current = markdownSerializer.serialize(view.state.doc);
            pendingLocalSaveNormalized = normalizeMarkdownForCompare(current);
          }
          if (onSave) onSave();
          return true;
        },
        'Escape': () => {
          if (onCancel) onCancel();
          return true;
        },
      }),
      keymap(baseKeymap),
    );
    return plugins;
  }

  // Custom NodeView for task_list_item — makes checkbox clickable
  function taskListItemNodeView(
    node: PmNode,
    editorView: EditorView,
    getPos: () => number | undefined,
  ): NodeView {
    const li = document.createElement('li');
    li.className = 'task-list-item';
    li.dataset.checked = node.attrs.checked ? 'true' : 'false';

    const checkboxWrap = document.createElement('span');
    checkboxWrap.className = 'task-checkbox';

    const checkbox = document.createElement('input');
    checkbox.type = 'checkbox';
    checkbox.checked = node.attrs.checked;
    // Checkbox is always clickable (both read-only and edit mode)
    checkbox.addEventListener('mousedown', (e) => {
      e.preventDefault();
      const pos = getPos();
      if (pos === undefined) return;
      const tr = editorView.state.tr.setNodeMarkup(pos, undefined, {
        ...node.attrs,
        checked: !node.attrs.checked,
      });
      editorView.dispatch(tr);
      // Serialize and send via IPC after checkbox toggle
      const md = markdownSerializer.serialize(editorView.state.doc);
      editSave(md);
      if (onCheckboxToggle) {
        onCheckboxToggle(md);
      }
    });

    checkboxWrap.appendChild(checkbox);
    li.appendChild(checkboxWrap);

    const contentDiv = document.createElement('div');
    contentDiv.className = 'task-content';
    li.appendChild(contentDiv);

    return {
      dom: li,
      contentDOM: contentDiv,
      update(updatedNode: PmNode) {
        if (updatedNode.type !== node.type) return false;
        node = updatedNode;
        checkbox.checked = node.attrs.checked;
        li.dataset.checked = node.attrs.checked ? 'true' : 'false';
        return true;
      },
    };
  }

  function createState(md: string): EditorState {
    const doc = parseMarkdownDoc(md, 'initial');
    return EditorState.create({
      doc,
      plugins: buildPlugins(md),
    });
  }

  function refreshMatchCount(): void {
    if (!view) {
      findMatchCount = 0;
      return;
    }
    findMatchCount = getMatchHighlights(view.state).find().length;
  }

  function updateSearchQuery(): void {
    if (!view) return;
    const query = new SearchQuery({ search: findQuery });
    view.dispatch(setSearchState(view.state.tr, query));
    refreshMatchCount();
  }

  function ensureSelectionVisible(): void {
    if (!view) return;

    const viewport = (
      view.dom.closest('[data-slot="scroll-area-viewport"]')
      ?? view.dom.closest('.attn-content-viewport')
    ) as HTMLElement | null;
    if (!viewport) return;

    const coords = view.coordsAtPos(view.state.selection.head, 1);
    const viewportRect = viewport.getBoundingClientRect();
    const topMargin = (findOpen ? (findBarEl?.offsetHeight ?? 0) + 16 : 24);
    const bottomMargin = 24;

    if (
      coords.top >= viewportRect.top + topMargin
      && coords.bottom <= viewportRect.bottom - bottomMargin
    ) {
      return;
    }

    const yInContent = coords.top - viewportRect.top + viewport.scrollTop;
    const centeredTop = Math.max(0, yInContent - viewport.clientHeight / 2);
    viewport.scrollTo({ top: centeredTop });
  }

  function findNextMatch(): void {
    if (!view || !findQuery.trim()) return;
    searchFindNext(view.state, view.dispatch, view);
    refreshMatchCount();
    requestAnimationFrame(ensureSelectionVisible);
  }

  function findPrevMatch(): void {
    if (!view || !findQuery.trim()) return;
    searchFindPrev(view.state, view.dispatch, view);
    refreshMatchCount();
    requestAnimationFrame(ensureSelectionVisible);
  }

  async function openFindPanel(): Promise<void> {
    if (!view) return;

    findOpen = true;

    const searchState = getSearchState(view.state);
    if (searchState) {
      findQuery = searchState.query.search;
    }

    if (!findQuery && !view.state.selection.empty) {
      const selected = view.state.doc.textBetween(
        view.state.selection.from,
        view.state.selection.to,
        ' ',
      ).trim();
      if (selected) {
        findQuery = selected;
      }
    }

    updateSearchQuery();
    await tick();
    findInputEl?.focus();
    findInputEl?.select();
  }

  function closeFindPanel(clearQuery = true): void {
    findOpen = false;
    if (!view) return;
    if (clearQuery) {
      findQuery = '';
      view.dispatch(setSearchState(view.state.tr, new SearchQuery({ search: '' })));
      findMatchCount = 0;
    }
  }

  function handleFindSubmit(e: SubmitEvent): void {
    e.preventDefault();
    findNextMatch();
  }

  function handleFindKeydown(e: KeyboardEvent): void {
    if (e.key === 'Escape') {
      e.preventDefault();
      closeFindPanel();
      return;
    }
    if (e.key === 'Enter') {
      e.preventDefault();
      if (e.shiftKey) {
        findPrevMatch();
      } else {
        findNextMatch();
      }
    }
  }

  function handleEditorClick(event: MouseEvent): boolean {
    const target = event.target as HTMLElement | null;
    const anchor = target?.closest('a[href]') as HTMLAnchorElement | null;
    if (!anchor) return false;

    // In edit mode, require Cmd/Ctrl+click so cursor placement still works.
    if (editable && !(event.metaKey || event.ctrlKey)) {
      return false;
    }

    const href = anchor.getAttribute('href')?.trim();
    if (!href) return false;
    event.preventDefault();
    if (onLinkNavigate) {
      onLinkNavigate(href);
      return true;
    }
    return false;
  }

  function handleEditorKeydown(editorView: EditorView, event: KeyboardEvent): boolean {
    const meta = event.metaKey || event.ctrlKey;
    if (!meta || event.altKey) return false;

    const key = event.key.toLowerCase();

    // Keep native clipboard operations unhandled by app-level logic.
    if (key === 'c' || key === 'v' || key === 'x') {
      return false;
    }

    // Ensure select-all is always available inside the editor surface.
    if (key === 'a' && !event.shiftKey) {
      return selectAll(editorView.state, editorView.dispatch, editorView);
    }

    return false;
  }

  export function getMarkdown(): string {
    if (!view) return markdown;
    return markdownSerializer.serialize(view.state.doc);
  }

  export function hasUnsavedChanges(): boolean {
    return dirty;
  }

  export function resetToMarkdown(nextMarkdown: string): void {
    if (!view) return;
    const updateStart = performance.now();
    const bookmark = view.state.selection.getBookmark();
    const updateDoc = parseMarkdownDoc(nextMarkdown, 'update');
    let state = EditorState.create({
      doc: updateDoc,
      plugins: buildPlugins(nextMarkdown),
    });
    try {
      const selection = bookmark.resolve(state.doc);
      state = state.apply(state.tr.setSelection(selection));
    } catch {
      // If previous selection can't be restored, keep default selection.
    }
    console.info(`[attn] pm update done in ${(performance.now() - updateStart).toFixed(1)}ms`);
    view.updateState(state);
    if (findOpen && findQuery) {
      updateSearchQuery();
    }
    lastMarkdown = nextMarkdown;
    setDirty(false);
  }

  export function commitSaved(): void {
    if (!view) return;
    lastMarkdown = markdownSerializer.serialize(view.state.doc);
    setDirty(false);
  }

  export function undoStep(): void {
    if (!view) return;
    undo(view.state, view.dispatch, view);
  }

  export function redoStep(): void {
    if (!view) return;
    redo(view.state, view.dispatch, view);
  }

  export function openFind(): void {
    void openFindPanel();
  }

  // Create EditorView on mount
  $effect(() => {
    if (!editorEl) return;
    const state = createState(markdown);
    view = new EditorView(editorEl, {
      state,
      editable: () => editable,
      handleDOMEvents: {
        click: (_view, event) => handleEditorClick(event as MouseEvent),
        keydown: (editorView, event) => handleEditorKeydown(editorView, event as KeyboardEvent),
      },
      dispatchTransaction(tr) {
        if (!view) return;
        const nextState = view.state.apply(tr);
        view.updateState(nextState);
        if (tr.docChanged && editable) {
          setDirty(true);
        }
      },
      nodeViews: {
        task_list_item: taskListItemNodeView,
        code_block(node, editorView, getPos) {
          const mermaid = mermaidNodeView(node, editorView, getPos);
          if (mermaid) return mermaid;
          const math = mathNodeView(node, editorView, getPos);
          if (math) return math;
          return codeBlockNodeView(node, editorView, getPos);
        },
      },
    });
    lastMarkdown = markdown;
    setDirty(false);

    return () => {
      view?.destroy();
      view = undefined;
    };
  });

  // React to markdown prop changes (from outside, e.g. file watcher updates)
  $effect(() => {
    if (!view) return;
    // Only update if the markdown actually changed from what we last set
    if (markdown === lastMarkdown) return;
    const normalizedIncoming = normalizeMarkdownForCompare(markdown);
    if (pendingLocalSaveNormalized && normalizedIncoming === pendingLocalSaveNormalized) {
      pendingLocalSaveNormalized = null;
      lastMarkdown = markdown;
      setDirty(false);
      return;
    }
    // Preserve undo history/cursor when incoming text is effectively the same
    // content we already have in the editor (common after save/watcher round-trip).
    const currentMarkdown = markdownSerializer.serialize(view.state.doc);
    if (normalizeMarkdownForCompare(currentMarkdown) === normalizedIncoming) {
      lastMarkdown = markdown;
      setDirty(false);
      return;
    }
    resetToMarkdown(markdown);
  });

  // React to editable changes
  $effect(() => {
    if (view) {
      view.setProps({ editable: () => editable });
    }
  });
</script>

<div class="editor-container">
  {#if findOpen}
    <form bind:this={findBarEl} class="pm-find-bar" onsubmit={handleFindSubmit}>
      <input
        bind:this={findInputEl}
        bind:value={findQuery}
        class="pm-find-input"
        type="text"
        placeholder="Find in document..."
        oninput={updateSearchQuery}
        onkeydown={handleFindKeydown}
      />
      <span class="pm-find-count">
        {#if findQuery}
          {#if findMatchCount === 0}
            No matches
          {:else}
            {findMatchCount} match{findMatchCount === 1 ? '' : 'es'}
          {/if}
        {/if}
      </span>
      <button type="button" class="pm-find-btn" aria-label="Previous match" onclick={findPrevMatch}>↑</button>
      <button type="submit" class="pm-find-btn" aria-label="Next match">↓</button>
      <button type="button" class="pm-find-btn" aria-label="Close find" onclick={() => closeFindPanel()}>✕</button>
    </form>
  {/if}
  <div bind:this={editorEl} class="prosemirror-mount"></div>
</div>
