import { useEffect, useRef } from 'react';
import { Terminal } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';
import { WebglAddon } from '@xterm/addon-webgl';
import type { PaneDefinition, WorkspaceDefinition } from '@/types/workbench';
import {
  buildVigthoriaEnv,
  defaultShell,
  onPtyExit,
  onPtyOutput,
  packageStartCommand,
  platform,
  ptyKill,
  ptyResize,
  ptySpawn,
  ptyWrite,
} from '@/lib/tauri-bridge';
import { buildCdCommand, resolvePaneCwd } from '@/lib/shell-utils';
import { isPlaceholderRoot } from '@/lib/workspace-path';
import { extractDeckEventsFromChunk } from '@/lib/deck-events';
import { stripGoaFromChunk } from '@/lib/goa-events';
import { extractV4RuntimeEvents } from '@/lib/v4-runtime-events';
import { createThinkTagState, stripThinkTagsFromChunk } from '@/lib/think-tags';
import { useWorkbenchStore } from '@/features/workspace/WorkspaceStore';
import '@xterm/xterm/css/xterm.css';

interface XTermPaneProps {
  pane: PaneDefinition;
  workspace: WorkspaceDefinition;
  registerMainAgent?: boolean;
}

function lineEnding(osPlatform: string): string {
  return osPlatform === 'windows' ? '\r\n' : '\n';
}

/** Below this, a measurement is almost certainly a transient layout glitch
 * (mid-drag sash, hidden pane, webview not yet painted) — never propagate it
 * to xterm or the PTY, or the shell's own line editor will wrap every
 * keystroke onto its own row (Windows cmd/PowerShell are especially prone to
 * this once told the console is 1 column wide). */
const MIN_TERMINAL_COLS = 30;
const MIN_TERMINAL_ROWS = 4;

/** Fit the terminal to its container, but never accept an implausibly small
 * result — fall back to the last known-good size (or the provided defaults)
 * instead so a bad measurement can't wrap the whole terminal to one column. */
function clampFit(
  term: Terminal,
  fitAddon: FitAddon,
  fallbackCols: number,
  fallbackRows: number,
): { cols: number; rows: number } {
  fitAddon.fit();
  if (term.cols < MIN_TERMINAL_COLS || term.rows < MIN_TERMINAL_ROWS) {
    const safeCols = Math.max(fallbackCols, MIN_TERMINAL_COLS);
    const safeRows = Math.max(fallbackRows, MIN_TERMINAL_ROWS);
    term.resize(safeCols, safeRows);
  }
  return { cols: term.cols, rows: term.rows };
}

/** Same as clampFit, but additionally re-measures one animation frame later
 * and only trusts the result if both measurements agree. A single fit() can
 * still report a momentarily-too-narrow value while xterm's cell-width metric
 * is settling (confirmed to reproduce live: a shell prompt whose subsequent
 * keystrokes each render on their own new line, as if told the console is ~1
 * column wide) -- this is exactly the class of glitch clampFit's own MIN_*
 * floor cannot catch, since a plausible-but-still-wrong reading (e.g. 20-25
 * cols when the pane actually renders far wider) passes that floor check.
 * Disagreement between the two reads falls back to the last known-good size
 * instead of trusting either potentially-transient measurement. */
async function stableClampFit(
  term: Terminal,
  fitAddon: FitAddon,
  fallbackCols: number,
  fallbackRows: number,
): Promise<{ cols: number; rows: number }> {
  const first = clampFit(term, fitAddon, fallbackCols, fallbackRows);
  await new Promise<void>((resolve) => window.requestAnimationFrame(() => resolve()));
  const second = clampFit(term, fitAddon, fallbackCols, fallbackRows);
  if (second.cols !== first.cols || second.rows !== first.rows) {
    if (fallbackCols >= MIN_TERMINAL_COLS && fallbackRows >= MIN_TERMINAL_ROWS) {
      term.resize(fallbackCols, fallbackRows);
      return { cols: fallbackCols, rows: fallbackRows };
    }
  }
  return second;
}

/** Wait until the pane size stops changing (maximize / allotment drag finished). */
async function waitForStableTerminalLayout(
  container: HTMLElement,
  fitAddon: FitAddon,
  term: Terminal,
  stableMs = 450,
  maxMs = 8000,
): Promise<void> {
  // A stable container pixel size is not sufficient on its own: xterm's own
  // cell-width metric (used to derive cols from pixel width) can still be
  // settling in during/just after the terminal's first paint, producing a
  // momentarily-too-narrow cols value that only self-corrects a frame later
  // (confirmed empirically -- content written before this self-correction
  // stays permanently mis-wrapped even though a later resize renders fine).
  // Waiting for fonts to be ready first removes one common source of that
  // late metric change.
  try {
    if (typeof document !== 'undefined' && document.fonts?.ready) {
      await document.fonts.ready;
    }
  } catch {
    // Font readiness isn't critical -- fall through to the fit-stability loop.
  }

  const start = Date.now();
  let lastW = 0;
  let lastH = 0;
  let lastCols = 0;
  let lastRows = 0;
  let stableSince = 0;

  while (Date.now() - start < maxMs) {
    fitAddon.fit();
    const w = container.clientWidth;
    const h = container.clientHeight;
    const now = Date.now();

    if (w > 40 && h > 40 && term.cols >= MIN_TERMINAL_COLS && term.rows >= MIN_TERMINAL_ROWS) {
      if (w === lastW && h === lastH && term.cols === lastCols && term.rows === lastRows) {
        if (stableSince === 0) stableSince = now;
        if (now - stableSince >= stableMs) return;
      } else {
        stableSince = 0;
      }
    }

    lastW = w;
    lastH = h;
    lastCols = term.cols;
    lastRows = term.rows;
    await new Promise<void>((resolve) => {
      window.requestAnimationFrame(() => resolve());
    });
  }

  // Timed out without a stable, sane measurement — never leave the terminal
  // sized to whatever tiny value the last in-progress layout produced.
  clampFit(term, fitAddon, 80, 24);
}

async function resizePtySafely(ptyId: string, cols: number, rows: number): Promise<void> {
  try {
    await ptyResize(ptyId, cols, rows);
  } catch (err) {
    console.warn('[XTermPane] ptyResize failed, retrying once', err);
    try {
      await ptyResize(ptyId, cols, rows);
    } catch (retryErr) {
      console.warn('[XTermPane] ptyResize retry failed; PTY size may be out of sync', retryErr);
    }
  }
}

async function syncPtySize(
  ptyId: string,
  fitAddon: FitAddon,
  term: Terminal,
  fallbackCols = 80,
  fallbackRows = 24,
): Promise<void> {
  const { cols, rows } = clampFit(term, fitAddon, fallbackCols, fallbackRows);
  await resizePtySafely(ptyId, cols, rows);
}

export function XTermPane({ pane, workspace, registerMainAgent }: XTermPaneProps) {
  const containerRef = useRef<HTMLDivElement>(null);
  const terminalRef = useRef<Terminal | null>(null);
  const fitRef = useRef<FitAddon | null>(null);
  const ptyIdRef = useRef<string | null>(null);
  const setActiveSessions = useWorkbenchStore((s) => s.setActiveSessions);
  const setMainAgentPtyId = useWorkbenchStore((s) => s.setMainAgentPtyId);
  const updateSessionActivity = useWorkbenchStore((s) => s.updateSessionActivity);
  const handleDeckEvent = useWorkbenchStore((s) => s.handleDeckEvent);
  const refreshAuth = useWorkbenchStore((s) => s.refreshAuth);
  const handleV4RuntimeEvent = useWorkbenchStore((s) => s.handleV4RuntimeEvent);
  const settings = useWorkbenchStore((s) => s.settings);
  const activeSessions = useWorkbenchStore((s) => s.activeSessions);

  useEffect(() => {
    const container = containerRef.current;
    if (!container) return;

    let disposed = false;
    const unsubs: Array<() => void> = [];
    let resizeTimer: number | undefined;
    let resizeLeadingSyncing = false;
    let lastCols = 0;
    let lastRows = 0;
    let startupCommandRef = '';
    let startupSent = false;
    let userTyped = false;
    let v4Carry = '';
    let deckCarry = '';
    let goaCarry = '';
    const thinkTagState = createThinkTagState();

    const boot = async () => {
      try {
        const osPlatform = await platform();
        const eol = lineEnding(osPlatform);

        const term = new Terminal({
          cursorBlink: true,
          fontFamily: '"Cascadia Code", "JetBrains Mono", Consolas, monospace',
          fontSize: 13,
          theme: {
            background: '#0d1117',
            foreground: '#e6edf3',
            cursor: '#58a6ff',
            selectionBackground: '#264f78',
            black: '#484f58',
            red: '#ff7b72',
            green: '#3fb950',
            yellow: '#d29922',
            blue: '#58a6ff',
            magenta: '#bc8cff',
            cyan: '#39c5cf',
            white: '#e6edf3',
          },
          scrollback: 10000,
        });

        const fitAddon = new FitAddon();
        term.loadAddon(fitAddon);
        // Canvas renderer reflows more reliably on Windows maximize/resize than WebGL.
        if (osPlatform !== 'windows') {
          try {
            term.loadAddon(new WebglAddon());
          } catch {
            // Canvas fallback is fine
          }
        }

        term.open(container);
        await waitForStableTerminalLayout(container, fitAddon, term);
        terminalRef.current = term;
        fitRef.current = fitAddon;

        const shell = await defaultShell();
        let cols = term.cols;
        let rows = term.rows;
        lastCols = cols;
        lastRows = rows;

        const env: Record<string, string> = {};
        if (pane.role === 'mainAgent') {
          Object.assign(env, buildVigthoriaEnv(workspace.rootPath, workspace.vigthoria, settings));
        }

        const projectRoot = workspace.rootPath.trim();
        if (isPlaceholderRoot(projectRoot)) {
          term.writeln('\r\n\x1b[38;5;214m[workspace needs a project folder — use sidebar Apply]\x1b[0m');
          return;
        }
        const cwd = resolvePaneCwd(pane.cwd, projectRoot) || projectRoot;
        const ptyId = await ptySpawn({
          cwd,
          shell,
          cols,
          rows,
          env,
        });
        if (disposed) {
          await ptyKill(ptyId);
          return;
        }

        ptyIdRef.current = ptyId;
        if (registerMainAgent && pane.role === 'mainAgent') {
          setMainAgentPtyId(ptyId);
        }

        // Cold-start safety net: on a freshly created (and, on Windows,
        // sometimes briefly not-yet-composited) window, xterm's very first
        // cell-width/font measurement can still be transiently wrong even
        // after waitForStableTerminalLayout reports a "stable" size -- the
        // container's CSS box can be correct while the actual glyph metrics
        // used to derive cols/rows are not yet final (GPU/webview first
        // paint still settling). That produces a plausible-but-wrong cols
        // value nothing else here catches, so the shell wraps every one of
        // the user's first keystrokes onto its own line. Re-measuring a
        // couple of times shortly after spawn -- independent of the
        // ResizeObserver, which only fires on an actual size *change* -- both
        // resyncs the PTY and (via xterm's own resize/reflow) repairs any
        // stairstep output already written before the correction landed.
        for (const delayMs of [400, 1200]) {
          window.setTimeout(() => {
            if (disposed) return;
            void (async () => {
              const { cols: colsNow, rows: rowsNow } = clampFit(term, fitAddon,
lastCols, lastRows);
              if (colsNow !== lastCols || rowsNow !== lastRows) {
                await resizePtySafely(ptyId, colsNow, rowsNow);
                lastCols = colsNow;
                lastRows = rowsNow;
              }
            })();
          }, delayMs);
        }

        const cdCommand =
          osPlatform === 'windows' ? '' : buildCdCommand(cwd, shell, osPlatform);
        let startupCommand = pane.startupCommand;
        if (pane.role === 'dev' && pane.startupCommand === 'npm run dev') {
          startupCommand = (await packageStartCommand(projectRoot)) ?? undefined;
          if (!startupCommand) {
            term.writeln('\r\n\x1b[38;5;214m[dev server not started]\x1b[0m');
            term.writeln('This project has no npm dev/start/serve/preview script in package.json.');
            term.writeln('Add a script or use this pane as a normal shell.');
          }
        }
        startupCommandRef = startupCommand ?? '';

        const sendStartup = async (clearScreen: boolean) => {
          if (!cdCommand && !startupCommand) return;
          await waitForStableTerminalLayout(container, fitAddon, term, 200, 3000);
          await syncPtySize(ptyId, fitAddon, term, lastCols, lastRows);
          lastCols = term.cols;
          lastRows = term.rows;

          if (osPlatform === 'windows') {
            // Empirically, Windows console-aware CLIs launched in this pane
            // can end up drawing box UI a few columns wider than xterm.js's
            // true rendered width (root cause looks like a stale/racy
            // ConPTY resize notification reaching the child's console width
            // query) — the box's own right border then lands past the
            // actual edge and wraps, fragmenting into floating pieces.
            // Re-issuing the resize after a longer settle delay helps but
            // isn't sufficient on its own, so on top of that we also report
            // a slightly NARROWER width than xterm's real fit (safety
            // margin) for the size the child process will observe. This
            // guarantees any box UI sized off that value always fits
            // inside the true (wider-or-equal) rendered width, independent
            // of whatever residual timing race remains.
            const safetyMarginCols = 6;
            const narrowedCols = Math.max(MIN_TERMINAL_COLS, term.cols - safetyMarginCols);
            await new Promise((r) => window.setTimeout(r, 400));
            await resizePtySafely(ptyId, narrowedCols, term.rows);
            await new Promise((r) => window.setTimeout(r, 150));
          }

          if (clearScreen) {
            term.clear();
            if (osPlatform === 'windows') {
              await ptyWrite(ptyId, 'cls\r');
              await new Promise((r) => window.setTimeout(r, 120));
            }
          }

          const startupScript = cdCommand
            ? startupCommand
              ? `${cdCommand}${eol}${startupCommand}${eol}`
              : `${cdCommand}${eol}`
            : `${startupCommand}${eol}`;
          await ptyWrite(ptyId, startupScript);
          startupSent = true;
        };

        await sendStartup(false);

        const nextSessions = [
          ...activeSessions.filter((s) => s.paneId !== pane.id),
          {
            paneId: pane.id,
            ptyId,
            role: pane.role,
            title: pane.title,
            isMain: pane.isMain,
            active: false,
          },
        ];
        setActiveSessions(nextSessions);

        const outputUnsub = await onPtyOutput((event) => {
          if (event.id !== ptyId) return;
          const visibleRaw = pane.role === 'mainAgent'
            ? (() => {
              const extracted = extractDeckEventsFromChunk(event.data, deckCarry, handleDeckEvent);
              deckCarry = extracted.carry;
              return extracted.visible;
            })()
            : event.data;
          const goaVisible = pane.role === 'mainAgent'
            ? (() => {
              const extracted = stripGoaFromChunk(visibleRaw, goaCarry);
              goaCarry = extracted.carry;
              return extracted.visible;
            })()
            : visibleRaw;
          const thinkFreeVisible = pane.role === 'mainAgent'
            ? stripThinkTagsFromChunk(goaVisible, thinkTagState)
            : goaVisible;

          const finalVisible = pane.role === 'mainAgent'
            ? (() => {
              const extracted = extractV4RuntimeEvents(thinkFreeVisible, v4Carry);
              v4Carry = extracted.carry;
              for (const runtimeEvent of extracted.events) {
                handleV4RuntimeEvent(runtimeEvent);
              }
              return extracted.visible;
            })()
            : goaVisible;

          if (finalVisible) {
            term.write(finalVisible);
          }
          if (
            pane.role === 'mainAgent'
            && /logged in successfully|logged in as|account:|authentication failed|authentification failed|session expired|log out and login again/i.test(event.data)
          ) {
            window.setTimeout(() => {
              void refreshAuth(true);
            }, 750);
          }
          updateSessionActivity(ptyId, true);
          window.setTimeout(() => updateSessionActivity(ptyId, false), 1200);
        });
        unsubs.push(outputUnsub);

        const exitUnsub = await onPtyExit((event) => {
          if (event.id !== ptyId) return;
          term.writeln('\r\n\x1b[38;5;214m[session ended]\x1b[0m');
        });
        unsubs.push(exitUnsub);

        term.onData((data) => {
          userTyped = true;
          void ptyWrite(ptyId, data);
        });

        // Debounced (trailing-edge) resize alone leaves a window -- as long as
        // the debounce delay -- where xterm.js has already re-measured/redrawn
        // at the new size but the real PTY (and the shell attached to it) is
        // still at the OLD size. Windows cmd/PowerShell redraw the in-progress
        // input line using their own console-buffer width, so any keystroke
        // typed during that window gets echoed wrapped at the stale width --
        // exactly like the shell thinks the terminal is much narrower than it
        // visually is. Firing an immediate PTY resize on the *leading* edge
        // (guarded so overlapping observer callbacks don't pile up calls)
        // closes that window almost entirely, while the trailing debounced
        // call still runs afterwards to settle on the final size and replay
        // the startup command if the layout changed significantly.
        const scheduleResize = () => {
          if (!resizeLeadingSyncing) {
            resizeLeadingSyncing = true;
            void (async () => {
              try {
                const { cols: colsNow, rows: rowsNow } = await stableClampFit(term, fitAddon, lastCols, lastRows);
                await resizePtySafely(ptyId, colsNow, rowsNow);
                lastCols = colsNow;
                lastRows = rowsNow;
              } finally {
                resizeLeadingSyncing = false;
              }
            })();
          }

          if (resizeTimer) window.clearTimeout(resizeTimer);
          resizeTimer = window.setTimeout(() => {
            void (async () => {
              const { cols: colsNow, rows: rowsNow } = clampFit(term, fitAddon, lastCols, lastRows);
              await resizePtySafely(ptyId, colsNow, rowsNow);

              const colsDelta = Math.abs(colsNow - lastCols);
              const rowsDelta = Math.abs(rowsNow - lastRows);
              const significant = colsDelta >= 6 || rowsDelta >= 2;

              if (
                significant
                && startupSent
                && startupCommandRef
                && pane.role === 'mainAgent'
                && !userTyped
              ) {
                await sendStartup(true);
              }

              lastCols = colsNow;
              lastRows = rowsNow;
            })();
          }, 200);
        };

        const resizeObserver = new ResizeObserver(scheduleResize);
        resizeObserver.observe(container);
        unsubs.push(() => resizeObserver.disconnect());

        window.addEventListener('resize', scheduleResize);
        unsubs.push(() => window.removeEventListener('resize', scheduleResize));
        unsubs.push(() => {
          if (resizeTimer) window.clearTimeout(resizeTimer);
        });
      } catch (err: unknown) {
        const message = err instanceof Error ? err.message : String(err);
        if (terminalRef.current) {
          terminalRef.current.writeln(`\r\n\x1b[38;5;203m[terminal failed to start]\x1b[0m`);
          terminalRef.current.writeln(message);
        } else if (containerRef.current) {
          containerRef.current.textContent = `Terminal failed to start: ${message}`;
        }
      }
    };

    void boot();

    return () => {
      disposed = true;
      for (const unsub of unsubs) unsub();
      const ptyId = ptyIdRef.current;
      if (registerMainAgent && pane.role === 'mainAgent') {
        setMainAgentPtyId(null);
      }
      if (ptyId) void ptyKill(ptyId);
      terminalRef.current?.dispose();
      terminalRef.current = null;
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [pane.id, workspace.id, workspace.rootPath, workspace.updatedAt]);

  return (
    <div className={`xterm-pane ${pane.isMain ? 'xterm-pane--main' : ''}`}>
      <div className="xterm-pane__header">
        <span className="xterm-pane__title">
          {pane.isMain ? '★ ' : ''}
          {pane.title}
        </span>
        <span className="xterm-pane__badge">{pane.role}</span>
      </div>
      <div ref={containerRef} className="xterm-pane__terminal" />
    </div>
  );
}
