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();

export function TerminalView({ surfaceId }: { surfaceId: string }) {
  const ref = useRef<HTMLDivElement>(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: "'JetBrains Mono Variable', 'JetBrains Mono', monospace", fontSize: 13, convertEol: false, scrollback: 10000, allowProposedApi: true });
    try {
      term.loadAddon(new WebglAddon());
    } 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);

    // 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();
    };
  }, [surfaceId]);

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