import { useRef, useEffect } from "react";
import { cn } from "@decocms/ui/lib/utils.ts";
import { Terminal } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";
import "@xterm/xterm/css/xterm.css";
import {
  useSandboxChunkHandler,
  useSandboxEvents,
} from "../../hooks/use-sandbox-events";

interface SandboxTerminalProps {
  /**
   * Log source this terminal renders ("setup", "daemon", or a script name
   * like "dev"). The terminal pulls the replay buffer at mount and subscribes
   * to live chunks for this source. Self-contained — no parent-side routing.
   */
  source: string;
  onReady?: (terminal: Terminal) => void;
  onSelectionChange?: (hasSelection: boolean, getText: () => string) => void;
  className?: string;
}

export function SandboxTerminal({
  source,
  onReady,
  onSelectionChange,
  className,
}: SandboxTerminalProps) {
  const containerRef = useRef<HTMLDivElement>(null);
  const terminalRef = useRef<Terminal | null>(null);
  const onSelectionChangeRef = useRef(onSelectionChange);
  // oxlint-disable-next-line ban-ref-current-assignment/ban-ref-current-assignment -- TODO: refactor render-time .current access
  onSelectionChangeRef.current = onSelectionChange;
  const vmEvents = useSandboxEvents();
  // Stable ref so the chunk handler (registered once on mount) always sees
  // the current source; no dep churn on prop changes.
  const sourceRef = useRef(source);
  // oxlint-disable-next-line ban-ref-current-assignment/ban-ref-current-assignment -- TODO: refactor render-time .current access
  sourceRef.current = source;

  useSandboxChunkHandler((chunkSource, data) => {
    if (chunkSource !== sourceRef.current) return;
    terminalRef.current?.write(data);
  });

  // oxlint-disable-next-line ban-use-effect/ban-use-effect — xterm.js lifecycle: create on mount, dispose on unmount
  useEffect(() => {
    const el = containerRef.current;
    if (!el) return;

    const style = getComputedStyle(document.documentElement);
    const cssVar = (name: string) =>
      style.getPropertyValue(name).trim() || undefined;

    const terminal = new Terminal({
      allowTransparency: false,
      fontFamily:
        cssVar("--font-mono") ||
        "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
      fontSize: 12.5,
      lineHeight: 1.5,
      scrollback: 5000,
      cursorBlink: false,
      disableStdin: true,
      theme: {
        background:
          cssVar("--sidebar") ||
          cssVar("--card") ||
          cssVar("--background") ||
          "#1e1e1e",
        cursor: "transparent",
        foreground: cssVar("--foreground") || "#d4d4d4",
        selectionBackground: cssVar("--accent"),
        selectionForeground: cssVar("--accent-foreground"),

        black: cssVar("--muted"),
        red: cssVar("--destructive"),
        green: cssVar("--success"),
        yellow: cssVar("--warning"),
        blue: cssVar("--chart-1"),
        magenta: cssVar("--chart-3"),
        cyan: cssVar("--chart-5"),
        white: cssVar("--foreground"),

        brightBlack: cssVar("--muted-foreground"),
        brightRed: cssVar("--destructive"),
        brightGreen: cssVar("--success"),
        brightYellow: cssVar("--warning"),
        brightBlue: cssVar("--chart-1"),
        brightMagenta: cssVar("--chart-3"),
        brightCyan: cssVar("--chart-5"),
        brightWhite: cssVar("--foreground"),
      },
    });

    const fitAddon = new FitAddon();
    terminal.loadAddon(fitAddon);

    terminal.open(el);
    fitAddon.fit();
    // Replay anything the buffer has accumulated for this source — covers
    // chunks that arrived before the tab mounted (e.g. clone output that
    // streamed during the "creating" status phase).
    const replay = vmEvents.getBuffer(source);
    if (replay) {
      terminal.write(replay);
    }
    terminalRef.current = terminal;
    onReady?.(terminal);

    const selectionDisposable = terminal.onSelectionChange(() => {
      const has = !!terminal.getSelection();
      onSelectionChangeRef.current?.(has, () => terminal.getSelection());
    });

    const observer = new ResizeObserver(() => {
      fitAddon.fit();
    });
    observer.observe(el);

    return () => {
      selectionDisposable.dispose();
      observer.disconnect();
      terminalRef.current = null;
      terminal.dispose();
    };
    // oxlint-disable-next-line react-hooks/exhaustive-deps — mount-only: source/vmEvents/onReady are consumed once during terminal setup
  }, []);

  // Outer wrapper holds the background + padding so xterm itself sees a
  // clean, unstyled container — no `overflow-hidden`, no `[&_.xterm-*]`
  // overrides. xterm.js installs its own `wheel` listener on `.xterm`
  // (which forwards to `Viewport.handleWheel`); fighting it with our
  // own scroll/overflow rules was the reason wheel scroll wasn't working.
  return (
    <div className={cn("h-full bg-sidebar p-3", className)}>
      <div ref={containerRef} className="h-full" />
    </div>
  );
}
