import type { CSSProperties } from 'react';
import { useEffect, useRef, useState } from 'react';
import { useClient } from 'hooks/useClient';
import { useMounted } from 'hooks/useMounted';

export type DesktopViewProps = {
  className?: string;
  style?: CSSProperties;
};

// The BUNDLE, not public/novnc/rfb.js. That entry is unbundled noVNC source: 42 ES modules whose
// imports nest six levels deep, so a native dynamic import made the browser walk the graph — fetch,
// parse, discover the next level, fetch again. Six sequential round trips and 42 requests before the
// desktop could even begin connecting, which over the tailnet is seconds, and a hard refresh pays it
// every time. One file is one round trip.
//
// Regenerate after updating vendored noVNC:
//   bun build public/novnc/rfb.js --outfile=public/novnc/rfb.bundle.js --format=esm --minify --target=browser
const NOVNC_URL = '/novnc/rfb.bundle.js';

const buildWsUrl = () => {
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
  const token = localStorage.getItem('BEARER_TOKEN') ?? '';
  return `${protocol}//${window.location.host}/api/desktop/ws?token=${encodeURIComponent(token)}`;
};

type RFBInstance = {
  resizeSession: boolean;
  scaleViewport: boolean;
  showDotCursor: boolean;
  focusOnClick: boolean;
  disconnect: () => void;
  sendCredentials: (creds: { password: string }) => void;
  addEventListener: (type: string, listener: (ev: CustomEvent) => void) => void;
};

let rfbModulePromise: Promise<{
  default: new (target: HTMLElement, url: string, options?: { credentials?: { password?: string } }) => RFBInstance;
}> | null = null;

const loadRFB = () => {
  if (!rfbModulePromise) {
    rfbModulePromise = import(/* @vite-ignore */ NOVNC_URL) as NonNullable<typeof rfbModulePromise>;
  }
  return rfbModulePromise!;
};

export const DesktopView = ({ className, style }: DesktopViewProps) => {
  const containerRef = useRef<HTMLDivElement>(null);
  const rfbRef = useRef<RFBInstance | null>(null);
  const isMounted = useMounted();
  const client = useClient();
  const [status, setStatus] = useState<'connecting' | 'connected' | 'reconnecting' | 'disconnected' | 'error'>(
    'connecting',
  );
  const [errorMsg, setErrorMsg] = useState('');

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

    let disposed = false;
    let attempts = 0;
    let retryTimer: ReturnType<typeof setTimeout> | null = null;
    // Tearing an RFB down makes it fire its own `disconnect`, and a bad password fires `securityfailure`
    // and then `disconnect` too. Both would otherwise be read as "officer went away, reattach".
    let generation = 0;
    let fatal = false;
    const MAX_ATTEMPTS = 5;
    const RETRY_DELAYS = [1000, 2000, 3000, 5000, 5000];

    const detach = () => {
      const rfb = rfbRef.current;
      rfbRef.current = null;
      generation++;
      if (!rfb) return;
      try {
        rfb.disconnect();
      } catch {
        /* ignore */
      }
    };

    // x11vnc mirrors :0 with `-forever`, so the desktop itself outlives this socket — losing it means
    // officer restarted under us, not that the session ended. Reattach instead of parking on
    // "Disconnected" until someone reopens the panel.
    const retry = (reason: string) => {
      if (disposed || fatal || retryTimer) return;
      detach();
      if (attempts >= MAX_ATTEMPTS) {
        setStatus('disconnected');
        setErrorMsg(reason);
        return;
      }
      const delay = RETRY_DELAYS[attempts] ?? 5000;
      attempts++;
      setStatus('reconnecting');
      setErrorMsg(`${reason} — reconnecting (${attempts}/${MAX_ATTEMPTS})...`);
      retryTimer = setTimeout(() => {
        retryTimer = null;
        void connect();
      }, delay);
    };

    const connect = async () => {
      const gen = ++generation;
      const isCurrent = () => !disposed && gen === generation;
      let password = '';
      try {
        const res = await client.get<{ password: string }>('/desktop/vnc-password');
        password = res.password;
      } catch {
        if (!isCurrent()) return;
        // Officer being down is the common case here, and it comes back — so this is a retry, not a
        // dead end. A sidecar that is genuinely missing still ends up at "Disconnected" after five.
        retry('Failed to fetch VNC password');
        return;
      }

      if (!isCurrent()) return;

      let RFB: Awaited<ReturnType<typeof loadRFB>>['default'];
      try {
        const mod = await loadRFB();
        RFB = mod.default;
      } catch (err) {
        console.error('[desktop] Failed to load noVNC:', err);
        if (!isCurrent()) return;
        fatal = true;
        setStatus('error');
        setErrorMsg('Failed to load noVNC library');
        return;
      }

      if (!isCurrent()) return;

      const wsUrl = buildWsUrl();
      const rfb = new RFB(container, wsUrl, {
        credentials: { password },
      });

      // The mirror cannot resize — it reflects the physical screen — so resizeSession is off.
      // scaleViewport STAYS ON, and it is load-bearing: noVNC maps a click as
      // (clientX - canvasRect.left) / display._scale, and autoscale() is the only thing that sets the
      // canvas's displayed size and _scale in the same call. Turn it off and _scale is pinned at 1 while
      // the canvas is displayed at some other size, which doubled every coordinate on 2026-08-01.
      rfb.resizeSession = false;
      rfb.scaleViewport = true;
      rfb.focusOnClick = true;
      // noVNC hides the browser's own cursor over the canvas and draws the REMOTE cursor in its place.
      // Until the server sends a cursor shape its image is RFB.cursors.none — so nothing is drawn, the
      // real cursor is hidden, and the pointer becomes invisible. Pointer coordinates are unaffected
      // (measured: x 5..1108, y 11..714 against a 1109x715 session — a perfect 1:1 map), which is why
      // this looked like a desync when it was only ever a visibility problem: the clicks were landing
      // correctly all along. A dot is better than nothing when the server has not supplied a shape.
      rfb.showDotCursor = true;
      rfbRef.current = rfb;

      rfb.addEventListener('connect', () => {
        if (!isCurrent()) return;
        attempts = 0;
        setErrorMsg('');
        setStatus('connected');
      });

      // Every disconnect we did not ask for is worth retrying, clean or not: officer closing its side
      // tidily during a restart still reports `clean`, and the desktop behind it is still there.
      rfb.addEventListener('disconnect', () => {
        if (!isCurrent()) return;
        rfbRef.current = null;
        retry('Connection lost');
      });

      rfb.addEventListener('credentialsrequired', () => {
        rfb.sendCredentials({ password });
      });

      rfb.addEventListener('securityfailure', (ev: CustomEvent) => {
        if (!isCurrent()) return;
        fatal = true;
        setStatus('error');
        setErrorMsg(ev.detail.reason || 'Security failure');
      });
    };

    // Coming back to the tab after the retries ran out should try once more rather than stay dead.
    const handleVisibility = () => {
      if (disposed || fatal || document.visibilityState !== 'visible') return;
      if (rfbRef.current || retryTimer) return;
      attempts = 0;
      setStatus('connecting');
      void connect();
    };
    document.addEventListener('visibilitychange', handleVisibility);

    void connect();

    return () => {
      disposed = true;
      document.removeEventListener('visibilitychange', handleVisibility);
      if (retryTimer) clearTimeout(retryTimer);
      detach();
    };
  }, [isMounted, client]);

  return (
    <div
      className={className}
      style={{ backgroundColor: '#1a1a2e', overflow: 'hidden', position: 'relative', ...style }}
    >
      {status === 'connecting' && (
        <div className="absolute inset-0 flex items-center justify-center text-sm text-muted-foreground">
          Connecting to desktop...
        </div>
      )}
      {(status === 'reconnecting' || status === 'disconnected' || status === 'error') && (
        <div className="absolute inset-0 flex items-center justify-center text-sm text-muted-foreground">
          {errorMsg || 'Disconnected from desktop'}
        </div>
      )}
      <div ref={containerRef} style={{ width: '100%', height: '100%' }} />
    </div>
  );
};
