import { useEffect, useRef } from 'react';
import { Terminal } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';
import { SearchAddon } from '@xterm/addon-search';
import { WebLinksAddon } from '@xterm/addon-web-links';
import '@xterm/xterm/css/xterm.css';
import { RefreshCw } from 'lucide-react';
import { useTerminalFit } from '@/hooks/terminal/useTerminalFit';
import { useTerminalSocket } from '@/hooks/terminal/useTerminalSocket';
import { useTerminalSelection } from '@/hooks/terminal/useTerminalSelection';
import { TerminalHotkeyBar } from '@/components/panes/terminal/TerminalHotkeyBar';
import { FloatingMenu } from '@/components/panes/terminal/FloatingMenu';
import { SearchBar } from '@/components/panes/terminal/SearchBar';
import { TERM_BG, XTERM_THEME } from '@/components/panes/terminal/theme';

// Architecture invariants (do not regress without reading the comments
// next to each):
//
//   - Resize is in-band on the WebSocket as a JSON text frame. The HTTP
//     /resize endpoint had a race with PTY-map registration; WS frames
//     don't. (lib/terminal-protocol.ts owns the wire encoding.)
//   - Server `init` text frame triggers term.clear() so a stale buffer
//     can't leak through the capture-pane replay.
//   - DOM renderer only (no @xterm/addon-webgl). The WebGL atlas bakes
//     glyphs via Canvas2D at load time and locks against whatever font
//     is registered then; @fontsource JBM Variable subsets don't cover
//     U+2500-25FF, so the atlas baked against the system fallback and
//     opencode/claude banners rendered garbled. DOM renderer uses real
//     text spans, so the browser's text rasterizer handles unicode
//     fallback per-character — boolab pattern, just works.
//   - Refit on document.fonts.ready (post-open) so xterm's cell metrics
//     don't bake against the system fallback. Boolab pattern: rAF inside
//     fonts.ready, plus a 150ms safety-net setTimeout. (useTerminalFit.)
//
// Decomposition (v2 Phase 9): this orchestrator constructs/disposes the
// Terminal (spine effect) and composes three hooks —
//   - useTerminalFit:       measurement + the pure-fit refit triggers.
//   - useTerminalSocket:    WS lifecycle, term I/O handlers, bootstrap,
//                           visibility reconnect, sticky Ctrl.
//   - useTerminalSelection: touch long-press, context menu, clipboard,
//                           custom keys, registries, menu actions.
// The spine effect is registered FIRST so termRef is populated before the
// hooks' effects run on mount. (On unmount that means term.dispose() runs
// before the hooks' cleanups — verified safe: no hook cleanup touches the
// terminal, and the WS message handler null-guards on termRef.)

interface Props {
  sessionId: string;
  paneId: string;
  label: string;
  description?: string;
  parentAgent?: string;
  active?: boolean;
}

export function TerminalPane({ sessionId, paneId, label, description, parentAgent, active = false }: Props) {
  const containerRef = useRef<HTMLDivElement | null>(null);
  const termRef = useRef<Terminal | null>(null);
  const searchRef = useRef<SearchAddon | null>(null);

  // Spine: construct + dispose the Terminal. Registered before the hooks so
  // termRef/searchRef are set when their effects run on mount. NOTE: `label`
  // is intentionally NOT a dependency — it is positional ("Terminal N") and
  // renumbers on pane add/remove; keeping it out of these deps stops the live
  // terminal from tearing down + reconnecting on a cosmetic renumber. The
  // terminal registry's label is refreshed by useTerminalSelection's
  // [paneId, label] effect instead.
  useEffect(() => {
    const container = containerRef.current;
    if (!container) return;

    // On mobile viewports drop to 11px so opencode/claude get enough cols.
    const fontSize = typeof window !== 'undefined' && window.innerWidth < 768 ? 11 : 13;
    const term = new Terminal({
      convertEol: true,
      cursorBlink: true,
      allowProposedApi: true,
      fontFamily:
        "'JetBrains Mono Variable', 'JetBrains Mono', 'Fira Code', Menlo, monospace",
      fontSize,
      // Locked cell metrics so DOM-renderer rows line up: integer-multiple
      // line height and zero letter spacing keep glyph spans cell-aligned.
      // globals.css enforces the same with !important so an inherited rule
      // can't fight us.
      lineHeight: 1.0,
      letterSpacing: 0,
      scrollback: 10_000,
      fastScrollModifier: 'shift',
      altClickMovesCursor: false,
      theme: XTERM_THEME,
    });
    termRef.current = term;
    const fitAddon = new FitAddon();
    const search = new SearchAddon();
    searchRef.current = search;
    term.loadAddon(fitAddon);
    term.loadAddon(search);
    term.loadAddon(new WebLinksAddon());
    term.open(container);

    return () => {
      try {
        term.dispose();
      } catch {
        /* ignore */
      }
      termRef.current = null;
      searchRef.current = null;
    };
  }, [sessionId, paneId]);

  const fit = useTerminalFit({ termRef, containerRef, sessionId, paneId });
  const socket = useTerminalSocket({
    termRef,
    sessionId,
    paneId,
    description,
    parentAgent,
    fit: fit.fit,
    getSize: fit.getSize,
    setSize: fit.setSize,
  });
  const selection = useTerminalSelection({
    termRef,
    containerRef,
    sessionId,
    paneId,
    label,
    send: socket.send,
  });

  // Refit when this pane becomes the visible/focused one (e.g. mobile pane
  // switch, maximize toggle). Routes through useTerminalFit().fit so the
  // visualViewport keyboard clip is applied consistently (v2 Phase 9 dedup —
  // previously this effect re-implemented the cell-metric→resize math WITHOUT
  // the keyboard clip). term.onResize sends the resize WS frame if cols or
  // rows actually changed.
  useEffect(() => {
    if (!active) return;
    const raf = requestAnimationFrame(() => fit.fit());
    return () => cancelAnimationFrame(raf);
  }, [active, fit.fit]);

  return (
    <div className="relative flex flex-col flex-1 min-w-0 self-stretch w-full h-full bg-[#0b0f14]">
      {/* xterm CSS overrides live in src/styles/globals.css (boolab pattern).
          Putting them in an inline <style> here races the upstream xterm.css
          on first paint — the right-edge #000 stripe survives the override. */}
      <TerminalHotkeyBar
        ctrlArmed={socket.ctrlArmed}
        onSendBytes={socket.send}
        onArmCtrl={socket.armCtrl}
        onFit={fit.fit}
      />
      {(description || parentAgent) && (
        <div className="flex items-center gap-2 px-3 py-1 text-xs border-b border-border/30 bg-[#0b0f14] shrink-0">
          {parentAgent && (
            <span className="inline-flex items-center rounded-full bg-primary/10 text-primary px-2 py-0.5 text-[10px] font-medium leading-none">
              {parentAgent}
            </span>
          )}
          {description && (
            <span className="text-muted-foreground truncate">{description}</span>
          )}
        </div>
      )}
      <div
        ref={containerRef}
        className="flex-1 min-h-0 w-full overflow-hidden"
        style={{ touchAction: 'pan-y', background: TERM_BG }}
        data-testid="terminal-pane"
      />
      {selection.searchOpen && (
        <SearchBar
          searchRef={searchRef}
          theme={XTERM_THEME}
          onClose={() => selection.setSearchOpen(false)}
        />
      )}
      {selection.menu && (
        <FloatingMenu
          x={selection.menu.x}
          y={selection.menu.y}
          hasSelection={selection.hasSelection}
          chatInputs={selection.chatInputs}
          onCopy={selection.actions.copy}
          onPaste={selection.actions.paste}
          onSelectAll={selection.actions.selectAll}
          onSearch={selection.actions.search}
          onSendToChat={selection.actions.sendToChat}
          onDismiss={() => selection.setMenu(null)}
        />
      )}
      {socket.connState === 'reconnecting' && (
        <div className="absolute inset-x-0 top-0 bg-amber-900/85 text-amber-100 text-xs px-3 py-1 flex items-center gap-2 pointer-events-none">
          <RefreshCw size={12} className="animate-spin" />
          <span>Reconnecting…</span>
        </div>
      )}
      {socket.connState === 'disconnected' && (
        <div className="absolute inset-x-0 top-0 bg-destructive/85 text-destructive-foreground text-xs px-3 py-1 flex items-center gap-2">
          <span>Disconnected from terminal server.</span>
          <button
            type="button"
            onClick={() => socket.reconnect()}
            className="ml-auto inline-flex items-center gap-1 rounded bg-background/20 hover:bg-background/30 px-2 py-0.5"
          >
            <RefreshCw size={12} /> Reconnect
          </button>
        </div>
      )}
    </div>
  );
}
