import { lazy, useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
  Outlet,
  useLoaderData,
  useLocation,
  useNavigate,
  useOutlet,
  useParams,
  useSearchParams,
} from "react-router";
import type { LoaderFunction, LoaderFunctionArgs, Params } from "react-router";
import { useInterval } from "usehooks-ts";
import { FocusTrap } from "focus-trap-react";
import { motion, AnimatePresence } from "framer-motion";
import useWebSocket from "react-use-websocket";

import { cx } from "@/cva.config";
import { CLOUD_API } from "@/ui.config";
import api from "@/api";
import { checkAuth, isInCloud, isOnDevice } from "@/main";
import {
  KeyboardLedState,
  KeysDownState,
  NetworkState,
  OtaState,
  PostRebootAction,
  USBStates,
  useHidStore,
  useNetworkStateStore,
  User,
  useRTCStore,
  useUiStore,
  useUpdateStore,
  useVideoStore,
  VideoState,
  useFailsafeModeStore,
} from "@hooks/stores";
import { JsonRpcRequest, JsonRpcResponse, RpcMethodNotFound, useJsonRpc } from "@hooks/useJsonRpc";
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
import { useVersion } from "@hooks/useVersion";
import WebRTCVideo from "@components/WebRTCVideo";
import DashboardNavbar from "@components/Header";
const ConnectionStatsSidebar = lazy(() => import('@components/sidebar/connectionStats'));
const Terminal = lazy(() => import('@components/Terminal'));
const UpdateInProgressStatusCard = lazy(() => import("@components/UpdateInProgressStatusCard"));
import Modal from "@components/Modal";
import { FailSafeModeOverlay } from "@components/FailSafeModeOverlay";
import {
  ConnectionFailedOverlay,
  LoadingConnectionOverlay,
  PeerConnectionDisconnectedOverlay,
  RebootingOverlay,
} from "@components/VideoOverlay";
import { FeatureFlagProvider } from "@providers/FeatureFlagProvider";
import { m } from "@localizations/messages.js";
import { doRpcHidHandshake } from "@hooks/useHidRpc";

export type AuthMode = "password" | "noPassword" | null;

interface LocalLoaderResp {
  authMode: AuthMode;
}

interface CloudLoaderResp {
  deviceName: string;
  user: User | null;
  iceConfig: {
    iceServers: { credential?: string; urls: string | string[]; username?: string };
  } | null;
}

export interface LocalDevice {
  authMode: AuthMode;
  deviceId: string;
}

const deviceLoader = async () => {
  const device = await checkAuth();
  return { authMode: device.authMode } as LocalLoaderResp;
};

const cloudLoader = async (params: Params<string>): Promise<CloudLoaderResp> => {
  const user = await checkAuth();
  const iceResp = await api.POST(`${CLOUD_API}/webrtc/ice_config`);
  const iceConfig = await iceResp.json();
  const deviceResp = await api.GET(`${CLOUD_API}/devices/${params.id}`);

  if (!deviceResp.ok) {
    if (deviceResp.status === 404) {
      throw new Response("Device not found", { status: 404 });
    }

    throw new Error("Error fetching device");
  }

  const { device } = (await deviceResp.json()) as {
    device: { id: string; name: string; user: { googleId: string } };
  };

  return { user, iceConfig, deviceName: device.name || device.id } as CloudLoaderResp;
};

const loader: LoaderFunction = ({ params }: LoaderFunctionArgs) => {
  return isOnDevice ? deviceLoader() : cloudLoader(params);
};


export default function KvmIdRoute() {
  const loaderResp = useLoaderData();
  // Depending on the mode, we set the appropriate variables
  const user = "user" in loaderResp ? loaderResp.user : null;
  const deviceName = "deviceName" in loaderResp ? loaderResp.deviceName : null;
  const iceConfig = "iceConfig" in loaderResp ? loaderResp.iceConfig : null;
  const authMode = "authMode" in loaderResp ? loaderResp.authMode : null;

  const params = useParams() as { id: string };
  const { sidebarView, setSidebarView, disableVideoFocusTrap, rebootState, setRebootState } = useUiStore();
  const [queryParams, setQueryParams] = useSearchParams();

  const {
    peerConnection, setPeerConnection,
    peerConnectionState, setPeerConnectionState,
    setMediaStream,
    setRpcDataChannel,
    isTurnServerInUse, setTurnServerInUse,
    rpcDataChannel,
    setTransceiver,
    setRpcHidChannel,
    setRpcHidUnreliableNonOrderedChannel,
    setRpcHidUnreliableChannel,
    setRpcHidProtocolVersion,
  } = useRTCStore();

  const location = useLocation();
  const isLegacySignalingEnabled = useRef(false);
  const [connectionFailed, setConnectionFailed] = useState(false);

  const navigate = useNavigate();
  const { otaState, setOtaState, setModalView } = useUpdateStore();

  const [loadingMessage, setLoadingMessage] = useState(m.connecting_to_device());
  const cleanupAndStopReconnecting = useCallback(
    function cleanupAndStopReconnecting() {
      console.log("Closing peer connection");

      setConnectionFailed(true);
      if (peerConnection) {
        setPeerConnectionState(peerConnection.connectionState);
      }
      connectionFailedRef.current = true;

      peerConnection?.close();
      signalingAttempts.current = 0;
    },
    [peerConnection, setPeerConnectionState],
  );

  // We need to track connectionFailed in a ref to avoid stale closure issues
  // This is necessary because syncRemoteSessionDescription is a callback that captures
  // the connectionFailed value at creation time, but we need the latest value
  // when the function is actually called. Without this ref, the function would use
  // a stale value of connectionFailed in some conditions.
  //
  // We still need the state variable for UI rendering, so we sync the ref with the state.
  // This pattern is a workaround for what useEvent hook would solve more elegantly
  // (which would give us a callback that always has access to latest state without re-creation).
  const connectionFailedRef = useRef(false);
  useEffect(() => {
    connectionFailedRef.current = connectionFailed;
  }, [connectionFailed]);

  const signalingAttempts = useRef(0);
  const setRemoteSessionDescription = useCallback(
    async function setRemoteSessionDescription(
      pc: RTCPeerConnection,
      remoteDescription: RTCSessionDescriptionInit,
    ) {
      setLoadingMessage(m.setting_remote_description());

      try {
        await pc.setRemoteDescription(new RTCSessionDescription(remoteDescription));
        console.log("[setRemoteSessionDescription] Remote description set successfully to: " + remoteDescription.sdp);
        setLoadingMessage(m.establishing_secure_connection());
      } catch (error) {
        console.error(
          "[setRemoteSessionDescription] Failed to set remote description:",
          error,
        );
        cleanupAndStopReconnecting();
        return;
      }

      // Replace the interval-based check with a more reliable approach
      let attempts = 0;
      const checkInterval = setInterval(() => {
        attempts++;

        // When vivaldi has disabled "Broadcast IP for Best WebRTC Performance", this never connects
        if (pc.sctp?.state === "connected") {
          console.log("[setRemoteSessionDescription] Remote description set");
          clearInterval(checkInterval);
          setLoadingMessage(m.connection_established());
        } else if (attempts >= 10) {
          console.warn(
            "[setRemoteSessionDescription] Failed to establish connection after 10 attempts",
            {
              connectionState: pc.connectionState,
              iceConnectionState: pc.iceConnectionState,
            },
          );
          cleanupAndStopReconnecting();
          clearInterval(checkInterval);
        } else {
          console.log("[setRemoteSessionDescription] Waiting for connection, state:", {
            connectionState: pc.connectionState,
            iceConnectionState: pc.iceConnectionState,
          });
        }
      }, 1000);
    },
    [cleanupAndStopReconnecting],
  );

  const ignoreOffer = useRef(false);
  const isSettingRemoteAnswerPending = useRef(false);
  const makingOffer = useRef(false);
  const reconnectAttemptsRef = useRef(2000);
  const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";

  const reconnectInterval = (attempt: number) => {
    // Exponential backoff with a max of 10 seconds between attempts
    return Math.min(500 * 2 ** attempt, 10000);
  }

  const { sendMessage, getWebSocket } = useWebSocket(
    isOnDevice
      ? `${wsProtocol}//${window.location.host}/webrtc/signaling/client`
      : `${CLOUD_API.replace("http", "ws")}/webrtc/signaling/client?id=${params.id}`,
    {
      heartbeat: true,
      retryOnError: true,
      reconnectAttempts: reconnectAttemptsRef.current,
      reconnectInterval: reconnectInterval,
      onReconnectStop: (numAttempts: number) => {
        console.debug("Reconnect stopped after ", numAttempts, "attempts");
        cleanupAndStopReconnecting();
      },

      shouldReconnect(event: WebSocketEventMap['close']) {
        console.debug("[Websocket] shouldReconnect", event);
        return !isLegacySignalingEnabled.current;
      },

      onClose(event: WebSocketEventMap['close']) {
        console.debug("[Websocket] onClose", event);
        // We don't want to close everything down, we wait for the reconnect to stop instead
      },

      onError(event: WebSocketEventMap['error']) {
        console.error("[Websocket] onError", event);
        // We don't want to close everything down, we wait for the reconnect to stop instead
      },

      onOpen() {
        console.debug("[Websocket] onOpen");
        // We want to clear the reboot state when the websocket connection is opened
        // Currently the flow is:
        // 1. User clicks reboot
        // 2. Device sends event 'willReboot'
        // 3. We set the reboot state
        // 4. Reboot modal is shown
        // 5. WS tries to reconnect
        // 6. WS reconnects
        // 7. This function is called and now we clear the reboot state
        setRebootState({ isRebooting: false, postRebootAction: null });
      },

      onMessage(event: WebSocketEventMap['message']) {
        const message = event;
        if (message.data === "pong") return;

        /*
          Currently the signaling process is as follows:
            After open, the other side will send a `device-metadata` message with the device version
            If the device version is not set, we can assume the device is using the legacy signaling
            Otherwise, we can assume the device is using the new signaling

            If the device is using the legacy signaling, we close the websocket connection
            and use the legacy HTTPSignaling function to get the remote session description

            If the device is using the new signaling, we don't need to do anything special, but continue to use the websocket connection
            to chat with the other peer about the connection
        */

        const parsedMessage = JSON.parse(message.data);

        if (parsedMessage.type === "device-metadata") {
          const { deviceVersion } = parsedMessage.data;
          console.debug("[Websocket] Received device-metadata message");
          console.debug("[Websocket] Device version", deviceVersion);
          // If the device version is not set, we can assume the device is using the legacy signaling
          if (!deviceVersion) {
            console.log("[Websocket] Device is using legacy signaling");

            // Now we don't need the websocket connection anymore, as we've established that we need to use the legacy signaling
            // which does everything over HTTP(at least from the perspective of the client)
            isLegacySignalingEnabled.current = true;
            getWebSocket()?.close();
          } else {
            console.log("[Websocket] Device is using new signaling");
            isLegacySignalingEnabled.current = false;
          }

          setupPeerConnection();
        }

        if (!peerConnection) return;

        if (parsedMessage.type === "answer") {
          console.debug("[Websocket] Received answer");
          const readyForOffer =
            // If we're making an offer, we don't want to accept an answer
            !makingOffer &&
            // If the peer connection is stable or we're setting the remote answer pending, we're ready for an offer
            (peerConnection?.signalingState === "stable" ||
              isSettingRemoteAnswerPending.current);

          // If we're not ready for an offer, we don't want to accept an offer
          ignoreOffer.current = parsedMessage.type === "offer" && !readyForOffer;
          if (ignoreOffer.current) return;

          // Set so we don't accept an answer while we're setting the remote description
          isSettingRemoteAnswerPending.current = parsedMessage.type === "answer";
          console.debug(
            "[Websocket] Setting remote answer pending",
            isSettingRemoteAnswerPending.current,
          );

          const sd = atob(parsedMessage.data);
          const remoteSessionDescription = JSON.parse(sd);

          setRemoteSessionDescription(
            peerConnection,
            new RTCSessionDescription(remoteSessionDescription),
          );

          // Reset the remote answer pending flag
          isSettingRemoteAnswerPending.current = false;
        } else if (parsedMessage.type === "new-ice-candidate") {
          console.debug("[Websocket] Received new-ice-candidate");
          const candidate = parsedMessage.data;
          peerConnection.addIceCandidate(candidate);
        }
      },
    }
  );

  const sendWebRTCSignal = useCallback(
    (type: string, data: unknown) => {
      // Second argument tells the library not to queue the message, and send it once the connection is established again.
      // We have event handlers that handle the connection set up, so we don't need to queue the message.
      sendMessage(JSON.stringify({ type, data }), false);
    },
    [sendMessage],
  );

  const legacyHTTPSignaling = useCallback(
    async (pc: RTCPeerConnection) => {
      const sd = btoa(JSON.stringify(pc.localDescription));

      // Legacy mode == UI in cloud with updated code connecting to older device version.
      // In device mode, old devices wont serve this JS, and on newer devices legacy mode wont be enabled
      const sessionUrl = `${CLOUD_API}/webrtc/session`;

      console.log("Trying to get remote session description");
      setLoadingMessage(
        m.getting_remote_session_description({ attempt: signalingAttempts.current + 1 }),
      );
      const res = await api.POST(sessionUrl, {
        sd,
        // When on device, we don't need to specify the device id, as it's already known
        ...(isOnDevice ? {} : { id: params.id }),
      });

      const json = await res.json();
      if (res.status === 401) return navigate(isOnDevice ? "/login-local" : "/login");
      if (!res.ok) {
        console.error("Error getting SDP", { status: res.status, json });
        cleanupAndStopReconnecting();
        return;
      }

      console.debug("Successfully got Remote Session Description. Setting.");
      setLoadingMessage(m.setting_remote_session_description());

      const decodedSd = atob(json.sd);
      const parsedSd = JSON.parse(decodedSd);
      setRemoteSessionDescription(pc, new RTCSessionDescription(parsedSd));
    },
    [cleanupAndStopReconnecting, navigate, params.id, setRemoteSessionDescription],
  );

  const setupPeerConnection = useCallback(async () => {
    console.debug("[setupPeerConnection] Setting up peer connection");
    setConnectionFailed(false);
    setLoadingMessage(m.connecting_to_device());

    let pc: RTCPeerConnection;
    try {
      console.debug("[setupPeerConnection] Creating peer connection");
      setLoadingMessage(m.creating_peer_connection());
      pc = new RTCPeerConnection({
        // We only use STUN or TURN servers if we're in the cloud
        ...(isInCloud && iceConfig?.iceServers
          ? { iceServers: [iceConfig?.iceServers] }
          : {}),
      });

      setPeerConnectionState(pc.connectionState);
      console.debug("[setupPeerConnection] Peer connection created", pc);
      setLoadingMessage(m.setting_up_connection_to_device());
    } catch (e) {
      console.error(`[setupPeerConnection] Error creating peer connection: ${e}`);
      setTimeout(() => {
        cleanupAndStopReconnecting();
      }, 1000);
      return;
    }

    // Set up event listeners and data channels
    pc.onconnectionstatechange = () => {
      console.debug("[setupPeerConnection] Connection state changed", pc.connectionState);
      setPeerConnectionState(pc.connectionState);
    };

    pc.onnegotiationneeded = async () => {
      try {
        console.debug("[setupPeerConnection] Creating offer");
        makingOffer.current = true;

        const offer = await pc.createOffer();
        await pc.setLocalDescription(offer);
        const sd = btoa(JSON.stringify(pc.localDescription));
        const isNewSignalingEnabled = isLegacySignalingEnabled.current === false;
        if (isNewSignalingEnabled) {
          sendWebRTCSignal("offer", { sd: sd });
        } else {
          console.log("Legacy signaling. Waiting for ICE Gathering to complete...");
        }
      } catch (e) {
        console.error(
          `[setupPeerConnection] Error creating offer: ${e}`,
          new Date().toISOString(),
        );
        cleanupAndStopReconnecting();
      } finally {
        makingOffer.current = false;
      }
    };

    pc.onicecandidate = ({ candidate }) => {
      if (!candidate) return;
      if (candidate.candidate === "") return;
      sendWebRTCSignal("new-ice-candidate", candidate);
    };

    pc.onicegatheringstatechange = event => {
      const pc = event.currentTarget as RTCPeerConnection;
      if (pc.iceGatheringState === "complete") {
        console.debug("ICE Gathering completed");
        setLoadingMessage(m.ice_gathering_completed());

        if (isLegacySignalingEnabled.current) {
          // We can now start the https/ws connection to get the remote session description from the KVM device
          legacyHTTPSignaling(pc);
        }
      } else if (pc.iceGatheringState === "gathering") {
        console.debug("ICE Gathering Started");
        setLoadingMessage(m.gathering_ice_candidates());
      }
    };

    pc.ontrack = function (event) {
      setMediaStream(event.streams[0]);
    };

    setTransceiver(pc.addTransceiver("video", { direction: "recvonly" }));

    const rpcDataChannel = pc.createDataChannel("rpc");
    rpcDataChannel.onclose = () => console.log("rpcDataChannel has closed");
    rpcDataChannel.onerror = (ev: Event) => console.error(`Error on DataChannel '${rpcDataChannel.label}': ${ev}`);
    rpcDataChannel.onopen = () => {
      setRpcDataChannel(rpcDataChannel);
    };

    const rpcHidChannel = pc.createDataChannel("hidrpc");
    rpcHidChannel.binaryType = "arraybuffer";
    rpcHidChannel.onclose = () => console.log("rpcHidChannel has closed");
    rpcHidChannel.onerror = (ev: Event) => console.error(`Error on rpcHidChannel '${rpcHidChannel.label}': ${ev}`);
    rpcHidChannel.onopen = () => {
      setRpcHidChannel(rpcHidChannel);
    };
    doRpcHidHandshake(rpcHidChannel, setRpcHidProtocolVersion);

    const rpcHidUnreliableChannel = pc.createDataChannel("hidrpc-unreliable-ordered", {
      ordered: true,
      maxRetransmits: 0,
    });
    rpcHidUnreliableChannel.binaryType = "arraybuffer";
    rpcHidUnreliableChannel.onclose = () => console.log("rpcHidUnreliableChannel has closed");
    rpcHidUnreliableChannel.onerror = (ev: Event) => console.error(`Error on rpcHidUnreliableChannel '${rpcHidUnreliableChannel.label}': ${ev}`);
    rpcHidUnreliableChannel.onopen = () => {
      setRpcHidUnreliableChannel(rpcHidUnreliableChannel);
    };

    const rpcHidUnreliableNonOrderedChannel = pc.createDataChannel("hidrpc-unreliable-nonordered", {
      ordered: false,
      maxRetransmits: 0,
    });
    rpcHidUnreliableNonOrderedChannel.binaryType = "arraybuffer";
    rpcHidUnreliableNonOrderedChannel.onclose = () => console.log("rpcHidUnreliableNonOrderedChannel has closed");
    rpcHidUnreliableNonOrderedChannel.onerror = (ev: Event) => console.error(`Error on rpcHidUnreliableNonOrderedChannel '${rpcHidUnreliableNonOrderedChannel.label}': ${ev}`);
    rpcHidUnreliableNonOrderedChannel.onopen = () => {
      setRpcHidUnreliableNonOrderedChannel(rpcHidUnreliableNonOrderedChannel);
    };

    setPeerConnection(pc);
  }, [
    cleanupAndStopReconnecting,
    iceConfig?.iceServers,
    legacyHTTPSignaling,
    sendWebRTCSignal,
    setMediaStream,
    setPeerConnection,
    setPeerConnectionState,
    setRpcDataChannel,
    setRpcHidChannel,
    setRpcHidUnreliableNonOrderedChannel,
    setRpcHidUnreliableChannel,
    setRpcHidProtocolVersion,
    setTransceiver,
  ]);

  useEffect(() => {
    if (peerConnectionState === "failed") {
      console.warn("Connection failed, closing peer connection");
      cleanupAndStopReconnecting();
    }
  }, [peerConnectionState, cleanupAndStopReconnecting]);

  // Cleanup effect
  const { clearInboundRtpStats, clearCandidatePairStats } = useRTCStore();

  useEffect(() => {
    return () => {
      peerConnection?.close();
    };
  }, [peerConnection]);

  // For some reason, we have to have this unmount separate from the cleanup effect above
  useEffect(() => {
    return () => {
      clearInboundRtpStats();
      clearCandidatePairStats();
      setSidebarView(null);
      setPeerConnection(null);
      setRpcDataChannel(null);
    };
  }, [clearCandidatePairStats, clearInboundRtpStats, setPeerConnection, setSidebarView, setRpcDataChannel]);

  // TURN server usage detection
  useEffect(() => {
    if (peerConnectionState !== "connected") return;
    const { localCandidateStats, remoteCandidateStats } = useRTCStore.getState();

    const lastLocalStat = Array.from(localCandidateStats).pop();
    if (!lastLocalStat?.length) return;
    const localCandidateIsUsingTurn = lastLocalStat[1].candidateType === "relay"; // [0] is the timestamp, which we don't care about here

    const lastRemoteStat = Array.from(remoteCandidateStats).pop();
    if (!lastRemoteStat?.length) return;
    const remoteCandidateIsUsingTurn = lastRemoteStat[1].candidateType === "relay"; // [0] is the timestamp, which we don't care about here

    setTurnServerInUse(localCandidateIsUsingTurn || remoteCandidateIsUsingTurn);
  }, [peerConnectionState, setTurnServerInUse]);

  // TURN server usage reporting
  const lastBytesReceived = useRef<number>(0);
  const lastBytesSent = useRef<number>(0);

  useInterval(() => {
    // Don't report usage if we're not using the turn server
    if (!isTurnServerInUse) return;
    const { candidatePairStats } = useRTCStore.getState();

    const lastCandidatePair = Array.from(candidatePairStats).pop();
    const report = lastCandidatePair?.[1];
    if (!report) return;

    let bytesReceivedDelta = 0;
    let bytesSentDelta = 0;

    if (report.bytesReceived) {
      bytesReceivedDelta = report.bytesReceived - lastBytesReceived.current;
      lastBytesReceived.current = report.bytesReceived;
    }

    if (report.bytesSent) {
      bytesSentDelta = report.bytesSent - lastBytesSent.current;
      lastBytesSent.current = report.bytesSent;
    }

    // Fire and forget
    api.POST(`${CLOUD_API}/webrtc/turn_activity`, {
      bytesReceived: bytesReceivedDelta,
      bytesSent: bytesSentDelta,
    }).catch(() => {
      // we don't care about errors here, but we don't want unhandled promise rejections
    });
  }, 10000);

  const { setNetworkState } = useNetworkStateStore();
  const { setHdmiState } = useVideoStore();
  const {
    keyboardLedState, setKeyboardLedState,
    keysDownState, setKeysDownState,
    setUsbState,
  } = useHidStore();
  const setHidRpcDisabled = useRTCStore(state => state.setHidRpcDisabled);
  const { setFailsafeMode } = useFailsafeModeStore();

  const [hasUpdated, setHasUpdated] = useState(false);
  const { navigateTo } = useDeviceUiNavigation();

  function onJsonRpcRequest(resp: JsonRpcRequest) {
    if (resp.method === "otherSessionConnected") {
      navigateTo("/other-session");
    }

    if (resp.method === "usbState") {
      const usbState = resp.params as unknown as USBStates;
      console.debug("Setting USB state", usbState);
      setUsbState(usbState);
    }

    if (resp.method === "videoInputState") {
      const hdmiState = resp.params as Parameters<VideoState["setHdmiState"]>[0];
      console.debug("Setting HDMI state", hdmiState);
      setHdmiState(hdmiState);
    }

    if (resp.method === "networkState") {
      console.debug("Setting network state", resp.params);
      setNetworkState(resp.params as NetworkState);
    }

    if (resp.method === "keyboardLedState") {
      const ledState = resp.params as KeyboardLedState;
      console.debug("Setting keyboard led state", ledState);
      setKeyboardLedState(ledState);
    }

    if (resp.method === "keysDownState") {
      const downState = resp.params as KeysDownState;
      console.debug("Setting key down state:", downState);
      setKeysDownState(downState);
    }

    if (resp.method === "otaState") {
      const otaState = resp.params as OtaState;
      console.debug("Setting OTA state", otaState);
      setOtaState(otaState);

      if (otaState.updating === true) {
        setHasUpdated(true);
      }

      if (hasUpdated && otaState.updating === false) {
        setHasUpdated(false);

        if (otaState.error) {
          setModalView("error");
          navigateTo("/settings/general/update");
          return;
        }


        // This is to prevent the otaState from handling page refreshes after an update
        // We've recently implemented a new general rebooting flow, so we don't need to handle this specific ota-rebooting case
        // However, with old devices, we wont get the `willReboot` message, so we need to keep this for backwards compatibility
        // only for the cloud version with an old device
        if (rebootState?.isRebooting) return;

        const currentUrl = new URL(window.location.href);
        currentUrl.search = "";
        currentUrl.searchParams.set("updateSuccess", "true");
        window.location.href = currentUrl.toString();
      }
    }

    if (resp.method === "willReboot") {
      const postRebootAction = resp.params as unknown as PostRebootAction;
      console.debug("Setting reboot state", postRebootAction);

      setRebootState({
        isRebooting: true,
        postRebootAction: {
          healthCheck: postRebootAction?.healthCheck || `${window.location.origin}/device/status`,
          redirectTo: postRebootAction?.redirectTo || window.location.href,
        }
      });
      navigateTo("/");
    }

    if (resp.method === "failsafeMode") {
      const { active, reason } = resp.params as { active: boolean; reason: string };
      console.debug("Setting failsafe mode", { active, reason });
      setFailsafeMode(active, reason);
    }
  }

  const { send } = useJsonRpc(onJsonRpcRequest);

  useEffect(() => {
    if (rpcDataChannel?.readyState !== "open") return;
    console.log("Requesting video state");
    send("getVideoState", {}, (resp: JsonRpcResponse) => {
      if ("error" in resp) return;
      const hdmiState = resp.result as Parameters<VideoState["setHdmiState"]>[0];
      console.debug("Setting HDMI state", hdmiState);
      setHdmiState(hdmiState);
    });
  }, [rpcDataChannel?.readyState, send, setHdmiState]);

  const [needLedState, setNeedLedState] = useState(true);

  // request keyboard led state from the device
  useEffect(() => {
    if (rpcDataChannel?.readyState !== "open") return;
    if (!needLedState) return;
    console.log("Requesting keyboard led state");

    send("getKeyboardLedState", {}, (resp: JsonRpcResponse) => {
      if ("error" in resp) {
        console.error("Failed to get keyboard led state", resp.error);
        return;
      } else {
        const ledState = resp.result as KeyboardLedState;
        console.debug("Keyboard led state: ", ledState);
        setKeyboardLedState(ledState);
      }
      setNeedLedState(false);
    });
  }, [rpcDataChannel?.readyState, send, setKeyboardLedState, keyboardLedState, needLedState]);

  const [needKeyDownState, setNeedKeyDownState] = useState(true);

  // request keyboard key down state from the device
  useEffect(() => {
    if (rpcDataChannel?.readyState !== "open") return;
    if (!needKeyDownState) return;
    console.log("Requesting keys down state");

    send("getKeyDownState", {}, (resp: JsonRpcResponse) => {
      if ("error" in resp) {
        // -32601 means the method is not supported
        if (resp.error.code === RpcMethodNotFound) {
          // if we don't support key down state, we know key press is also not available
          console.warn("Failed to get key down state, switching to old-school", resp.error);
          setHidRpcDisabled(true);
        } else {
          console.error("Failed to get key down state", resp.error);
        }
      } else {
        const downState = resp.result as KeysDownState;
        console.debug("Keyboard key down state", downState);
        setKeysDownState(downState);
      }
      setNeedKeyDownState(false);
    });
  }, [keysDownState, needKeyDownState, rpcDataChannel?.readyState, send, setKeysDownState, setHidRpcDisabled]);

  // When the update is successful, we need to refresh the client javascript and show a success modal
  useEffect(() => {
    if (queryParams.get("updateSuccess")) {
      navigateTo("/settings/general/update", { state: { updateSuccess: true } });
    }
  }, [navigate, navigateTo, queryParams, setModalView, setQueryParams]);

  // System update
  const [kvmTerminal, setKvmTerminal] = useState<RTCDataChannel | null>(null);
  const [serialConsole, setSerialConsole] = useState<RTCDataChannel | null>(null);

  useEffect(() => {
    if (!peerConnection) return;
    if (!kvmTerminal) {
      setKvmTerminal(peerConnection.createDataChannel("terminal"));
    }

    if (!serialConsole) {
      setSerialConsole(peerConnection.createDataChannel("serial"));
    }
  }, [kvmTerminal, peerConnection, serialConsole]);

  const outlet = useOutlet();
  const onModalClose = useCallback(() => {
    if (location.pathname !== "/other-session") navigateTo("/");
  }, [navigateTo, location.pathname]);

  const { appVersion, getLocalVersion } = useVersion();

  useEffect(() => {
    if (appVersion) return;

    getLocalVersion();
  }, [appVersion, getLocalVersion]);

  const { isFailsafeMode, reason: failsafeReason } = useFailsafeModeStore();

  const ConnectionStatusElement = useMemo(() => {
    const isOtherSession = location.pathname.includes("other-session");
    if (isOtherSession) return null;

    // Rebooting takes priority over connection status
    if (rebootState?.isRebooting) {
      return <RebootingOverlay show={true} postRebootAction={rebootState.postRebootAction} />;
    }

    if (isFailsafeMode && failsafeReason) {
      return <FailSafeModeOverlay reason={failsafeReason} />;
    }

    const hasConnectionFailed =
      connectionFailed || ["failed", "closed"].includes(peerConnectionState ?? "");

    const isPeerConnectionLoading =
      ["connecting", "new"].includes(peerConnectionState ?? "") ||
      peerConnection === null;

    const isDisconnected = peerConnectionState === "disconnected";

    if (peerConnectionState === "connected") return null;
    if (isDisconnected) {
      return <PeerConnectionDisconnectedOverlay show={true} />;
    }

    if (hasConnectionFailed)
      return (
        <ConnectionFailedOverlay show={true} setupPeerConnection={setupPeerConnection} />
      );

    if (isPeerConnectionLoading) {
      return <LoadingConnectionOverlay show={true} text={loadingMessage} />;
    }

    return null;
  }, [location.pathname, rebootState?.isRebooting, rebootState?.postRebootAction, isFailsafeMode, failsafeReason, connectionFailed, peerConnectionState, peerConnection, setupPeerConnection, loadingMessage]);

  return (
    <FeatureFlagProvider appVersion={appVersion}>
      {!outlet && otaState.updating && (
        <AnimatePresence>
          <motion.div
            className="pointer-events-none fixed inset-0 top-16 z-10 mx-auto flex h-full w-full max-w-xl translate-y-8 items-start justify-center"
            initial={{ opacity: 0, y: -20 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0, y: -20 }}
            transition={{ duration: 0.3, ease: "easeInOut" }}
          >
            <UpdateInProgressStatusCard />
          </motion.div>
        </AnimatePresence>
      )}
      <div className="relative h-full">
        <FocusTrap
          paused={disableVideoFocusTrap}
          focusTrapOptions={{
            allowOutsideClick: true,
            escapeDeactivates: false,
            fallbackFocus: "#videoFocusTrap",
          }}
        >
          <div className="absolute top-0">
            <button className="absolute top-0" tabIndex={-1} id="videoFocusTrap" />
          </div>
        </FocusTrap>

        <div className="grid h-full grid-rows-(--grid-headerBody) select-none">
          <DashboardNavbar
            primaryLinks={isOnDevice ? [] : [{ title: "Cloud Devices", to: "/devices" }]}
            showConnectionStatus={true}
            isLoggedIn={authMode === "password" || !!user}
            userEmail={user?.email}
            picture={user?.picture}
            kvmName={deviceName ?? m.jetkvm_device()}
          />

          <div className="relative flex h-full w-full overflow-hidden">
            {(isFailsafeMode && failsafeReason === "video") ? null : <WebRTCVideo hasConnectionIssues={!!ConnectionStatusElement} />}
            <div
              style={{ animationDuration: "500ms" }}
              className="animate-slideUpFade pointer-events-none absolute inset-0 flex items-center justify-center p-4"
            >
              <div className="relative h-full max-h-[720px] w-full max-w-[1280px] rounded-md">
                {!!ConnectionStatusElement && ConnectionStatusElement}
              </div>
            </div>
            <SidebarContainer sidebarView={sidebarView} />
          </div>
        </div>
      </div>

      <div
        className="z-50"
        role="form"
        onClick={e => e.stopPropagation()}
        onMouseUp={e => e.stopPropagation()}
        onMouseDown={e => e.stopPropagation()}
        onKeyUp={e => e.stopPropagation()}
        onKeyDown={e => {
          e.stopPropagation();
          if (e.key === "Escape") navigateTo("/");
        }}
      >
        <Modal open={outlet !== null} onClose={onModalClose}>
          {/* The 'used by other session' modal needs to have access to the connectWebRTC function */}
          <Outlet context={{ setupPeerConnection }} />
        </Modal>
      </div>

      {kvmTerminal && (
        <Terminal type="kvm" dataChannel={kvmTerminal} title={m.kvm_terminal()} />
      )}

      {serialConsole && (
        <Terminal type="serial" dataChannel={serialConsole} title={m.serial_console()} />
      )}
    </FeatureFlagProvider>
  );
}

interface SidebarContainerProps {
  readonly sidebarView: string | null;
}

function SidebarContainer(props: SidebarContainerProps) {
  const { sidebarView } = props;
  return (
    <div
      className={cx(
        "flex shrink-0 border-l border-l-slate-800/20 transition-all duration-500 ease-in-out dark:border-l-slate-300/20",
        { "border-x-transparent": !sidebarView },
      )}
      style={{ width: sidebarView ? "493px" : 0 }}
    >
      <div className="relative w-[493px] shrink-0">
        <AnimatePresence>
          {sidebarView === "connection-stats" && (
            <motion.div
              className="absolute inset-0"
              initial={{ opacity: 0 }}
              animate={{ opacity: 1 }}
              exit={{ opacity: 0 }}
              transition={{
                duration: 0.5,
                ease: "easeInOut",
              }}
            >
              <ConnectionStatsSidebar />
            </motion.div>
          )}
        </AnimatePresence>
      </div>
    </div>
  );
}

KvmIdRoute.loader = loader;
