/**
 * @file TerminalView.tsx
 * @description Renders one lane's live terminal — a real `xterm.js` instance
 * attached via WebSocket to the server's `/ws-pty/:runId` path (see
 * server/lib/pty-attach.js), which is itself a `node-pty`-backed
 * `tmux attach-session`. Binary WS frames are raw PTY bytes in both
 * directions; a JSON text frame carries the initial `resize` on mount and
 * the server's one-shot `exit` notice when the pane process ends.
 * @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
 */
import { useEffect, useRef } from "react";
import { Terminal } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";
import "@xterm/xterm/css/xterm.css";

interface TerminalViewProps {
  runId: string;
  wsBaseUrl: string;
}

export function TerminalView({ runId, wsBaseUrl }: TerminalViewProps) {
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const term = new Terminal({ convertEol: true, fontSize: 13, cursorBlink: true });
    const fit = new FitAddon();
    term.loadAddon(fit);
    if (containerRef.current) term.open(containerRef.current);
    fit.fit();

    const ws = new WebSocket(`${wsBaseUrl}/ws-pty/${encodeURIComponent(runId)}`);
    // Server sends PTY bytes as binary frames — default binaryType ("blob")
    // would hand onmessage a Blob that the string checks below never match,
    // silently dropping all terminal output. "arraybuffer" keeps it sync.
    ws.binaryType = "arraybuffer";

    const decoder = new TextDecoder();
    ws.onopen = () => {
      ws.send(JSON.stringify({ type: "resize", cols: term.cols, rows: term.rows }));
    };
    ws.onmessage = (event) => {
      const isArrayBuffer = Object.prototype.toString.call(event.data) === "[object ArrayBuffer]";
      const data = isArrayBuffer ? decoder.decode(event.data as ArrayBuffer) : event.data;
      if (typeof data === "string") {
        // A JSON control frame is the only thing that starts with `{"type"`.
        if (data.startsWith('{"type"')) {
          try {
            const msg = JSON.parse(data);
            if (msg.type === "exit") {
              term.write(`\r\n[session ended, exit code ${msg.code}]\r\n`);
            }
            return;
          } catch {
            /* not JSON — fall through and render as PTY output */
          }
        }
        term.write(data);
      }
    };

    const dataDisposable = term.onData((data) => {
      if (ws.readyState === WebSocket.OPEN) ws.send(data);
    });

    const resizeObserver = new ResizeObserver(() => {
      fit.fit();
      if (ws.readyState === WebSocket.OPEN) {
        ws.send(JSON.stringify({ type: "resize", cols: term.cols, rows: term.rows }));
      }
    });
    if (containerRef.current) resizeObserver.observe(containerRef.current);

    return () => {
      resizeObserver.disconnect();
      dataDisposable.dispose();
      ws.close();
      term.dispose();
    };
  }, [runId, wsBaseUrl]);

  return <div ref={containerRef} className="h-full w-full" data-testid="terminal-view" />;
}
