import { useCallback, useEffect, useMemo, useReducer, useRef } from 'react';

import { type CheatSchema, type InstalledAppSummary } from '../../protocol/messages';
import { normalizeCheatValue } from '../trainer/model/values';
import { RemoteSessionClient } from './remote-session.client';
import { localizeTrainerMeta } from './remote-session.i18n';
import {
  createInitialRemoteSessionState,
  EConnectionStatus,
  remoteSessionReducer,
  type RemoteSessionState,
} from './remote-session.reducer';
import { protocolAction } from './remote-session.protocol';
import { selectIsConnected, selectPendingTargets } from './selectors';

const RECONNECT_DELAY_MS = 2000;

export function useRemoteSession() {
  const [state, dispatch] = useReducer(remoteSessionReducer, undefined, createInitialRemoteSessionState);
  const stateRef = useRef(state);
  const clientRef = useRef<RemoteSessionClient | null>(null);
  const reconnectTimeoutRef = useRef<number | null>(null);
  const connectRef = useRef<() => void>(() => {});
  useEffect(() => {
    stateRef.current = state;
  }, [state]);

  const clearReconnect = useCallback(() => {
    if (reconnectTimeoutRef.current !== null) {
      window.clearTimeout(reconnectTimeoutRef.current);
      reconnectTimeoutRef.current = null;
    }
  }, []);

  const scheduleReconnect = useCallback(() => {
    clearReconnect();
    if (document.visibilityState !== 'visible') {
      return;
    }
    reconnectTimeoutRef.current = window.setTimeout(() => {
      if (document.visibilityState === 'visible' && stateRef.current.wsUrl.trim()) {
        connectRef.current();
      }
    }, RECONNECT_DELAY_MS);
  }, [clearReconnect]);

  const connect = useCallback(() => {
    clientRef.current?.disconnect();
    clearReconnect();

    const wsUrl = stateRef.current.wsUrl.trim();
    if (!wsUrl) {
      dispatch({ type: 'error', message: 'Enter a WebSocket URL first.' });
      return;
    }

    const client = new RemoteSessionClient(wsUrl, {
      onConnecting: () => dispatch({
        type: 'connecting',
        reconnecting: stateRef.current.connectionStatus === EConnectionStatus.Reconnecting,
      }),
      onTransportOpen: () => undefined,
      onMessage: (message) => {
        const action = protocolAction(message);
        if (!action) return;
        if (action.type === 'trainerMeta') {
          void localizeTrainerMeta(action.payload).then((payload) => dispatch({ ...action, payload }));
          return;
        }
        dispatch(action);
      },
      onClose: () => {
        dispatch({ type: 'connectionClosed', message: 'The WebSocket connection closed. Reconnecting...' });
        scheduleReconnect();
      },
      onError: (message) => dispatch({ type: 'error', message }),
    });

    clientRef.current = client;
    client.connect();
  }, [clearReconnect, scheduleReconnect]);

  const disconnect = useCallback(() => {
    clearReconnect();
    clientRef.current?.disconnect();
    clientRef.current = null;
    dispatch({ type: 'disconnected' });
  }, [clearReconnect]);

  const setWsUrl = useCallback((wsUrl: string) => dispatch({ type: 'setWsUrl', wsUrl }), []);
  const reportError = useCallback((message: string | null) => dispatch({ type: 'error', message }), []);

  const changeCheat = useCallback((cheat: CheatSchema, nextValue: unknown) => {
    const current = stateRef.current;
    if (current.connectionStatus !== EConnectionStatus.Connected || !current.trainerMeta) {
      dispatch({ type: 'error', message: 'The bridge socket is not connected.' });
      return false;
    }

    const value = normalizeCheatValue(cheat, nextValue);
    const requestId = clientRef.current?.setValue(
      current.trainerMeta.trainer.trainerId,
      cheat.target,
      value,
      cheat.uuid,
    ) ?? null;
    if (!requestId) {
      dispatch({ type: 'error', message: 'The bridge socket is not open.' });
      return false;
    }

    dispatch({ type: 'writeStarted', target: cheat.target, value, requestId });
    return true;
  }, []);

  const launchGame = useCallback((app: InstalledAppSummary): boolean => {
    if (!app.gameId) {
      dispatch({ type: 'error', message: 'This My Games entry does not expose a Wand game id.' });
      return false;
    }
    if (!isReadyToSend(stateRef.current, clientRef.current)) {
      dispatch({ type: 'error', message: 'The bridge socket is not connected.' });
      return false;
    }
    if (!clientRef.current?.launchGame(app.gameId, app.titleId ?? undefined)) {
      dispatch({ type: 'error', message: 'Failed to send the launch command to the bridge.' });
      return false;
    }
    return true;
  }, []);

  const stopPlaying = useCallback(() => {
    const current = stateRef.current;
    if (!isReadyToSend(current, clientRef.current)) {
      dispatch({ type: 'error', message: 'The bridge socket is not connected.' });
      return;
    }
    const gameId = current.gameStatus?.session.gameId ?? current.gameStatus?.trainer.gameId ?? undefined;
    const titleId = current.gameStatus?.session.titleId ?? current.gameStatus?.trainer.titleId ?? undefined;
    if (!clientRef.current?.stopPlaying(gameId ?? undefined, titleId ?? undefined)) {
      dispatch({ type: 'error', message: 'Failed to send the stop command to the bridge.' });
    }
  }, []);

  useEffect(() => {
    connectRef.current = connect;
  }, [connect]);

  useEffect(() => {
    const onVisibilityChange = () => {
      if (document.visibilityState === 'visible' && !clientRef.current?.isOpen()) {
        connectRef.current();
      }
    };
    document.addEventListener('visibilitychange', onVisibilityChange);
    return () => document.removeEventListener('visibilitychange', onVisibilityChange);
  }, []);

  useEffect(() => {
    if (stateRef.current.wsUrl.trim()) {
      connectRef.current();
    }
    return () => {
      clearReconnect();
      clientRef.current?.disconnect();
      clientRef.current = null;
    };
  }, [clearReconnect]);

  const connected = selectIsConnected(state);
  const pendingTargets = useMemo(() => selectPendingTargets(state), [state]);

  return {
    state,
    connected,
    pendingTargets,
    socketReady: connected,
    connect,
    disconnect,
    setWsUrl,
    reportError,
    changeCheat,
    launchGame,
    stopPlaying,
  };
}

function isReadyToSend(state: RemoteSessionState, client: RemoteSessionClient | null): boolean {
  return state.connectionStatus === EConnectionStatus.Connected && Boolean(client?.isOpen());
}
