import { useCallback, useEffect, useRef, useState } from 'react';
import type { Terminal } from '@xterm/xterm';
import { api } from '@/api/client';
import { sendToTerminal } from '@/lib/events';
import { encodeInput, encodeResize, parseServerFrame } from '@/lib/terminal-protocol';
import type { TerminalFit } from './useTerminalFit';

// useTerminalSocket — owns the WebSocket lifecycle (connect / backoff
// reconnect / manual reconnect), the terminal's onData + onResize handlers,
// the bootstrap (init rAF: fit → /start → connect), the visibilitychange
// reconnect-or-refit, the sendToTerminal subscription, and sticky Ctrl.
//
// It calls the fit hook's `getSize()` LAZILY (never captures a size value), so
// every WS open / init / resize carries the currently-measured size, not a
// stale one — preserving the read-latest contract.

export type ConnState = 'connecting' | 'open' | 'reconnecting' | 'disconnected';

const MAX_RECONNECT_ATTEMPTS = 5;
const RECONNECT_DELAYS_MS = [500, 1000, 2000, 4000, 8000];

interface SocketDeps {
  termRef: React.MutableRefObject<Terminal | null>;
  sessionId: string;
  paneId: string;
  description?: string;
  parentAgent?: string;
  fit: TerminalFit['fit'];
  getSize: TerminalFit['getSize'];
  setSize: TerminalFit['setSize'];
}

export interface TerminalSocket {
  connState: ConnState;
  send: (text: string) => void;
  reconnect: () => void;
  ctrlArmed: boolean;
  armCtrl: () => void;
}

export function useTerminalSocket({
  termRef,
  sessionId,
  paneId,
  description,
  parentAgent,
  fit,
  getSize,
  setSize,
}: SocketDeps): TerminalSocket {
  const wsRef = useRef<WebSocket | null>(null);
  const reconnectTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const reconnectAttemptRef = useRef(0);
  const closedByUserRef = useRef(false);
  const reconnectFnRef = useRef<() => void>(() => {});

  const [connState, setConnState] = useState<ConnState>('connecting');

  // v1.10.8d: sticky Ctrl for the on-screen hotkey bar (boolab pattern).
  // ctrlArmedRef is read synchronously inside term.onData (per keystroke);
  // ctrlArmed state mirror exists so the Ctrl button can highlight in React.
  // Auto-disarms after 5s so a stray tap doesn't quietly mangle the next
  // keystroke minutes later.
  const [ctrlArmed, setCtrlArmed] = useState(false);
  const ctrlArmedRef = useRef(false);
  const ctrlTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const setCtrlArmedSync = useCallback((armed: boolean) => {
    ctrlArmedRef.current = armed;
    setCtrlArmed(armed);
    if (ctrlTimerRef.current) {
      clearTimeout(ctrlTimerRef.current);
      ctrlTimerRef.current = null;
    }
    if (armed) {
      ctrlTimerRef.current = setTimeout(() => {
        ctrlArmedRef.current = false;
        setCtrlArmed(false);
        ctrlTimerRef.current = null;
      }, 5000);
    }
  }, []);
  const armCtrl = useCallback(() => {
    setCtrlArmedSync(!ctrlArmedRef.current);
  }, [setCtrlArmedSync]);

  const send = useCallback((text: string) => {
    if (!text) return;
    const ws = wsRef.current;
    if (!ws || ws.readyState !== WebSocket.OPEN) return;
    try {
      ws.send(encodeInput(text));
    } catch {
      /* ignore */
    }
  }, []);

  const reconnect = useCallback(() => {
    reconnectFnRef.current();
  }, []);

  useEffect(() => {
    const term = termRef.current;
    if (!term) return;

    let disposed = false;
    closedByUserRef.current = false;
    reconnectAttemptRef.current = 0;

    // boolab pattern: term.onResize fires synchronously inside term.resize()
    // and on init. We always update lastSizeRef so a fresh WS connect (or
    // reconnect) carries the current measured size; we send a resize frame
    // when the WS is open. No gating by an "is-started" flag — the WS itself
    // is the gate (it ignores writes when not open).
    term.onResize(({ cols, rows }) => {
      setSize(cols, rows);
      if (cols < 2 || rows < 1) return;
      const ws = wsRef.current;
      if (!ws || ws.readyState !== WebSocket.OPEN) return;
      try {
        ws.send(encodeResize(cols, rows));
      } catch {
        /* ignore */
      }
    });

    // Keystrokes go out as a BINARY frame so the server can disambiguate them
    // from JSON control frames. TextEncoder is in every modern browser.
    //
    // Sticky Ctrl: when armed (via the hotkey bar), apply Ctrl to the next
    // single ASCII printable (0x40-0x7e — '@', A-Z, [, \, ], ^, _, `, a-z,
    // {, |, }, ~) and disarm. Multi-byte sequences (arrows, Esc, F-keys) pass
    // through untouched and disarm so a stuck arm doesn't poison them.
    term.onData((data) => {
      const ws = wsRef.current;
      if (!ws || ws.readyState !== WebSocket.OPEN) return;
      let out = data;
      if (ctrlArmedRef.current) {
        if (data.length === 1) {
          const c = data.charCodeAt(0);
          if (c >= 0x40 && c <= 0x7e) {
            out = String.fromCharCode(c & 0x1f);
          }
        }
        setCtrlArmedSync(false);
      }
      try {
        ws.send(encodeInput(out));
      } catch {
        /* ignore */
      }
    });

    function buildWsUrl(): string {
      const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
      const { cols, rows } = getSize();
      return `${proto}//${window.location.host}/ws/term/sessions/${sessionId}/panes/${paneId}?cols=${cols}&rows=${rows}`;
    }

    function scheduleReconnect(): void {
      if (disposed || closedByUserRef.current) return;
      const attempt = reconnectAttemptRef.current;
      if (attempt >= MAX_RECONNECT_ATTEMPTS) {
        setConnState('disconnected');
        return;
      }
      const delay = RECONNECT_DELAYS_MS[Math.min(attempt, RECONNECT_DELAYS_MS.length - 1)]!;
      reconnectAttemptRef.current = attempt + 1;
      if (reconnectTimerRef.current) clearTimeout(reconnectTimerRef.current);
      reconnectTimerRef.current = setTimeout(connect, delay);
    }

    function connect(): void {
      if (disposed || closedByUserRef.current) return;
      const existing = wsRef.current;
      if (existing && existing.readyState !== WebSocket.CLOSED) return;
      let ws: WebSocket;
      try {
        ws = new WebSocket(buildWsUrl());
      } catch {
        scheduleReconnect();
        return;
      }
      ws.binaryType = 'arraybuffer';
      wsRef.current = ws;

      ws.addEventListener('open', () => {
        if (disposed) return;
        reconnectAttemptRef.current = 0;
        setConnState('open');
        // boolab pattern: send our currently-measured size as a resize frame
        // on every WS open. Server's PTY adopts it immediately; no race
        // window between attach and resize.
        const { cols, rows } = getSize();
        if (cols >= 2 && rows >= 1) {
          try {
            ws.send(encodeResize(cols, rows));
          } catch {
            /* ignore */
          }
        }
      });

      ws.addEventListener('message', (e) => {
        const t = termRef.current;
        if (!t) return;
        if (typeof e.data === 'string') {
          const frame = parseServerFrame(e.data);
          if (frame?.type === 'init') {
            // boolab pattern: wipe any pre-attach buffer state, then the
            // server's next frame (a single binary capture-pane replay)
            // will paint the current tmux pane state in cleanly. Re-send
            // our measured size in case the server's init size (from the
            // WS query string) is stale relative to a fit that landed
            // between opening and now.
            try {
              t.clear();
            } catch {
              /* ignore */
            }
            const { cols, rows } = getSize();
            if (cols >= 2 && rows >= 1) {
              try {
                ws.send(encodeResize(cols, rows));
              } catch {
                /* ignore */
              }
            }
            return;
          }
          if (frame?.type === 'exit') {
            t.write(`\r\n\x1b[2m[process exited with code ${frame.code}]\x1b[0m\r\n`);
            return;
          }
          if (frame?.type === 'pty_exited') {
            if (frame.timed_out) {
              t.write('\r\n\x1b[2m[process timed out and was killed]\x1b[0m\r\n');
            } else {
              t.write(`\r\n\x1b[2m[process exited with code ${frame.exit_code}]\x1b[0m\r\n`);
            }
            if (frame.last_lines.length > 0) {
              t.write(frame.last_lines[frame.last_lines.length - 1] + '\r\n');
            }
            return;
          }
          t.write(e.data);
        } else {
          t.write(new Uint8Array(e.data as ArrayBuffer));
        }
      });

      ws.addEventListener('close', () => {
        if (disposed) return;
        wsRef.current = null;
        if (closedByUserRef.current) return;
        setConnState('reconnecting');
        scheduleReconnect();
      });

      ws.addEventListener('error', () => {
        // close fires after; let close own the retry.
      });
    }

    function manualReconnect(): void {
      if (disposed) return;
      closedByUserRef.current = false;
      reconnectAttemptRef.current = 0;
      if (reconnectTimerRef.current) {
        clearTimeout(reconnectTimerRef.current);
        reconnectTimerRef.current = null;
      }
      try {
        wsRef.current?.close();
      } catch {
        /* ignore */
      }
      setConnState('reconnecting');
      setTimeout(connect, 50);
    }
    reconnectFnRef.current = manualReconnect;

    // Init: rAF-defer one frame so xterm's render service can populate
    // _renderService.dimensions, then fit → /start (idempotent, sizes
    // the tmux session) → connect WS. The WS handler also ensureSession's
    // as belt-and-suspenders, so a /start failure is non-fatal.
    const initRaf = requestAnimationFrame(() => {
      if (disposed) return;
      fit();
      const { cols, rows } = getSize();
      api.terminals
        .start(sessionId, paneId, cols, rows, description, parentAgent)
        .catch(() => {
          /* WS handler will ensureSession itself — non-fatal */
        })
        .finally(() => {
          if (disposed) return;
          connect();
        });
    });

    const onVis = (): void => {
      if (document.visibilityState !== 'visible') return;
      const ws = wsRef.current;
      if (!ws || ws.readyState === WebSocket.CLOSED || ws.readyState === WebSocket.CLOSING) {
        manualReconnect();
      } else {
        // Refit in case the layout changed while hidden. Single rAF so the
        // measurement runs after the tab's first composited frame back in
        // the foreground.
        requestAnimationFrame(() => {
          if (disposed) return;
          fit();
        });
      }
    };
    document.addEventListener('visibilitychange', onVis);

    const unsubscribe = sendToTerminal.subscribe(({ pane_id, text }) => {
      if (pane_id !== paneId) return;
      const ws = wsRef.current;
      if (!ws || ws.readyState !== WebSocket.OPEN) return;
      const payload = text.endsWith('\n') ? text : `${text}\n`;
      try {
        ws.send(encodeInput(payload));
      } catch {
        /* ignore */
      }
    });

    return () => {
      disposed = true;
      closedByUserRef.current = true;
      cancelAnimationFrame(initRaf);
      if (reconnectTimerRef.current) {
        clearTimeout(reconnectTimerRef.current);
        reconnectTimerRef.current = null;
      }
      if (ctrlTimerRef.current) {
        clearTimeout(ctrlTimerRef.current);
        ctrlTimerRef.current = null;
      }
      document.removeEventListener('visibilitychange', onVis);
      unsubscribe();
      const ws = wsRef.current;
      if (ws) {
        try {
          ws.close(1000, 'unmount');
        } catch {
          /* ignore */
        }
        wsRef.current = null;
      }
      // term.onResize / term.onData disposables are cleaned up by term.dispose()
      // (in the orchestrator's spine effect), matching the pre-split teardown.
      reconnectFnRef.current = () => {};
    };
    // fit/getSize/setSize are stable; sessionId/paneId drive reconnect-on-change
    // and term-handler rebinding after the orchestrator recreates the terminal.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [sessionId, paneId, fit, getSize, setSize]);

  return { connState, send, reconnect, ctrlArmed, armCtrl };
}
