/**
 * CodeMirror 6 editor wrapper component.
 *
 * Provides markdown editing with syntax highlighting and dark theme.
 * Exposes imperative methods via ref: getValue, setValue, focus.
 */

import { markdown } from "@codemirror/lang-markdown";
import { oneDark } from "@codemirror/theme-one-dark";
import { EditorView, basicSetup } from "codemirror";
import { useEffect, useImperativeHandle, useRef, type Ref } from "react";

export interface CodeMirrorEditorProps {
  /** Initial content to display */
  initialContent: string;
  /** Called when content changes */
  onChange: (content: string) => void;
  /** Called when scroll position changes (0-1 percentage) */
  onScroll?: (scrollPercent: number) => void;
  /** Additional CSS classes */
  className?: string;
  /** Ref for imperative methods (React 19 ref-as-prop) */
  ref?: Ref<CodeMirrorEditorRef>;
}

export interface CodeMirrorEditorRef {
  /** Get current editor content */
  getValue: () => string;
  /** Set editor content programmatically */
  setValue: (content: string) => void;
  /** Focus the editor */
  focus: () => void;
  /** Wrap selected text with prefix and suffix */
  wrapSelection: (prefix: string, suffix: string) => void;
  /** Insert text at cursor position */
  insertAtCursor: (text: string) => void;
  /** Scroll to percentage position (0-1). Returns true if scroll actually changed. */
  scrollToPercent: (percent: number) => boolean;
  /** Reveal a 1-based line number and select it. */
  revealLine: (lineNumber: number) => boolean;
  /** Current cursor position and screen coordinates. */
  getCursorInfo: () => {
    pos: number;
    x: number;
    y: number;
  } | null;
  /** Replace a text range and place the cursor at the end. */
  replaceRange: (from: number, to: number, text: string) => boolean;
}

export function CodeMirrorEditor({
  initialContent,
  onChange,
  onScroll,
  className,
  ref,
}: CodeMirrorEditorProps) {
  const containerRef = useRef<HTMLDivElement>(null);
  const viewRef = useRef<EditorView | null>(null);
  const onChangeRef = useRef(onChange);
  const onScrollRef = useRef(onScroll);

  // Keep callback refs current to avoid recreating editor
  useEffect(() => {
    onChangeRef.current = onChange;
  }, [onChange]);

  useEffect(() => {
    onScrollRef.current = onScroll;
  }, [onScroll]);

  // Initialize CodeMirror
  useEffect(() => {
    if (!containerRef.current) return;

    const view = new EditorView({
      doc: initialContent,
      extensions: [
        basicSetup,
        markdown(),
        oneDark,
        EditorView.lineWrapping,
        EditorView.updateListener.of((update) => {
          if (update.docChanged) {
            onChangeRef.current(update.state.doc.toString());
          }
        }),
        // Dark theme base styling
        EditorView.theme({
          "&": {
            height: "100%",
          },
          ".cm-scroller": {
            fontFamily: "ui-monospace, monospace",
            fontSize: "14px",
          },
        }),
      ],
      parent: containerRef.current,
    });

    // Attach scroll listener directly to scrollDOM for reliable event capture
    // (scroll events don't bubble, so domEventHandlers may miss them)
    const scroller = view.scrollDOM;
    const handleScroll = () => {
      const maxScroll = scroller.scrollHeight - scroller.clientHeight;
      if (maxScroll > 0 && onScrollRef.current) {
        const percent = Math.max(
          0,
          Math.min(1, scroller.scrollTop / maxScroll)
        );
        if (Number.isFinite(percent)) {
          onScrollRef.current(percent);
        }
      }
    };
    scroller.addEventListener("scroll", handleScroll, { passive: true });

    viewRef.current = view;
    return () => {
      scroller.removeEventListener("scroll", handleScroll);
      view.destroy();
    };
    // Only run on mount - initialContent should not trigger re-creation
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // Expose imperative methods
  useImperativeHandle(ref, () => ({
    getValue: () => {
      return viewRef.current?.state.doc.toString() ?? "";
    },
    setValue: (content: string) => {
      const view = viewRef.current;
      if (view) {
        view.dispatch({
          changes: { from: 0, to: view.state.doc.length, insert: content },
        });
      }
    },
    focus: () => {
      viewRef.current?.focus();
    },
    wrapSelection: (prefix: string, suffix: string) => {
      const view = viewRef.current;
      if (!view) return;

      const { from, to } = view.state.selection.main;
      const selectedText = view.state.doc.sliceString(from, to);

      view.dispatch({
        changes: {
          from,
          to,
          insert: `${prefix}${selectedText}${suffix}`,
        },
        selection: {
          anchor: from + prefix.length,
          head: from + prefix.length + selectedText.length,
        },
      });
      view.focus();
    },
    insertAtCursor: (text: string) => {
      const view = viewRef.current;
      if (!view) return;

      const pos = view.state.selection.main.head;
      view.dispatch({
        changes: { from: pos, to: pos, insert: text },
        selection: { anchor: pos + text.length },
      });
      view.focus();
    },
    scrollToPercent: (percent: number): boolean => {
      const view = viewRef.current;
      if (!view || !Number.isFinite(percent)) return false;

      const clamped = Math.max(0, Math.min(1, percent));
      const scroller = view.scrollDOM;
      const maxScroll = scroller.scrollHeight - scroller.clientHeight;
      if (maxScroll > 0) {
        const targetScroll = clamped * maxScroll;
        // Only scroll if position actually changes (avoids unnecessary events)
        if (Math.abs(scroller.scrollTop - targetScroll) > 0.5) {
          scroller.scrollTop = targetScroll;
          return true;
        }
      }
      return false;
    },
    revealLine: (lineNumber: number): boolean => {
      const view = viewRef.current;
      if (!view || !Number.isFinite(lineNumber)) return false;

      const clampedLine = Math.max(
        1,
        Math.min(Math.trunc(lineNumber), view.state.doc.lines)
      );
      const line = view.state.doc.line(clampedLine);
      view.dispatch({
        selection: { anchor: line.from, head: line.to },
        scrollIntoView: true,
      });
      view.focus();
      return true;
    },
    getCursorInfo: () => {
      const view = viewRef.current;
      if (!view) return null;

      const pos = view.state.selection.main.head;
      const coords = view.coordsAtPos(pos);
      if (!coords) return null;

      return {
        pos,
        x: coords.left,
        y: coords.bottom,
      };
    },
    replaceRange: (from: number, to: number, text: string): boolean => {
      const view = viewRef.current;
      if (!view) return false;

      view.dispatch({
        changes: { from, to, insert: text },
        selection: { anchor: from + text.length },
      });
      view.focus();
      return true;
    },
  }));

  return <div ref={containerRef} className={className} />;
}
