import { useEffect, useRef } from "react";
import { Terminal } from "@xterm/xterm";
import { WebglAddon } from "@xterm/addon-webgl";
import { SearchAddon } from "@xterm/addon-search";
import { FitAddon } from "@xterm/addon-fit";
import { attachSurface, detachSurface, sendInput, resizeSurface } from "./socketBridge";
import { registerSearch, unregisterSearch } from "./searchRegistry";

const decoder = new TextDecoder();
const encoder = new TextEncoder();

function xtermTheme(p: Record<string, string>) {
  return {
    background: p["bg-panel"],
    foreground: p["text-primary"],
    cursor: p["text-primary"],
    selectionBackground: p["search-match"],
  };
}

export function TerminalView({ surfaceId, font, palette }: { surfaceId: string; font: { family: string; size: number } | null; palette: Record<string, string> | null }) {
  const ref = useRef<HTMLDivElement>(null);
  const termRef = useRef<Terminal | null>(null);
  const fitRef = useRef<FitAddon | null>(null);
  const webglRef = useRef<WebglAddon | null>(null);

  useEffect(() => {
    if (!ref.current) return;
    // allowProposedApi is required by the search addon: its match decorations
    // call registerMarker/registerDecoration (proposed API). Without it findNext
    // throws and the scrollback search counter never updates.
    const term = new Terminal({
      fontFamily: font ? `'${font.family}', monospace` : "'JetBrains Mono Variable', 'JetBrains Mono', monospace",
      fontSize: font?.size ?? 13,
      convertEol: false,
      scrollback: 10000,
      allowProposedApi: true,
      theme: palette ? xtermTheme(palette) : undefined,
    });
    termRef.current = term;

    try {
      const webgl = new WebglAddon();
      term.loadAddon(webgl);
      webglRef.current = webgl;
    } catch {
      // webgl unavailable → fall back to canvas/dom renderer silently
    }
    term.open(ref.current);

    const search = new SearchAddon();
    term.loadAddon(search);
    registerSearch(surfaceId, search);

    const fit = new FitAddon();
    term.loadAddon(fit);
    fitRef.current = fit;

    // Fit the grid to the container and tell the daemon the new size. Coalesced
    // through rAF so a burst of resize callbacks yields one resize per frame.
    let rafId = 0;
    let lastCols = 0, lastRows = 0;
    const doFit = () => {
      rafId = 0;
      try { fit.fit(); } catch { return; }
      if (term.cols !== lastCols || term.rows !== lastRows) {
        lastCols = term.cols;
        lastRows = term.rows;
        void resizeSurface(surfaceId, term.cols, term.rows);
      }
    };
    const scheduleFit = () => { if (!rafId) rafId = requestAnimationFrame(doFit); };

    const ro = new ResizeObserver(scheduleFit);
    ro.observe(ref.current);

    // Input → daemon.
    const inputDisposable = term.onData((data) => {
      void sendInput(surfaceId, encoder.encode(data));
    });

    let disposed = false;

    // Attach: fresh xterm instance, write snapshot, then stream live output.
    void attachSurface(surfaceId, (bytes) => {
      if (!disposed) term.write(decoder.decode(bytes));
    }).then((res) => {
      if (disposed) return;
      if (res.snapshot) term.write(res.snapshot);
      // Fit to the actual container rather than the daemon's stored geometry,
      // then push the resulting size back so the PTY reflows to match.
      scheduleFit();
    });

    return () => {
      disposed = true;
      if (rafId) cancelAnimationFrame(rafId);
      ro.disconnect();
      inputDisposable.dispose();
      void detachSurface(surfaceId);
      unregisterSearch(surfaceId);
      term.dispose();
      termRef.current = null;
      fitRef.current = null;
      webglRef.current = null;
    };
  }, [surfaceId]); // eslint-disable-line react-hooks/exhaustive-deps

  // Live re-apply font and theme when config changes without remounting.
  // font and palette are memoized in App.tsx so stable identity = no spurious re-applies.
  useEffect(() => {
    const t = termRef.current;
    if (!t) return;
    if (font) {
      t.options.fontFamily = `'${font.family}', monospace`;
      t.options.fontSize = font.size;
      // The WebGL renderer caches rasterized glyphs in a texture atlas keyed by
      // the old font/size; without clearing it the grid keeps rendering stale
      // glyphs after a font change.
      webglRef.current?.clearTextureAtlas();
    }
    if (palette) t.options.theme = xtermTheme(palette);
    requestAnimationFrame(() => { try { fitRef.current?.fit(); } catch { /* ignore */ } });
  }, [font, palette]); // eslint-disable-line react-hooks/exhaustive-deps

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