import { useRef, useEffect } from "react";
import { Terminal } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";
import { WebLinksAddon } from "@xterm/addon-web-links";
import type { UnlistenFn } from "@tauri-apps/api/event";
import {
  readText as clipboardReadText,
  writeText as clipboardWriteText,
} from "@tauri-apps/plugin-clipboard-manager";
import { openUrl } from "@tauri-apps/plugin-opener";
import {
  spawnPane,
  writeToPane,
  resizePane,
  killPane,
  onPaneData,
  onPaneExit,
  type PaneId,
  type SpawnSpec,
} from "../ipc";

// ---------------------------------------------------------------------------
// base64 helpers (private to this module)
// ---------------------------------------------------------------------------

function b64ToBytes(b64: string): Uint8Array {
  const bin = atob(b64);
  const out = new Uint8Array(bin.length);
  for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
  return out;
}

function bytesToB64(bytes: Uint8Array): string {
  let s = "";
  for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
  return btoa(s);
}

function stringToB64(s: string): string {
  // xterm.js's onData emits a JS string; UTF-8 encode before base64.
  return bytesToB64(new TextEncoder().encode(s));
}

// ---------------------------------------------------------------------------
// Props
// ---------------------------------------------------------------------------

interface XtermPaneProps {
  /** Spec describing what to spawn into this pane's PTY. Read once at mount;
   *  changing it later does NOT respawn — callers force a respawn by
   *  changing the React `key` (see Pane.svelte / LeafPane). */
  spec: SpawnSpec;
  onStatus?: (msg: string, ok: boolean) => void;
  /** Fired once when the backend PTY is alive and we have its PaneId. */
  onSpawn?: (paneId: PaneId) => void;
  /** Fired AFTER each writeToPane on user keypress. Used by broadcasting. */
  onInput?: (dataB64: string) => void;
  /** Fired whenever output arrives from the PTY. Used for idle detection. */
  onDataReceived?: () => void;
  /** Fired when xterm's textarea gains focus (i.e., user clicked here). */
  onFocus?: () => void;
  /** Increment to refocus the terminal programmatically (palette etc.). */
  focusTrigger?: number;
  /** Absolute font size in px. Changes are applied live (fit + PTY resize). */
  fontSize?: number;
}

const DEFAULT_XTERM_FONT_SIZE = 13;

// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------

export default function XtermPane({
  spec,
  onStatus,
  onSpawn,
  onInput,
  onDataReceived,
  onFocus,
  focusTrigger = 0,
  fontSize,
}: XtermPaneProps) {
  const containerRef = useRef<HTMLDivElement>(null);
  const termRef = useRef<Terminal | null>(null);
  const fitRef = useRef<FitAddon | null>(null);
  const paneIdRef = useRef<PaneId | null>(null);
  // Stash the most recent `fontSize` prop so the mount effect can pick
  // up the initial value without re-running when it changes (the secondary
  // effect below handles dynamic updates).
  const initialFontSizeRef = useRef(fontSize);

  // Stable refs for callbacks so the mount effect doesn't need to re-run when
  // parents pass new inline functions, while still always calling the latest version.
  const onStatusRef = useRef(onStatus);
  const onSpawnRef = useRef(onSpawn);
  const onInputRef = useRef(onInput);
  const onDataReceivedRef = useRef(onDataReceived);
  const onFocusRef = useRef(onFocus);

  useEffect(() => { onStatusRef.current = onStatus; }, [onStatus]);
  useEffect(() => { onSpawnRef.current = onSpawn; }, [onSpawn]);
  useEffect(() => { onInputRef.current = onInput; }, [onInput]);
  useEffect(() => { onDataReceivedRef.current = onDataReceived; }, [onDataReceived]);
  useEffect(() => { onFocusRef.current = onFocus; }, [onFocus]);

  // -------------------------------------------------------------------------
  // Mount / unmount: create terminal, spawn PTY, wire listeners
  // -------------------------------------------------------------------------
  useEffect(() => {
    const container = containerRef.current;
    if (!container) return;

    let term: Terminal | null = new Terminal({
      fontFamily: '"Cascadia Mono", "JetBrains Mono", "Consolas", monospace',
      fontSize: initialFontSizeRef.current ?? DEFAULT_XTERM_FONT_SIZE,
      cursorBlink: true,
      theme: {
        background: "#0c0c0c",
        foreground: "#e6e6e6",
      },
      scrollback: 5000,
      convertEol: false,
      allowProposedApi: true,
    });
    termRef.current = term;

    const fit = new FitAddon();
    fitRef.current = fit;
    term.loadAddon(fit);
    // Underlines http(s) URLs in the terminal output and routes clicks
    // through Tauri's opener plugin so they open in the user's default
    // browser (WebView2 won't navigate on a plain window.open).
    term.loadAddon(
      new WebLinksAddon((_event, uri) => {
        void openUrl(uri).catch((err) =>
          console.warn("openUrl failed:", err),
        );
      }),
    );
    term.open(container);

    // Initial size — fit before asking the PTY for its dimensions.
    fit.fit();

    let paneId: PaneId | null = null;
    let unlistenData: UnlistenFn | null = null;
    let unlistenExit: UnlistenFn | null = null;
    let ro: ResizeObserver | null = null;
    let destroyed = false;

    (async () => {
      const cols = term!.cols;
      const rows = term!.rows;

      try {
        paneId = await spawnPane({ spec, cols, rows });
        if (destroyed) {
          void killPane(paneId);
          return;
        }
        paneIdRef.current = paneId;
        onStatusRef.current?.(`pane ${paneId} alive`, true);
        onSpawnRef.current?.(paneId);
      } catch (e) {
        if (destroyed) return;
        const msg = `spawn_pane failed: ${e}`;
        term?.write(`\r\n\x1b[31m${msg}\x1b[0m\r\n`);
        onStatusRef.current?.(msg, false);
        return;
      }

      unlistenData = await onPaneData(paneId, (b64) => {
        term?.write(b64ToBytes(b64));
        onDataReceivedRef.current?.();
      });

      unlistenExit = await onPaneExit(paneId, () => {
        term?.write("\r\n\x1b[33m[pane exited]\x1b[0m\r\n");
        onStatusRef.current?.(`pane ${paneId} exited`, false);
      });

      term?.onData((data) => {
        if (paneId == null) return;
        const b64 = stringToB64(data);
        void writeToPane(paneId, b64);
        onInputRef.current?.(b64);
      });

      // Ctrl+Shift+C / Ctrl+Shift+V — copy selection / paste from clipboard.
      // Runs before xterm consumes the key, so the textarea never sees a raw
      // Ctrl+V (which would otherwise inject ^V into the PTY). term.paste()
      // routes through onData → writeToPane, so broadcasting and bracketed
      // paste both keep working for free.
      //
      // Uses tauri-plugin-clipboard-manager instead of navigator.clipboard so
      // WebView2 doesn't surface its native "Allow clipboard access?" prompt.
      term?.attachCustomKeyEventHandler((e) => {
        if (e.type !== "keydown") return true;
        if (!e.ctrlKey || !e.shiftKey || e.altKey) return true;
        if (e.code === "KeyC") {
          const sel = term?.getSelection();
          if (sel) {
            void clipboardWriteText(sel).catch((err) =>
              console.warn("clipboard write failed:", err),
            );
          }
          e.preventDefault();
          return false;
        }
        if (e.code === "KeyV") {
          e.preventDefault();
          clipboardReadText()
            .then((text) => {
              if (text && term) term.paste(text);
            })
            .catch((err) => console.warn("clipboard read failed:", err));
          return false;
        }
        return true;
      });

      // Focus detection: xterm.js doesn't expose onFocus as a first-class event
      // in all versions, so try the proposed API first then fall back to the DOM.
      term?.onSelectionChange(() => {}); // ensure addon system is initialised; noop
      const termAny = term as unknown as { onFocus?: (cb: () => void) => void };
      if (typeof termAny.onFocus === "function") {
        termAny.onFocus(() => onFocusRef.current?.());
      } else {
        const ta = container.querySelector(".xterm-helper-textarea");
        if (ta) ta.addEventListener("focus", () => onFocusRef.current?.(), true);
      }

      // Re-fit on container resize. xterm.fit() + a forced refresh run
      // immediately (visual must stay smooth during a drag), but the
      // actual PTY resize call is debounced: every SIGWINCH makes bash
      // redraw the prompt, and if we send 60+ of them per second during a
      // gutter drag, the redraws corrupt each other and the terminal
      // fills with garbled half-prompts. The debounce means the PTY
      // hears about resizes ~150 ms after you stop dragging, at the
      // final size — bash gets a single clean redraw.
      let resizeRaf: number | null = null;
      let resizePtyTimer: number | null = null;
      let lastSentCols = -1;
      let lastSentRows = -1;
      ro = new ResizeObserver(() => {
        if (resizeRaf != null) return;
        resizeRaf = requestAnimationFrame(() => {
          resizeRaf = null;
          if (!term) return;
          try {
            fit.fit();
            term.refresh(0, term.rows - 1);
            if (resizePtyTimer != null) clearTimeout(resizePtyTimer);
            resizePtyTimer = window.setTimeout(() => {
              resizePtyTimer = null;
              if (paneId == null || !term) return;
              // Skip if the cell grid didn't actually change — saves a
              // pointless SIGWINCH that would make bash redraw its prompt
              // (which feeds back into onDataReceived and causes the idle
              // indicator to flap; see the analysis around v0.2.2).
              if (term.cols === lastSentCols && term.rows === lastSentRows) {
                return;
              }
              lastSentCols = term.cols;
              lastSentRows = term.rows;
              void resizePane(paneId, term.cols, term.rows);
            }, 150);
          } catch (e) {
            console.warn("resize failed", e);
          }
        });
      });
      ro.observe(container);

      // Focus so typing immediately lands in the terminal.
      term?.focus();
    })();

    return () => {
      destroyed = true;
      ro?.disconnect();
      unlistenData?.();
      unlistenExit?.();
      if (paneId != null) void killPane(paneId);
      term?.dispose();
      term = null;
      termRef.current = null;
      fitRef.current = null;
      paneIdRef.current = null;
    };
    // spec is read once at mount; intentionally omitted from deps so we
    // don't remount on parent re-renders. Callers force a respawn by
    // bumping the React `key` (changeShell swaps the leaf id for that).
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // -------------------------------------------------------------------------
  // focusTrigger: programmatic refocus from parent (palette navigation etc.)
  // -------------------------------------------------------------------------
  useEffect(() => {
    if (focusTrigger > 0 && containerRef.current) {
      const ta = containerRef.current.querySelector<HTMLTextAreaElement>(
        ".xterm-helper-textarea",
      );
      ta?.focus();
    }
  }, [focusTrigger]);

  // -------------------------------------------------------------------------
  // Live font-size changes (Ctrl+Shift+= / - / 0).
  //
  // Setting term.options.fontSize re-rasterises glyphs immediately, but the
  // cols/rows the terminal thinks it has are still based on the OLD cell
  // size — so we have to fit() to recompute, refresh() to repaint, then
  // ship the new dimensions to the PTY so bash redraws the prompt at the
  // right width.
  // -------------------------------------------------------------------------
  useEffect(() => {
    const term = termRef.current;
    const fit = fitRef.current;
    if (!term || !fit) return;
    const target = fontSize ?? DEFAULT_XTERM_FONT_SIZE;
    if (term.options.fontSize === target) return;
    try {
      term.options.fontSize = target;
      fit.fit();
      term.refresh(0, term.rows - 1);
      const paneId = paneIdRef.current;
      if (paneId != null) void resizePane(paneId, term.cols, term.rows);
    } catch (e) {
      console.warn("font-size apply failed", e);
    }
  }, [fontSize]);

  return <div ref={containerRef} style={{ width: "100%", height: "100%" }} />;
}
