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

// xterm.js auto-answers device queries (Device Attributes, cursor/status reports,
// OSC color queries, DECRPM mode reports) by emitting the reply through onData.
// But the daemon's alacritty grid is the authoritative emulator and already
// answers these on the PTY (see spacesh-core grid.rs `take_replies` →
// `write_input`). Forwarding xterm's duplicate — which arrives a full IPC
// roundtrip late — lands in the shell's input buffer after it stopped reading
// the reply, so the shell echoes it as literal escape gibberish and the prompt
// shifts. Drop these standalone reports; never the user's keystrokes/paste/mouse
// (mouse reports end in M/m and are real user input the program asked for).
function isDeviceReport(data: string): boolean {
  if (data.charCodeAt(0) !== 0x1b) return false;
  return (
    /^\x1b\[[?>=]?[0-9;]*[cntR]$/.test(data) ||                // DA1/DA2 (c), DSR status (n), text-area/cell-size (t), cursor position (R)
    /^\x1b\[\?[0-9;]*u$/.test(data) ||                         // kitty keyboard QUERY reply (\x1b[?flags u) — NOT key input \x1b[<code>u
    /^\x1b\[\?[0-9;]*\$[py]$/.test(data) ||                    // DECRPM mode report
    /^\x1b\][0-9]+;[^\x07\x1b]*(?:\x07|\x1b\\)$/.test(data) || // OSC color / query reply (BEL- or ST-terminated)
    /^\x1bP[\s\S]*\x1b\\$/.test(data) ||                       // any DCS report (XTVERSION / DECRQSS / status string)
    /^\x1b_[\s\S]*\x1b\\$/.test(data)                          // any APC report (kitty graphics)
  );
}

// Appended after the user font so Nerd Font icon glyphs (Private Use Area) render
// via fallback instead of blank boxes, without changing the base monospace font.
const NERD_FALLBACK = "'Symbols Nerd Font Mono'";
const fontStack = (family: string | null) =>
  family ? `'${family}', ${NERD_FALLBACK}, monospace`
         : `'JetBrains Mono Variable', 'JetBrains Mono', ${NERD_FALLBACK}, monospace`;

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

export function TerminalView({ surfaceId, font, palette, focused }: { surfaceId: string; font: { family: string; size: number } | null; palette: Record<string, string> | null; focused?: boolean }) {
  const ref = useRef<HTMLDivElement>(null);
  const termRef = useRef<Terminal | null>(null);
  const fitRef = useRef<FitAddon | null>(null);
  const webglRef = useRef<WebglAddon | null>(null);
  // A background theme makes term-bg fully transparent so the panel's glass fill
  // shows through. allowTransparency is construction-time only, so it's part of the
  // effect key to force a remount when it flips. WebGL stays on in both modes — the
  // glass/blur lives on a sibling layer (see LayoutEngine), not an ancestor, so the
  // WebGL canvas composites its transparent background without the WKWebView
  // clipping/smearing artifacts that backdrop-filter ancestors cause.
  const transparent = palette?.["term-bg"] === "rgba(0,0,0,0)";

  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: fontStack(font?.family ?? null),
      fontSize: font?.size ?? 13,
      convertEol: false,
      scrollback: 10000,
      allowProposedApi: true,
      allowTransparency: transparent,
      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) => {
      if (isDeviceReport(data)) return; // daemon answers the PTY authoritatively; xterm's dup arrives late and echoes
      void sendInput(surfaceId, encoder.encode(data));
    });

    let disposed = false;

    // The Nerd Font fallback may finish loading after the first paint; once it's
    // ready, drop the WebGL glyph atlas so cached blank cells re-rasterize with icons.
    void document.fonts.load("16px 'Symbols Nerd Font Mono'").then(() => {
      if (!disposed) webglRef.current?.clearTextureAtlas();
    }).catch(() => {});

    // 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, transparent]); // eslint-disable-line react-hooks/exhaustive-deps

  // Keyboard focus cycling (cmd+]/[) only changes the focusedId state — it never
  // touches the DOM, so the new panel's xterm textarea stays unfocused and keys
  // keep flowing to the old terminal. Mouse clicks don't hit this because the
  // click lands on the textarea directly. Drive xterm focus from the prop.
  useEffect(() => {
    if (focused) termRef.current?.focus();
  }, [focused]);

  // 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 = fontStack(font.family);
      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%" }} />;
}
