import { useCallback, useEffect, useRef } from 'react';
import type { Terminal } from '@xterm/xterm';

// useTerminalFit — terminal measurement + the refit triggers that are purely
// about sizing (fonts.ready rAF, 150ms settle timeout, ResizeObserver,
// visualViewport resize). The two refit triggers that are entangled with the
// connection lifecycle — the init rAF (fit → /start → connect) and the
// visibilitychange handler (reconnect-or-refit) — live in useTerminalSocket
// and call this hook's `fit()`; that keeps each of those single-rAF / if-else
// sequences intact rather than splitting one listener across two hooks.
//
// Exposes a stable `fit()` (the FitAddon-bypass measurement, with the iOS
// keyboard clip) and `getSize()`/`setSize()` over the read-latest size mirror.

// xterm 5 ships no public dimensions API — `_core._renderService.dimensions`
// is internal but stable across the package versions we ship. This is the ONE
// place that reaches into the private cell metrics; everything else funnels
// through `cellSize` or `readCellMetrics`.
function readCellMetrics(term: Terminal): { w: number; h: number } | null {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const dims = (term as any)._core?._renderService?.dimensions?.css?.cell;
  if (dims && dims.width > 0 && dims.height > 0) {
    return { w: dims.width, h: dims.height };
  }
  return null;
}

// Cell metrics with a getBoundingClientRect fallback for callers (touch
// point→cell mapping) that need an estimate even before xterm has measured.
// `fit()` deliberately does NOT use this fallback — it bails until real
// metrics exist so it never resizes against a fallback estimate.
export function cellSize(term: Terminal, container: HTMLElement): { w: number; h: number } {
  const m = readCellMetrics(term);
  if (m) return m;
  const rect = container.getBoundingClientRect();
  return { w: rect.width / Math.max(term.cols, 1), h: rect.height / Math.max(term.rows, 1) };
}

interface FitDeps {
  termRef: React.MutableRefObject<Terminal | null>;
  containerRef: React.MutableRefObject<HTMLDivElement | null>;
  sessionId: string;
  paneId: string;
}

export interface TerminalFit {
  fit: () => void;
  getSize: () => { cols: number; rows: number };
  setSize: (cols: number, rows: number) => void;
}

export function useTerminalFit({ termRef, containerRef, sessionId, paneId }: FitDeps): TerminalFit {
  // Last size pushed through term.onResize. Mirrors xterm's authoritative
  // cols/rows; consulted on every WS open so the resize frame carries the
  // currently-measured size, not a stale lastSize from before the latest fit.
  const lastSizeRef = useRef<{ cols: number; rows: number }>({ cols: 80, rows: 24 });

  const getSize = useCallback(() => lastSizeRef.current, []);
  const setSize = useCallback((cols: number, rows: number) => {
    lastSizeRef.current = { cols, rows };
  }, []);

  // Bypass FitAddon's proposeDimensions(), which subtracts the native
  // scrollbar's reserved width even after CSS hides it (boolab fix). We
  // compute cols/rows directly from host.clientWidth/Height ÷ cell metrics.
  // Falls through (no-op) if cell metrics aren't measured yet — a later
  // refit (fonts.ready / setTimeout / ResizeObserver) will catch it.
  //
  // v1.10.8d: row count uses the VISIBLE height (clipped against the
  // visualViewport) rather than raw clientHeight. h-dvh on the root only
  // shrinks for the URL bar — not the iOS keyboard — so without the clip
  // we allocate rows for the area hidden behind the keyboard, and bash's
  // cursor (pushed down by MOTD on SSH login) ends up below the fold.
  const fit = useCallback((): void => {
    const t = termRef.current;
    if (!t) return;
    const host = containerRef.current;
    if (!host) return;
    if (!t.element || !(t.element as HTMLElement).offsetParent) return;
    const m = readCellMetrics(t);
    if (!m) return;
    const cellW = m.w;
    const cellH = m.h;
    let visibleHeight = host.clientHeight;
    const vp2 = typeof window !== 'undefined' ? window.visualViewport : null;
    if (vp2) {
      const rect = host.getBoundingClientRect();
      // visualViewport.height is the layout viewport minus the keyboard.
      // rect.bottom is in layout-viewport coordinates. clip to whichever
      // is smaller. offsetTop/pageTop adjustments aren't needed at zoom=1.
      const visibleBottom = Math.min(rect.bottom, vp2.height);
      const clipped = Math.max(0, visibleBottom - rect.top);
      if (clipped > 0) visibleHeight = clipped;
    }
    const cols = Math.max(2, Math.floor(host.clientWidth / cellW));
    const rows = Math.max(1, Math.floor(visibleHeight / cellH));
    if (cols !== t.cols || rows !== t.rows) {
      try {
        t.resize(cols, rows);
      } catch {
        /* ignore */
      }
    }
  }, [termRef, containerRef]);

  // Pure-fit refit triggers. Re-run on session/pane change so a recreated
  // terminal gets the same fonts.ready / settle / ResizeObserver / vpResize
  // coverage the pre-split single effect gave it.
  useEffect(() => {
    const term = termRef.current;
    const ctr = containerRef.current;
    if (!term || !ctr) return;

    let disposed = false;

    // Boolab pattern: term measures cell metrics on open() against whatever
    // font is registered at that instant; if the font hasn't loaded yet we
    // get a fallback measurement and the wrong cols. Refit when
    // document.fonts.ready resolves.
    let fontRaf: number | null = null;
    if (typeof document !== 'undefined' && document.fonts && document.fonts.ready) {
      document.fonts.ready
        .then(() => {
          if (disposed) return;
          fontRaf = requestAnimationFrame(() => fit());
        })
        .catch(() => {
          /* ignore */
        });
    }

    // Second-pass refit: mobile browsers can settle layout slightly late,
    // causing the rAF fit to measure a stale clientWidth.
    const delayedFit = setTimeout(() => fit(), 150);

    // ResizeObserver: refit on any container size change. No debounce —
    // term.onResize itself is the throttle (no-op when cols/rows unchanged).
    const ro = new ResizeObserver(() => {
      fit();
    });
    ro.observe(ctr);

    // v1.10.8d: visualViewport.resize fires when the iOS keyboard opens or
    // closes — the layout viewport stays full-height (so our h-dvh root
    // doesn't shrink), but the visual viewport contracts above the keyboard.
    // Refit so xterm's row count matches the visible area, then scroll to
    // bottom so the cursor stays above the keyboard fold (boolab pattern).
    let onVpResize: (() => void) | null = null;
    const vp = typeof window !== 'undefined' ? window.visualViewport : null;
    if (vp) {
      onVpResize = (): void => {
        if (disposed) return;
        fit();
        const t = termRef.current;
        if (t) {
          try {
            t.scrollToBottom();
          } catch {
            /* ignore */
          }
        }
      };
      vp.addEventListener('resize', onVpResize);
    }

    return () => {
      disposed = true;
      if (fontRaf !== null) cancelAnimationFrame(fontRaf);
      clearTimeout(delayedFit);
      ro.disconnect();
      if (vp && onVpResize) vp.removeEventListener('resize', onVpResize);
    };
    // sessionId/paneId drive re-run on terminal recreation (term is rebuilt by
    // the orchestrator on those deps); fit is stable.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [sessionId, paneId, fit]);

  return { fit, getSize, setSize };
}
