import { useEffect, useRef, type Dispatch, type RefObject, type SetStateAction } from "react";
import type { Terminal as XtermTerminal } from "@xterm/xterm";
import type { FitAddon as XtermFitAddon } from "@xterm/addon-fit";

import {
  buildTerminalWebSocketUrl,
  parseTerminalServerMessage,
} from "../components/terminal-view-helpers";
import { importChunk } from "../lib/import-chunk";

export type ConnectionStatus = "connecting" | "ready" | "closed" | "error";

export type TerminalConnectionError = {
  message: string;
  recoverable: boolean;
};

export const TERMINAL_BACKGROUND = "#05080F";

// Pin the cell to a whole-pixel height so xterm's fit math stays exact.
// fontSize × lineHeight = 13 × (19/13) = 19px → no sub-pixel rounding,
// no bottom-row clipping.
const TERMINAL_FONT_SIZE = 13;
const TERMINAL_CELL_HEIGHT_PX = 19;
const TERMINAL_LINE_HEIGHT = TERMINAL_CELL_HEIGHT_PX / TERMINAL_FONT_SIZE;

const TERMINAL_THEME = {
  background:          TERMINAL_BACKGROUND,
  foreground:          "#E6EDF3",
  cursor:              "#7AC4E5",
  cursorAccent:        TERMINAL_BACKGROUND,
  selectionBackground: "#1F4F73",

  black:   TERMINAL_BACKGROUND,
  red:     "#FF6B6B",
  green:   "#5EE6A8",
  yellow:  "#FFC857",
  blue:    "#82AAFF",
  magenta: "#C792EA",
  cyan:    "#7AC4E5",
  white:   "#D5DCE3",

  brightBlack:   "#4B5563",
  brightRed:     "#FF8B8B",
  brightGreen:   "#85F5C2",
  brightYellow:  "#FFD98A",
  brightBlue:    "#A4C4FF",
  brightMagenta: "#E0B6FF",
  brightCyan:    "#A8DFF5",
  brightWhite:   "#FFFFFF",
};

function sendResize(socket: WebSocket | null, terminal: XtermTerminal | null) {
  if (!socket || socket.readyState !== WebSocket.OPEN || !terminal) return;
  socket.send(JSON.stringify({
    type: "resize",
    cols: terminal.cols,
    rows: terminal.rows,
  }));
}

/**
 * Synchronizes a mounted DOM node with xterm, its FitAddon, ResizeObserver, and
 * the run terminal WebSocket. All listeners, observers, sockets, and xterm
 * disposables are cleaned up before reconnect and on unmount.
 */
export function useTerminalSession({
  connectionKey,
  runId,
  setError,
  setStatus,
  terminalEl,
}: {
  connectionKey: number;
  runId: string;
  setError: Dispatch<SetStateAction<TerminalConnectionError | null>>;
  setStatus: Dispatch<SetStateAction<ConnectionStatus>>;
  terminalEl: RefObject<HTMLDivElement | null>;
}) {
  const terminalRef = useRef<XtermTerminal | null>(null);
  const fitRef = useRef<XtermFitAddon | null>(null);
  const socketRef = useRef<WebSocket | null>(null);

  useEffect(() => {
    if (!terminalEl.current) return undefined;

    let disposed = false;
    let resizeObserver: ResizeObserver | null = null;
    const textEncoder = new TextEncoder();
    const disposables: Array<{ dispose: () => void }> = [];

    function disposeResources() {
      resizeObserver?.disconnect();
      resizeObserver = null;
      for (const disposable of disposables.splice(0)) disposable.dispose();

      const socket = socketRef.current;
      if (socket) {
        if (socket.readyState === WebSocket.OPEN) {
          socket.send(JSON.stringify({ type: "close" }));
        }
        socket.close();
        socketRef.current = null;
      }

      terminalRef.current?.dispose();
      terminalRef.current = null;
      fitRef.current = null;
    }

    async function connect() {
      setStatus("connecting");
      setError(null);

      const [{ Terminal }, { FitAddon }] = await Promise.all([
        importChunk(() => import("@xterm/xterm")),
        importChunk(() => import("@xterm/addon-fit")),
      ]);
      if (disposed || !terminalEl.current) return;

      const terminal = new Terminal({
        cursorBlink: true,
        convertEol: true,
        fontFamily: "\"JetBrains Mono\", ui-monospace, monospace",
        fontSize: TERMINAL_FONT_SIZE,
        lineHeight: TERMINAL_LINE_HEIGHT,
        scrollback: 5000,
        theme: TERMINAL_THEME,
      });
      const fitAddon = new FitAddon();
      terminalRef.current = terminal;
      fitRef.current = fitAddon;
      terminal.loadAddon(fitAddon);
      terminal.open(terminalEl.current);
      fitAddon.fit();
      terminal.focus();

      const socket = new WebSocket(buildTerminalWebSocketUrl(window.location, runId));
      socket.binaryType = "arraybuffer";
      socketRef.current = socket;

      disposables.push(terminal.onData((data) => {
        if (socket.readyState === WebSocket.OPEN) {
          socket.send(textEncoder.encode(data));
        }
      }));

      const handleOpen = () => {
        sendResize(socket, terminal);
      };
      const handleMessage = (event: MessageEvent) => {
        if (typeof event.data === "string") {
          const message = parseTerminalServerMessage(event.data);
          if (!message) return;
          if (message.type === "ready") {
            setStatus("ready");
            return;
          }
          if (message.type === "closed") {
            setStatus("closed");
            return;
          }
          setStatus("error");
          setError({
            message: message.message ?? "Terminal session failed.",
            recoverable: false,
          });
          return;
        }
        const bytes = event.data instanceof ArrayBuffer
          ? new Uint8Array(event.data)
          : event.data;
        terminal.write(bytes);
      };
      const handleClose = () => {
        setStatus((current) => current === "error" ? current : "closed");
      };
      const handleError = () => {
        setStatus("error");
        setError({
          message: "Terminal WebSocket connection failed.",
          recoverable: true,
        });
      };
      socket.addEventListener("open", handleOpen);
      socket.addEventListener("message", handleMessage);
      socket.addEventListener("close", handleClose);
      socket.addEventListener("error", handleError);
      disposables.push({
        dispose: () => {
          socket.removeEventListener("open", handleOpen);
          socket.removeEventListener("message", handleMessage);
          socket.removeEventListener("close", handleClose);
          socket.removeEventListener("error", handleError);
        },
      });

      resizeObserver = new ResizeObserver(() => {
        fitAddon.fit();
        sendResize(socket, terminal);
      });
      resizeObserver.observe(terminalEl.current);

      if (typeof document !== "undefined" && document.fonts?.ready) {
        void document.fonts.ready.then(() => {
          if (disposed) return;
          fitAddon.fit();
          sendResize(socket, terminal);
        });
      }
    }

    void connect().catch((error: unknown) => {
      if (disposed) return;
      disposeResources();
      setStatus("error");
      setError({
        message: error instanceof Error
          ? `Terminal initialization failed: ${error.message}`
          : "Terminal initialization failed.",
        recoverable: true,
      });
    });

    return () => {
      disposed = true;
      disposeResources();
    };
  }, [connectionKey, runId, setError, setStatus, terminalEl]);
}
