import { useRef, useEffect } from "react";
import { Terminal } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";
import type { UnlistenFn } from "@tauri-apps/api/event";
import {
  spawnPane,
  writeToPane,
  resizePane,
  killPane,
  onPaneData,
  onPaneExit,
  type PaneId,
} 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 {
  distro?: string;
  cwd?: string;
  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;
}

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

export default function XtermPane({
  distro,
  cwd,
  onStatus,
  onSpawn,
  onInput,
  onDataReceived,
  onFocus,
  focusTrigger = 0,
}: XtermPaneProps) {
  const containerRef = useRef<HTMLDivElement>(null);

  // 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: 13,
      cursorBlink: true,
      theme: {
        background: "#0c0c0c",
        foreground: "#e6e6e6",
      },
      scrollback: 5000,
      convertEol: false,
      allowProposedApi: true,
    });

    const fit = new FitAddon();
    term.loadAddon(fit);
    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({ distro, cwd, cols, rows });
        if (destroyed) {
          void killPane(paneId);
          return;
        }
        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);
      });

      // 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; forward new size to the PTY.
      ro = new ResizeObserver(() => {
        try {
          fit.fit();
          if (paneId != null && term) {
            void resizePane(paneId, term.cols, term.rows);
          }
        } 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;
    };
    // distro/cwd are only used at spawn time; intentionally omitted from deps
    // so remounting doesn't happen if a parent re-renders with the same values.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // -------------------------------------------------------------------------
  // focusTrigger: programmatic refocus from parent (palette navigation etc.)
  // -------------------------------------------------------------------------
  const termRef = useRef<Terminal | null>(null);

  // Keep termRef in sync via a second effect that runs after mount.
  // We can't easily share the Terminal instance across the two effects without
  // a ref, so we store it on termRef inside the mount effect instead.
  // Actually, let's just wire focusTrigger by querying the textarea directly —
  // that avoids the cross-effect coupling problem entirely.
  useEffect(() => {
    if (focusTrigger > 0 && containerRef.current) {
      const ta = containerRef.current.querySelector<HTMLTextAreaElement>(
        ".xterm-helper-textarea",
      );
      ta?.focus();
    }
  }, [focusTrigger]);

  // Suppress unused ref warning
  void termRef;

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