import {
  createLearnitiveMathField,
  insertMathLiveCommand,
  searchMathLiveSymbols,
  type MathLiveField,
} from '@learnitive/latex-mathlive';
import type {
  LatexMathInsertionStyle,
  LatexMathKind,
} from '@learnitive/latex-source';
import { Button } from '@learnitive/ui-components';
import { GripHorizontal, Sigma, X } from 'lucide-react';
import React from 'react';
import { createPortal } from 'react-dom';

const VIEWPORT_MARGIN = 16;
const DEFAULT_PANEL_HEIGHT = 330;

function panelWidthForViewport(): number {
  const viewportWidth = typeof window === 'undefined' ? 1024 : window.innerWidth;
  return Math.min(520, Math.max(320, viewportWidth - VIEWPORT_MARGIN * 2));
}

function clampPanelPosition(
  left: number,
  top: number,
  width: number,
  height: number,
): { left: number; top: number } {
  const viewportWidth = typeof window === 'undefined' ? 1024 : window.innerWidth;
  const viewportHeight = typeof window === 'undefined' ? 768 : window.innerHeight;
  return {
    left: Math.min(
      Math.max(VIEWPORT_MARGIN, left),
      Math.max(VIEWPORT_MARGIN, viewportWidth - width - VIEWPORT_MARGIN),
    ),
    top: Math.min(
      Math.max(VIEWPORT_MARGIN, top),
      Math.max(VIEWPORT_MARGIN, viewportHeight - height - VIEWPORT_MARGIN),
    ),
  };
}

function initialPanelPosition(anchor?: DOMRect): { left: number; top: number } {
  const viewportWidth = typeof window === 'undefined' ? 1024 : window.innerWidth;
  const panelWidth = panelWidthForViewport();
  const desiredLeft = anchor?.left ?? viewportWidth / 2 - panelWidth / 2;
  const desiredTop = (anchor?.bottom ?? 120) + 10;
  return clampPanelPosition(
    desiredLeft,
    desiredTop,
    panelWidth,
    DEFAULT_PANEL_HEIGHT,
  );
}

function isInteractiveDragTarget(target: EventTarget | null): boolean {
  return target instanceof Element && Boolean(
    target.closest(
      'button,input,select,textarea,math-field,a,[contenteditable="true"],[data-mathlive-no-drag]',
    ),
  );
}

export function MathLivePreview({
  value,
  onEdit,
}: {
  value: string;
  onEdit: () => void;
}) {
  const hostRef = React.useRef<HTMLButtonElement>(null);
  const [previewFailed, setPreviewFailed] = React.useState(false);

  React.useEffect(() => {
    const host = hostRef.current;
    if (!host) return;
    host.replaceChildren();
    setPreviewFailed(false);
    try {
      const field = createLearnitiveMathField({
        parent: host,
        value,
        readOnly: true,
        ariaLabel: t.app.latexSourceEditor.equationPreview,
        className: 'pointer-events-none min-w-12 bg-transparent text-base',
      });
      field.style.border = '0';
      field.style.padding = '0';
      field.style.background = 'transparent';
      return () => field.remove();
    } catch (reason) {
      console.error('MathLive preview unavailable', reason);
      host.textContent = `$${value}$`;
      setPreviewFailed(true);
      return () => host.replaceChildren();
    }
  }, [value]);

  return (
    <button
      ref={hostRef}
      type="button"
      className="inline-flex max-w-72 items-center overflow-hidden rounded-md border bg-muted/30 px-2 py-1 hover:bg-muted"
      title={
        previewFailed
          ? 'Equation preview unavailable; click to edit or use Source'
          : t.app.latexSourceEditor.editEquation
      }
      onClick={onEdit}
    />
  );
}

export function MathLiveEquationEditor({
  anchor,
  initialValue,
  kind,
  insertionStyle,
  allowInsertionStyleChange = false,
  title,
  onSave,
  onCancel,
}: {
  anchor?: DOMRect;
  initialValue: string;
  kind: LatexMathKind;
  insertionStyle?: LatexMathInsertionStyle;
  allowInsertionStyleChange?: boolean;
  title: string;
  onSave: (value: string, insertionStyle: LatexMathInsertionStyle) => void;
  onCancel: () => void;
}) {
  const panelRef = React.useRef<HTMLDivElement>(null);
  const fieldHostRef = React.useRef<HTMLDivElement>(null);
  const fieldRef = React.useRef<MathLiveField | undefined>(undefined);
  const onSaveRef = React.useRef(onSave);
  const onCancelRef = React.useRef(onCancel);
  const dragRef = React.useRef<{
    pointerId: number;
    offsetX: number;
    offsetY: number;
  } | undefined>(undefined);
  const [value, setValue] = React.useState(initialValue);
  const dirtyRef = React.useRef(false);
  const hydratedFieldValueRef = React.useRef(initialValue);
  const [mountError, setMountError] = React.useState<string>();
  const [symbolQuery, setSymbolQuery] = React.useState('');
  const [symbolsOpen, setSymbolsOpen] = React.useState(false);
  const [dragging, setDragging] = React.useState(false);
  const [position, setPosition] = React.useState(() => initialPanelPosition(anchor));
  const [selectedStyle, setSelectedStyle] = React.useState<LatexMathInsertionStyle>(
    insertionStyle ?? (kind === 'inline' ? 'inline' : 'equation'),
  );
  const selectedStyleRef = React.useRef(selectedStyle);
  selectedStyleRef.current = selectedStyle;
  const symbolResults = React.useMemo(
    () => searchMathLiveSymbols(symbolQuery, 36),
    [symbolQuery],
  );
  onSaveRef.current = onSave;
  onCancelRef.current = onCancel;

  const reclamp = React.useCallback(() => {
    const panel = panelRef.current;
    if (!panel) return;
    const rect = panel.getBoundingClientRect();
    setPosition((current) =>
      clampPanelPosition(current.left, current.top, rect.width, rect.height),
    );
  }, []);

  React.useEffect(() => {
    const host = fieldHostRef.current;
    if (!host) return;
    host.replaceChildren();
    setMountError(undefined);
    setValue(initialValue);
    dirtyRef.current = false;
    hydratedFieldValueRef.current = initialValue;
    let field: MathLiveField;
    try {
      field = createLearnitiveMathField({
        parent: host,
        value: initialValue,
        ariaLabel: title,
        className: 'w-full rounded-md border bg-background px-3 py-2 text-xl',
        onInput: (nextValue) => {
          dirtyRef.current = true;
          setValue(nextValue);
        },
        onEscape: () => onCancelRef.current(),
      });
    } catch (reason) {
      console.error('MathLive editor unavailable', reason);
      setMountError(reason instanceof Error ? reason.message : String(reason));
      return;
    }
    // MathLive can be registered asynchronously. Re-assert the canonical PM
    // value after the custom element exists so an existing derivative/fraction
    // can never fall back to the insertion default from an earlier session.
    field.value = initialValue;
    field.style.minHeight = selectedStyleRef.current === 'inline' ? '46px' : '64px';
    const currentValue = () =>
      field.value !== hydratedFieldValueRef.current ? field.value : initialValue;
    const commitShortcut = (event: KeyboardEvent) => {
      if (event.key !== 'Enter' || (!event.ctrlKey && !event.metaKey)) return;
      event.preventDefault();
      event.stopPropagation();
      onSaveRef.current(currentValue(), selectedStyleRef.current);
    };
    field.addEventListener('keydown', commitShortcut);
    fieldRef.current = field;
    queueMicrotask(() => {
      // Some MathLive/custom-element builds finish their own initialization in
      // a microtask. Hydrate once more after that step, then focus.
      if (!dirtyRef.current) field.value = initialValue;
      hydratedFieldValueRef.current = field.value;
      field.focus();
    });
    return () => {
      field.removeEventListener('keydown', commitShortcut);
      fieldRef.current = undefined;
      field.remove();
    };
  }, [initialValue, title]);

  React.useEffect(() => {
    const field = fieldRef.current;
    if (field) field.style.minHeight = selectedStyle === 'inline' ? '46px' : '64px';
  }, [selectedStyle]);

  React.useEffect(() => {
    const onResize = () => requestAnimationFrame(reclamp);
    window.addEventListener('resize', onResize);
    return () => window.removeEventListener('resize', onResize);
  }, [reclamp]);

  React.useLayoutEffect(() => {
    const firstFrame = requestAnimationFrame(reclamp);
    if (!symbolsOpen) return () => cancelAnimationFrame(firstFrame);

    // The symbols panel can increase the modal height after the first layout pass.
    // Re-clamp once more on the next frame so the expanded editor stays on-screen.
    const secondFrame = requestAnimationFrame(reclamp);
    return () => {
      cancelAnimationFrame(firstFrame);
      cancelAnimationFrame(secondFrame);
    };
  }, [reclamp, symbolsOpen]);

  const finishDrag = (event: React.PointerEvent<HTMLButtonElement>) => {
    if (dragRef.current?.pointerId !== event.pointerId) return;
    dragRef.current = undefined;
    setDragging(false);
    if (event.currentTarget.hasPointerCapture(event.pointerId)) {
      event.currentTarget.releasePointerCapture(event.pointerId);
    }
  };

  const handlePointerDown = (event: React.PointerEvent<HTMLButtonElement>) => {
    if (event.pointerType === 'mouse' && event.button !== 0) return;
    if (isInteractiveDragTarget(event.target)) return;
    const panel = panelRef.current;
    if (!panel) return;
    const rect = panel.getBoundingClientRect();
    dragRef.current = {
      pointerId: event.pointerId,
      offsetX: event.clientX - rect.left,
      offsetY: event.clientY - rect.top,
    };
    event.currentTarget.setPointerCapture(event.pointerId);
    setDragging(true);
    event.preventDefault();
  };

  const handlePointerMove = (event: React.PointerEvent<HTMLButtonElement>) => {
    const drag = dragRef.current;
    const panel = panelRef.current;
    if (!drag || drag.pointerId !== event.pointerId || !panel) return;
    const rect = panel.getBoundingClientRect();
    setPosition(
      clampPanelPosition(
        event.clientX - drag.offsetX,
        event.clientY - drag.offsetY,
        rect.width,
        rect.height,
      ),
    );
  };

  const displayModeLabel =
    selectedStyle === 'inline'
      ? t.app.latexSourceEditor.inlineEquation
      : selectedStyle === 'equation'
        ? 'Equation environment'
        : t.app.latexSourceEditor.displayEquation;

  const panel = (
    <div
      ref={panelRef}
      className="fixed z-[180] rounded-xl border bg-background p-3 shadow-2xl"
      data-testid="mathlive-equation-editor"
      style={{
        left: position.left,
        top: position.top,
        width: 'min(520px, calc(100vw - 32px))',
      }}
      role="dialog"
      aria-label={title}
    >
      <div className="mb-2 flex items-center gap-2">
        <button
          type="button"
          className={`flex min-w-0 flex-1 touch-none select-none items-center gap-2 rounded-md text-left ${dragging ? 'cursor-grabbing' : 'cursor-grab'}`}
          data-testid="mathlive-equation-drag-handle"
          aria-label="Drag equation editor"
          title="Drag equation editor"
          onPointerDown={handlePointerDown}
          onPointerMove={handlePointerMove}
          onPointerUp={finishDrag}
          onPointerCancel={finishDrag}
        >
          <GripHorizontal className="size-4 shrink-0 text-muted-foreground" aria-hidden="true" />
          <Sigma className="size-4 shrink-0" aria-hidden="true" />
          <strong className="min-w-0 flex-1 truncate text-sm">{title}</strong>
        </button>
        <Button
          size="icon"
          variant="ghost"
          data-mathlive-no-drag
          aria-label={t.app.latexSourceEditor.closeEquationEditor}
          onClick={onCancel}
        >
          <X className="size-4" />
        </Button>
      </div>

      {allowInsertionStyleChange && (
        <div
          className="mb-2 grid grid-cols-2 gap-1 rounded-md border bg-muted/20 p-1"
          role="group"
          aria-label="Equation insertion style"
          data-mathlive-no-drag
        >
          {([
            ['inline', 'Inline'],
            ['equation', 'Equation'],
          ] as const).map(([style, label]) => (
            <Button
              key={style}
              type="button"
              size="sm"
              variant={selectedStyle === style ? 'secondary' : 'ghost'}
              aria-pressed={selectedStyle === style}
              onClick={() => setSelectedStyle(style)}
            >
              {label}
            </Button>
          ))}
        </div>
      )}

      {mountError ? (
        <div className="space-y-2">
          <div className="rounded-md border border-destructive/40 bg-destructive/5 p-2 text-xs">
            MathLive is unavailable in this browser session. You can still edit the LaTeX equation text below.
          </div>
          <textarea
            className="min-h-20 w-full rounded-md border bg-background px-3 py-2 font-mono text-sm"
            aria-label="Equation LaTeX fallback editor"
            value={value}
            onChange={(event) => {
              dirtyRef.current = true;
              setValue(event.target.value);
            }}
          />
        </div>
      ) : null}
      <div ref={fieldHostRef} className={mountError ? 'hidden' : undefined} />

      <div
        className="mt-2"
        role="group"
        aria-label={t.app.latexSourceEditor.equationSymbols}
        data-mathlive-no-drag
      >
        <div className="flex items-center gap-2">
          <Button
            type="button"
            size="sm"
            variant={symbolsOpen ? 'secondary' : 'outline'}
            onClick={() => setSymbolsOpen((open) => !open)}
          >
            Symbols
          </Button>
          {symbolsOpen && (
            <input
              className="h-8 min-w-0 flex-1 rounded-md border bg-background px-2 text-sm"
              value={symbolQuery}
              onChange={(event) => setSymbolQuery(event.target.value)}
              placeholder="Search symbols…"
              aria-label="Search equation symbols"
            />
          )}
        </div>
        {symbolsOpen && (
          <div className="mt-2 grid max-h-36 grid-cols-6 gap-1 overflow-auto rounded-md border p-1">
            {symbolResults.map((symbol) => (
              <Button
                key={symbol.command}
                type="button"
                size="sm"
                variant="ghost"
                className="h-8 min-w-8 px-1 font-serif"
                title={`${symbol.command} — ${symbol.keywords}`}
                onClick={() => {
                  const field = fieldRef.current;
                  if (field) {
                    dirtyRef.current = true;
                    insertMathLiveCommand(field, symbol.command);
                    setValue(field.value);
                  }
                }}
              >
                {symbol.label}
              </Button>
            ))}
            {symbolResults.length === 0 && (
              <span className="col-span-6 px-2 py-1 text-muted-foreground text-xs">
                No matching symbols.
              </span>
            )}
          </div>
        )}
      </div>

      <div className="mt-3 flex items-center justify-between gap-2">
        <span className="text-muted-foreground text-xs">
          {displayModeLabel}
          {' · '}
          {t.app.latexSourceEditor.equationSaveShortcut}
        </span>
        <div className="flex gap-2" data-mathlive-no-drag>
          <Button type="button" variant="outline" onClick={onCancel}>
            {t.app.latexSourceEditor.cancelEquation}
          </Button>
          <Button
            type="button"
            onClick={() => {
              const field = fieldRef.current;
              const current = field
                ? field.value !== hydratedFieldValueRef.current
                  ? field.value
                  : initialValue
                : value;
              onSave(current, selectedStyle);
            }}
          >
            {t.app.latexSourceEditor.applyEquation}
          </Button>
        </div>
      </div>
    </div>
  );

  return typeof document === 'undefined' ? panel : createPortal(panel, document.body);
}
