import { onBeforeUnmount } from "vue";
import { Terminal } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";
import { SearchAddon } from "@xterm/addon-search";
import { WebLinksAddon } from "@xterm/addon-web-links";
import { Call, Events } from "@wailsio/runtime";
import "@xterm/xterm/css/xterm.css";

const SSH = "github.com/vstockwell/wraith/internal/ssh.SSHService";

/** MobaXTerm Classic–inspired terminal theme colors. */
const defaultTheme = {
  background: "#0d1117",
  foreground: "#e0e0e0",
  cursor: "#58a6ff",
  cursorAccent: "#0d1117",
  selectionBackground: "rgba(88, 166, 255, 0.3)",
  selectionForeground: "#ffffff",
  black: "#0d1117",
  red: "#f85149",
  green: "#3fb950",
  yellow: "#e3b341",
  blue: "#58a6ff",
  magenta: "#bc8cff",
  cyan: "#39c5cf",
  white: "#e0e0e0",
  brightBlack: "#484f58",
  brightRed: "#ff7b72",
  brightGreen: "#56d364",
  brightYellow: "#e3b341",
  brightBlue: "#79c0ff",
  brightMagenta: "#d2a8ff",
  brightCyan: "#56d4dd",
  brightWhite: "#f0f6fc",
};

export interface UseTerminalReturn {
  terminal: Terminal;
  fitAddon: FitAddon;
  mount: (container: HTMLElement) => void;
  destroy: () => void;
  write: (data: string) => void;
  fit: () => void;
}

/**
 * Composable that manages an xterm.js Terminal lifecycle.
 *
 * Wires bidirectional I/O:
 *   - User keystrokes → SSHService.Write (via Wails Call)
 *   - SSH stdout → xterm.js (via Wails Events, base64 encoded)
 *   - Terminal resize → SSHService.Resize (via Wails Call)
 */
export function useTerminal(sessionId: string): UseTerminalReturn {
  const fitAddon = new FitAddon();
  const searchAddon = new SearchAddon();
  const webLinksAddon = new WebLinksAddon();

  const terminal = new Terminal({
    theme: defaultTheme,
    fontFamily: "'JetBrains Mono', 'Cascadia Code', 'Fira Code', Menlo, Monaco, monospace",
    fontSize: 14,
    lineHeight: 1.2,
    cursorBlink: true,
    cursorStyle: "block",
    scrollback: 10000,
    allowProposedApi: true,
    convertEol: true,
    rightClickSelectsWord: false,
  });

  terminal.loadAddon(fitAddon);
  terminal.loadAddon(searchAddon);
  terminal.loadAddon(webLinksAddon);

  // Forward typed data to the SSH backend
  terminal.onData((data: string) => {
    Call.ByName(`${SSH}.Write`, sessionId, data).catch((err: unknown) => {
      console.error("SSH write error:", err);
    });
  });

  // Forward resize events to the SSH backend
  terminal.onResize((size: { cols: number; rows: number }) => {
    Call.ByName(`${SSH}.Resize`, sessionId, size.cols, size.rows).catch((err: unknown) => {
      console.error("SSH resize error:", err);
    });
  });

  // MobaXTerm-style clipboard: highlight to copy, right-click to paste
  const selectionDisposable = terminal.onSelectionChange(() => {
    const sel = terminal.getSelection();
    if (sel) {
      navigator.clipboard.writeText(sel).catch(() => {});
    }
  });

  function handleRightClickPaste(e: MouseEvent): void {
    e.preventDefault();
    e.stopPropagation();
    navigator.clipboard.readText().then((text) => {
      if (text) {
        Call.ByName(`${SSH}.Write`, sessionId, text).catch(() => {});
      }
    }).catch(() => {});
  }

  // Listen for SSH output events from the Go backend (base64 encoded)
  let cleanupEvent: (() => void) | null = null;

  let resizeObserver: ResizeObserver | null = null;

  // Streaming TextDecoder persists across events so split multi-byte UTF-8
  // sequences at chunk boundaries are decoded correctly (e.g. a 3-byte em-dash
  // split across two Go read() calls).
  const utf8Decoder = new TextDecoder("utf-8", { fatal: false });

  // Write batching — accumulate chunks and flush once per animation frame.
  // Without this, every tiny SSH read (sometimes single characters) triggers
  // a separate terminal.write(), producing a laggy typewriter effect.
  let pendingData = "";
  let rafId: number | null = null;

  function flushPendingData(): void {
    rafId = null;
    if (pendingData) {
      terminal.write(pendingData);
      pendingData = "";
    }
  }

  function queueWrite(data: string): void {
    pendingData += data;
    if (rafId === null) {
      rafId = requestAnimationFrame(flushPendingData);
    }
  }

  function mount(container: HTMLElement): void {
    terminal.open(container);
    fitAddon.fit();

    // Right-click paste on the terminal's DOM element
    terminal.element?.addEventListener("contextmenu", handleRightClickPaste);

    // Subscribe to SSH output events for this session
    // Wails v3 Events.On callback receives a CustomEvent object with .data property
    cleanupEvent = Events.On(`ssh:data:${sessionId}`, (event: any) => {
      // Extract the base64 string from the event
      // event.data is the first argument passed to app.Event.Emit()
      let b64data: string;
      if (typeof event === "string") {
        b64data = event;
      } else if (event?.data && typeof event.data === "string") {
        b64data = event.data;
      } else if (Array.isArray(event?.data)) {
        b64data = String(event.data[0] ?? "");
      } else {
        // Debug: log the actual shape so we can fix it
        console.warn("ssh:data unexpected shape:", JSON.stringify(event)?.slice(0, 200));
        return;
      }

      try {
        // atob() returns Latin-1 — each byte becomes a char code 0x00–0xFF.
        // Reconstruct raw bytes, then decode with the streaming TextDecoder
        // which buffers incomplete multi-byte sequences between calls.
        const binaryStr = atob(b64data);
        const bytes = new Uint8Array(binaryStr.length);
        for (let i = 0; i < binaryStr.length; i++) {
          bytes[i] = binaryStr.charCodeAt(i);
        }
        const decoded = utf8Decoder.decode(bytes, { stream: true });
        if (decoded) {
          queueWrite(decoded);
        }
      } catch {
        // Fallback: write raw if not valid base64
        queueWrite(b64data);
      }
    });

    // Auto-fit when the container resizes
    resizeObserver = new ResizeObserver(() => {
      fitAddon.fit();
    });
    resizeObserver.observe(container);
  }

  function destroy(): void {
    if (rafId !== null) {
      cancelAnimationFrame(rafId);
      rafId = null;
    }
    // Flush any remaining buffered data before teardown
    if (pendingData) {
      terminal.write(pendingData);
      pendingData = "";
    }
    terminal.element?.removeEventListener("contextmenu", handleRightClickPaste);
    selectionDisposable.dispose();
    if (cleanupEvent) {
      cleanupEvent();
      cleanupEvent = null;
    }
    if (resizeObserver) {
      resizeObserver.disconnect();
      resizeObserver = null;
    }
    terminal.dispose();
  }

  function write(data: string): void {
    terminal.write(data);
  }

  function fit(): void {
    fitAddon.fit();
  }

  onBeforeUnmount(() => {
    destroy();
  });

  return { terminal, fitAddon, mount, destroy, write, fit };
}
