'use client';

import type { ITheme, Terminal } from '@xterm/xterm';
import { useEffect, useRef, useState } from 'react';
import { useTheme } from 'next-themes';

import { Button } from '@spoon/ui';

import { scheduleTerminalFits } from './terminal-fit';

import '@xterm/xterm/css/xterm.css';

const TERMINAL_FONT =
  "var(--font-victor-mono), 'Symbols Nerd Font Mono', 'Geist Mono', ui-monospace, monospace";

// Persisted user preference for copy-on-select. A stored value takes precedence
// over the `copyOnSelect` prop default so the toggle sticks across sessions.
const COPY_ON_SELECT_KEY = 'spoon.terminal.copyOnSelect';

const readStoredCopyOnSelect = (): boolean | undefined => {
  try {
    const stored = window.localStorage.getItem(COPY_ON_SELECT_KEY);
    if (stored === 'true') return true;
    if (stored === 'false') return false;
  } catch {
    // localStorage may be unavailable (private mode, SSR); fall back to prop.
  }
  return undefined;
};

export type XtermStatus =
  | 'idle'
  | 'connecting'
  | 'connected'
  | 'closed'
  | 'error'
  | 'unconfigured';

const darkTheme: ITheme = {
  background: '#080e14',
  foreground: '#eef3f5',
  cursor: '#1fb895',
  cursorAccent: '#080e14',
  selectionBackground: '#1fb89544',
  black: '#10171e',
  red: '#f3625d',
  green: '#8fd6b4',
  yellow: '#e3b341',
  blue: '#6aa6ff',
  magenta: '#b692e8',
  cyan: '#5fd0e0',
  white: '#cdd6dc',
  brightBlack: '#93a1a9',
  brightRed: '#f3625d',
  brightGreen: '#8fd6b4',
  brightYellow: '#e3b341',
  brightBlue: '#6aa6ff',
  brightMagenta: '#b692e8',
  brightCyan: '#5fd0e0',
  brightWhite: '#eef3f5',
};

const lightTheme: ITheme = {
  background: '#f7fbfa',
  foreground: '#0d1218',
  cursor: '#007560',
  cursorAccent: '#f7fbfa',
  selectionBackground: '#00756033',
  black: '#0d1218',
  red: '#d73337',
  green: '#2f8f6e',
  yellow: '#9a6b00',
  blue: '#2f6bd8',
  magenta: '#7c4dd1',
  cyan: '#0f7d92',
  white: '#26323c',
  brightBlack: '#555f68',
  brightRed: '#d73337',
  brightGreen: '#2f8f6e',
  brightYellow: '#9a6b00',
  brightBlue: '#2f6bd8',
  brightMagenta: '#7c4dd1',
  brightCyan: '#0f7d92',
  brightWhite: '#0d1218',
};

export const XtermSession = ({
  active,
  visible = true,
  tokenUrl,
  sessionKey,
  waitingLabel,
  copyOnSelect,
  label = 'workspace shell',
}: {
  active: boolean;
  visible?: boolean;
  tokenUrl: string;
  sessionKey: string;
  waitingLabel?: string;
  copyOnSelect?: boolean;
  label?: string;
}) => {
  const containerRef = useRef<HTMLDivElement>(null);
  const termRef = useRef<Terminal | null>(null);
  const fitRef = useRef<{ fit: () => void } | null>(null);
  const wsRef = useRef<WebSocket | null>(null);
  const { resolvedTheme } = useTheme();
  const themeIsLight = resolvedTheme === 'light';
  const [status, setStatus] = useState<XtermStatus>('idle');
  const [errorText, setErrorText] = useState<string>();
  const [reconnectKey, setReconnectKey] = useState(0);
  // localStorage overrides the prop default; the prop is only the initial value
  // when nothing has been persisted yet.
  const [copyEnabled, setCopyEnabled] = useState<boolean>(
    () => readStoredCopyOnSelect() ?? copyOnSelect ?? false,
  );
  // The connect effect registers onSelectionChange once; it reads the current
  // toggle through this ref so live flips take effect without a reconnect.
  const copyEnabledRef = useRef(copyEnabled);
  useEffect(() => {
    copyEnabledRef.current = copyEnabled;
  }, [copyEnabled]);

  const toggleCopyOnSelect = () => {
    setCopyEnabled((prev) => {
      const next = !prev;
      try {
        window.localStorage.setItem(COPY_ON_SELECT_KEY, String(next));
      } catch {
        // Persistence is best-effort; ignore storage failures.
      }
      return next;
    });
  };

  // Update the live terminal's theme without tearing down the session.
  useEffect(() => {
    if (termRef.current) {
      termRef.current.options.theme = themeIsLight ? lightTheme : darkTheme;
    }
  }, [themeIsLight]);

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

    const abortController = new AbortController();
    const signal = abortController.signal;
    // Read through a function so TS doesn't narrow `aborted` to a constant after
    // the first guard (it changes asynchronously, on cleanup).
    const isAborted = () => signal.aborted;
    let ws: WebSocket | undefined;
    let resizeObserver: ResizeObserver | undefined;
    const encoder = new TextEncoder();

    const start = async () => {
      const [{ Terminal }, { FitAddon }, { WebLinksAddon }] = await Promise.all(
        [
          import('@xterm/xterm'),
          import('@xterm/addon-fit'),
          import('@xterm/addon-web-links'),
        ],
      );
      if (isAborted()) return;

      setStatus('connecting');
      setErrorText(undefined);
      let response: Response;
      try {
        response = await fetch(tokenUrl, { signal });
      } catch {
        if (!isAborted()) setStatus('error');
        return;
      }
      if (isAborted()) return;
      if (response.status === 503) {
        setStatus('unconfigured');
        return;
      }
      if (!response.ok) {
        setStatus('error');
        setErrorText(await response.text().catch(() => undefined));
        return;
      }
      const { url } = (await response.json()) as { url: string };

      const term = new Terminal({
        fontFamily: TERMINAL_FONT,
        fontSize: 13,
        lineHeight: 1.2,
        cursorBlink: true,
        theme: themeIsLight ? lightTheme : darkTheme,
        allowProposedApi: true,
        scrollback: 5000,
      });
      const fit = new FitAddon();
      term.loadAddon(fit);
      term.loadAddon(new WebLinksAddon());
      term.open(container);
      termRef.current = term;
      fitRef.current = fit;

      // Copy-on-select: when enabled, mirror the selection to the clipboard.
      // Registered once; the ref lets live toggle flips apply without a
      // reconnect. Clipboard access is guarded — it may be unavailable or
      // denied, and empty selections are skipped.
      term.onSelectionChange(() => {
        if (!copyEnabledRef.current) return;
        const selection = term.getSelection();
        if (!selection) return;
        try {
          void navigator.clipboard.writeText(selection).catch(() => {
            // clipboard write denied/unavailable; ignore
          });
        } catch {
          // navigator.clipboard missing entirely; ignore
        }
      });

      const sendResize = () => {
        if (ws?.readyState !== WebSocket.OPEN) return;
        ws.send(
          JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }),
        );
      };

      scheduleTerminalFits({
        fit: () => {
          try {
            fit.fit();
          } catch {
            // ignore transient layout errors
          }
        },
        sendResize,
        fontsReady: document.fonts.ready,
        loadNerdFont: () =>
          document.fonts.load("16px 'Symbols Nerd Font Mono'", '').then(() => {
            if (!isAborted()) term.refresh(0, term.rows - 1);
          }),
        raf: (cb) => requestAnimationFrame(cb),
        isAborted,
      });

      ws = new WebSocket(url);
      wsRef.current = ws;
      ws.binaryType = 'arraybuffer';
      ws.onopen = () => {
        if (isAborted()) return;
        setStatus('connected');
        sendResize();
        term.focus();
      };
      ws.onmessage = (event: MessageEvent<ArrayBuffer | string>) => {
        if (typeof event.data === 'string') term.write(event.data);
        else term.write(new Uint8Array(event.data));
      };
      ws.onclose = () => {
        if (!isAborted()) setStatus('closed');
      };
      ws.onerror = () => {
        if (!isAborted()) setStatus('error');
      };

      term.onData((data) => {
        if (ws?.readyState === WebSocket.OPEN) ws.send(encoder.encode(data));
      });
      term.onResize(() => sendResize());
      resizeObserver = new ResizeObserver(() => {
        try {
          fit.fit();
        } catch {
          // ignore transient layout errors
        }
      });
      resizeObserver.observe(container);
    };

    void start();

    return () => {
      abortController.abort();
      resizeObserver?.disconnect();
      ws?.close();
      termRef.current?.dispose();
      termRef.current = null;
      fitRef.current = null;
      wsRef.current = null;
    };
    // resolvedTheme intentionally excluded: handled by the theme effect above.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [active, sessionKey, reconnectKey]);

  // A hidden (display:none) container measures 0×0, so xterm mis-sizes while the
  // tab is inactive. When the terminal becomes visible again, refit inside a
  // frame (so layout has settled) and push the new dimensions to the PTY. The
  // WebSocket stays open across visibility changes — this never reconnects.
  useEffect(() => {
    if (!visible || !active) return;
    const frame = requestAnimationFrame(() => {
      try {
        fitRef.current?.fit();
      } catch {
        // ignore transient layout errors
      }
      const ws = wsRef.current;
      const term = termRef.current;
      if (ws?.readyState === WebSocket.OPEN && term) {
        ws.send(
          JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }),
        );
      }
    });
    return () => cancelAnimationFrame(frame);
  }, [visible, active]);

  return (
    <div className='relative flex h-full min-h-0 flex-col'>
      <div className='border-border flex h-10 flex-none items-center justify-between gap-3 border-b px-3'>
        <p className='text-muted-foreground text-xs'>
          {status === 'connected'
            ? `Connected · ${label}`
            : status === 'idle'
              ? (waitingLabel ? 'Waiting…' : 'Idle')
              : status === 'connecting'
                ? 'Connecting…'
                : status === 'closed'
                  ? 'Session ended'
                  : status === 'unconfigured'
                    ? 'Terminal not configured'
                    : 'Connection error'}
        </p>
        <div className='flex items-center gap-3'>
          <label className='text-muted-foreground flex cursor-pointer items-center gap-1.5 text-xs select-none'>
            <input
              type='checkbox'
              className='accent-primary size-3.5'
              checked={copyEnabled}
              onChange={toggleCopyOnSelect}
            />
            Copy on select
          </label>
          {status === 'closed' || status === 'error' ? (
            <Button
              type='button'
              variant='outline'
              size='sm'
              onClick={() => setReconnectKey((key) => key + 1)}
            >
              Reconnect
            </Button>
          ) : null}
        </div>
      </div>
      {status === 'unconfigured' ? (
        <div className='text-muted-foreground flex flex-1 items-center justify-center p-6 text-center text-sm'>
          The terminal is not enabled on this deployment.
          <br />
          Set NEXT_PUBLIC_SPOON_AGENT_WORKER_WS_URL to enable it.
        </div>
      ) : !active && waitingLabel ? (
        <div className='text-muted-foreground flex flex-1 items-center justify-center p-6 text-center text-sm'>
          {waitingLabel}
        </div>
      ) : (
        <div className='min-h-0 flex-1 overflow-hidden bg-[#080e14] p-2'>
          <div ref={containerRef} className='h-full w-full' />
          {errorText ? (
            <p className='text-destructive mt-2 px-1 text-xs break-all'>
              {errorText}
            </p>
          ) : null}
        </div>
      )}
    </div>
  );
};
