"use client";

import * as React from "react";
import { getHtmlToMarkdown, shouldConvertHtmlToMarkdown } from "@/lib/html-to-markdown";
import { cn } from "@/lib/utils";

export type TextareaEditorHandle = {
  applyAction: (id: string) => void;
  getElement: () => HTMLTextAreaElement | null;
};

type Props = {
  value: string;
  onChange: (next: string) => void;
  showLineNumbers?: boolean;
  className?: string;
  /** Convert rich HTML on the clipboard to Markdown on paste. */
  smartPaste?: boolean;
};

/**
 * Plain textarea-based editor with markdown-aware actions.
 * Lightweight on purpose — no monaco / CodeMirror dependency.
 */
export const TextareaEditor = React.forwardRef<TextareaEditorHandle, Props>(
  function TextareaEditor({ value, onChange, showLineNumbers, className, smartPaste = true }, ref) {
    const textareaRef = React.useRef<HTMLTextAreaElement>(null);
    const gutterRef = React.useRef<HTMLDivElement>(null);

    React.useImperativeHandle(ref, () => ({
      applyAction: (id: string) => applyMarkdownAction(textareaRef.current, id, onChange),
      getElement: () => textareaRef.current,
    }));

    const lineCount = React.useMemo(() => value.split("\n").length, [value]);

    function handleScroll(e: React.UIEvent<HTMLTextAreaElement>) {
      if (gutterRef.current) {
        gutterRef.current.scrollTop = (e.target as HTMLTextAreaElement).scrollTop;
      }
    }

    async function handlePaste(e: React.ClipboardEvent<HTMLTextAreaElement>) {
      if (!smartPaste) return;
      const html = e.clipboardData.getData("text/html");
      if (!html || !shouldConvertHtmlToMarkdown(html)) return;
      e.preventDefault();
      try {
        const toMd = await getHtmlToMarkdown();
        const markdown = toMd(html).trim();
        const el = textareaRef.current;
        if (!el) return;
        const { selectionStart, selectionEnd, value: text } = el;
        const scrollSnapshot = { top: el.scrollTop, left: el.scrollLeft };
        const next = text.slice(0, selectionStart) + markdown + text.slice(selectionEnd);
        onChange(next);
        afterControlledTextareaChange(el, scrollSnapshot, () => {
          const cursor = selectionStart + markdown.length;
          el.setSelectionRange(cursor, cursor);
        });
      } catch {
        // Fall back to the default paste behavior on conversion failure.
      }
    }

    function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
      const meta = e.ctrlKey || e.metaKey;

      if (meta && e.key.toLowerCase() === "b") {
        e.preventDefault();
        applyMarkdownAction(textareaRef.current, "bold", onChange);
      } else if (meta && e.key.toLowerCase() === "i") {
        e.preventDefault();
        applyMarkdownAction(textareaRef.current, "italic", onChange);
      } else if (meta && e.key.toLowerCase() === "k") {
        e.preventDefault();
        applyMarkdownAction(textareaRef.current, "link", onChange);
      } else if (meta && e.key === "/") {
        e.preventDefault();
        applyMarkdownAction(textareaRef.current, "code", onChange);
      } else if (e.key === "Tab") {
        // Indent / outdent by two spaces, supporting selection ranges.
        e.preventDefault();
        const el = textareaRef.current;
        if (!el) return;
        const { selectionStart, selectionEnd, value: text } = el;
        if (e.shiftKey) {
          const lineStart = text.lastIndexOf("\n", selectionStart - 1) + 1;
          if (text.startsWith("  ", lineStart)) {
            const updated = text.slice(0, lineStart) + text.slice(lineStart + 2);
            const scrollSnapshot = { top: el.scrollTop, left: el.scrollLeft };
            onChange(updated);
            afterControlledTextareaChange(el, scrollSnapshot, () => {
              el.setSelectionRange(Math.max(0, selectionStart - 2), Math.max(0, selectionEnd - 2));
            });
          }
        } else {
          const updated = text.slice(0, selectionStart) + "  " + text.slice(selectionEnd);
          const scrollSnapshot = { top: el.scrollTop, left: el.scrollLeft };
          onChange(updated);
          afterControlledTextareaChange(el, scrollSnapshot, () => {
            el.setSelectionRange(selectionStart + 2, selectionStart + 2);
          });
        }
      }
    }

    return (
      <div
        className={cn(
          "relative flex h-full min-h-0 overflow-hidden bg-background",
          className,
        )}
      >
        {showLineNumbers ? (
          <div
            ref={gutterRef}
            aria-hidden
            className="scrollbar-thin pointer-events-none w-12 shrink-0 overflow-hidden border-r border-border/60 bg-muted/30 py-4 font-mono text-xs leading-6 text-muted-foreground"
          >
            <div className="px-2 text-right">
              {Array.from({ length: lineCount }, (_, i) => (
                <div key={i}>{i + 1}</div>
              ))}
            </div>
          </div>
        ) : null}
        <textarea
          ref={textareaRef}
          wrap="off"
          value={value}
          onChange={(e) => onChange(e.target.value)}
          onScroll={handleScroll}
          onKeyDown={handleKeyDown}
          onPaste={handlePaste}
          spellCheck
          aria-label="Markdown source"
          placeholder="Start writing markdown…"
          className={cn(
            "scrollbar-thin min-h-0 min-w-0 flex-1 resize-none overflow-auto bg-transparent p-4 font-mono text-sm leading-6 outline-none placeholder:text-muted-foreground",
            "selection:bg-primary/20",
          )}
        />
      </div>
    );
  },
);

let measureCanvas: HTMLCanvasElement | null = null;

function measureTextWidth(font: string, text: string): number {
  if (typeof document === "undefined") return text.length * 8;
  measureCanvas ??= document.createElement("canvas");
  const ctx = measureCanvas.getContext("2d");
  if (!ctx) return text.length * 8;
  ctx.font = font;
  return ctx.measureText(text).width;
}

/** Keeps the caret in view after nowrap / multi-line edits (toolbar blur + controlled value updates reset scroll). */
function ensureCaretVisibleInTextarea(el: HTMLTextAreaElement) {
  const cs = getComputedStyle(el);
  const lh = cs.lineHeight;
  const fontSize = parseFloat(cs.fontSize) || 14;
  const lineHeight =
    lh === "normal" ? fontSize * 1.5 : parseFloat(lh) || fontSize * 1.5;
  const padTop = parseFloat(cs.paddingTop) || 0;
  const padBottom = parseFloat(cs.paddingBottom) || 0;
  const padLeft = parseFloat(cs.paddingLeft) || 0;
  const padRight = parseFloat(cs.paddingRight) || 0;
  const font = `${cs.fontStyle} ${cs.fontWeight} ${cs.fontSize} ${cs.fontFamily}`;

  const pos = el.selectionStart;
  const before = el.value.slice(0, pos);
  const row = before.split("\n").length - 1;
  const lineStart = before.lastIndexOf("\n") + 1;
  const linePrefix = el.value.slice(lineStart, pos);

  const caretYTop = row * lineHeight + padTop;
  const caretYBottom = caretYTop + lineHeight;
  const viewTop = el.scrollTop;
  const viewBottom = el.scrollTop + el.clientHeight;

  if (caretYTop < viewTop + padTop * 0.5) {
    el.scrollTop = Math.max(0, caretYTop - padTop);
  } else if (caretYBottom > viewBottom - padBottom - 4) {
    el.scrollTop = caretYBottom - el.clientHeight + padBottom + 4;
  }

  const caretX = padLeft + measureTextWidth(font, linePrefix);
  const innerRight = el.scrollLeft + el.clientWidth - padRight;
  const margin = 16;

  if (caretX < el.scrollLeft + padLeft + margin) {
    el.scrollLeft = Math.max(0, caretX - padLeft - margin);
  } else if (caretX > innerRight - margin) {
    el.scrollLeft = Math.max(0, caretX - el.clientWidth + padLeft + padRight + margin);
  }
}

/**
 * Controlled `<textarea>` updates reset scroll position; toolbar clicks also
 * blur the field before React commits. Restore scroll after paint, then nudge
 * the caret into view if the insertion moved it outside the viewport.
 */
export function afterControlledTextareaChange(
  el: HTMLTextAreaElement,
  savedScroll: { top: number; left: number },
  applySelection: () => void,
) {
  requestAnimationFrame(() => {
    requestAnimationFrame(() => {
      el.focus();
      applySelection();
      el.scrollTop = savedScroll.top;
      el.scrollLeft = savedScroll.left;
      ensureCaretVisibleInTextarea(el);
    });
  });
}

/**
 * Apply a markdown formatting action to the textarea's current selection.
 * Imperative on purpose so the textarea remains the source of truth.
 */
export function applyMarkdownAction(
  el: HTMLTextAreaElement | null,
  action: string,
  onChange: (next: string) => void,
) {
  if (!el) return;
  const { selectionStart, selectionEnd, value } = el;
  const selected = value.slice(selectionStart, selectionEnd);
  const scrollSnapshot = { top: el.scrollTop, left: el.scrollLeft };

  const wrap = (left: string, right: string = left, placeholder = "text") => {
    const inner = selected || placeholder;
    const next = `${value.slice(0, selectionStart)}${left}${inner}${right}${value.slice(selectionEnd)}`;
    const cursor = selectionStart + left.length + inner.length + right.length;
    onChange(next);
    afterControlledTextareaChange(el, scrollSnapshot, () => {
      el.setSelectionRange(selectionStart + left.length, cursor - right.length);
    });
  };

  const prefixLines = (prefix: string) => {
    const before = value.slice(0, selectionStart);
    const lineStart = before.lastIndexOf("\n") + 1;
    const segment = value.slice(lineStart, selectionEnd);
    const replaced = segment
      .split("\n")
      .map((line) => `${prefix}${line}`)
      .join("\n");
    const next = value.slice(0, lineStart) + replaced + value.slice(selectionEnd);
    onChange(next);
    afterControlledTextareaChange(el, scrollSnapshot, () => {
      el.setSelectionRange(
        selectionStart + prefix.length,
        selectionEnd + prefix.length * segment.split("\n").length,
      );
    });
  };

  switch (action) {
    case "bold":
      return wrap("**", "**", "bold");
    case "italic":
      return wrap("_", "_", "italic");
    case "strike":
      return wrap("~~", "~~", "strikethrough");
    case "code":
      return wrap("`", "`", "code");
    case "h1":
      return prefixLines("# ");
    case "h2":
      return prefixLines("## ");
    case "quote":
      return prefixLines("> ");
    case "ul":
      return prefixLines("- ");
    case "ol":
      return prefixLines("1. ");
    case "task":
      return prefixLines("- [ ] ");
    case "link": {
      const label = selected || "link";
      const insertion = `[${label}](https://)`;
      const next = `${value.slice(0, selectionStart)}${insertion}${value.slice(selectionEnd)}`;
      onChange(next);
      afterControlledTextareaChange(el, scrollSnapshot, () => {
        const urlStart = selectionStart + label.length + 3;
        el.setSelectionRange(urlStart, urlStart + 8);
      });
      return;
    }
    case "image": {
      const alt = selected || "alt text";
      const insertion = `![${alt}](https://)`;
      const next = `${value.slice(0, selectionStart)}${insertion}${value.slice(selectionEnd)}`;
      onChange(next);
      afterControlledTextareaChange(el, scrollSnapshot, () => {
        const urlStart = selectionStart + alt.length + 4;
        el.setSelectionRange(urlStart, urlStart + 8);
      });
      return;
    }
    case "table": {
      const tbl = `\n\n| Header 1 | Header 2 | Header 3 |\n| --- | --- | --- |\n| Cell | Cell | Cell |\n| Cell | Cell | Cell |\n\n`;
      const next = value.slice(0, selectionStart) + tbl + value.slice(selectionEnd);
      onChange(next);
      afterControlledTextareaChange(el, scrollSnapshot, () => {
        const cursor = selectionStart + tbl.length;
        el.setSelectionRange(cursor, cursor);
      });
      return;
    }
    case "math": {
      // Block-level math by default; if there's a selection on a single line,
      // wrap inline.
      const inline = selected && !selected.includes("\n");
      const insertion = inline ? `$${selected}$` : `\n$$\n${selected || "x^2 + y^2 = z^2"}\n$$\n`;
      const next = value.slice(0, selectionStart) + insertion + value.slice(selectionEnd);
      onChange(next);
      afterControlledTextareaChange(el, scrollSnapshot, () => {
        const cursor = selectionStart + insertion.length;
        el.setSelectionRange(cursor, cursor);
      });
      return;
    }
    case "diagram": {
      const insertion = `\n\n\`\`\`mermaid\nflowchart LR\n  A[Start] --> B{Decision}\n  B -->|Yes| C[Done]\n  B -->|No| D[Try again]\n\`\`\`\n\n`;
      const next = value.slice(0, selectionStart) + insertion + value.slice(selectionEnd);
      onChange(next);
      afterControlledTextareaChange(el, scrollSnapshot, () => {
        const cursor = selectionStart + insertion.length;
        el.setSelectionRange(cursor, cursor);
      });
      return;
    }
    case "emoji": {
      const insertion = ":smile:";
      const next = value.slice(0, selectionStart) + insertion + value.slice(selectionEnd);
      onChange(next);
      afterControlledTextareaChange(el, scrollSnapshot, () => {
        // Highlight the placeholder name so the user can immediately type a different shortcode.
        el.setSelectionRange(selectionStart + 1, selectionStart + insertion.length - 1);
      });
      return;
    }
    default:
      return;
  }
}
