import React, { useEffect, useRef } from 'react';
import { useMutation, useQuery } from '@tanstack/react-query';
import { useGame } from '../contexts/GameContext';
import { DESC } from '../utils/constants';

/** Handles all server communication: SSE (Mercure), REST calls, and the initialization lifecycle. */
const useServerCommunication = (gameAssoc, gameInherited, isEnvDev) => {
  const {
    /** Async-safe refs */
    webPlayerRef, activePlayerRef, bombSelectedRef, connectionLostRef, endRef,
    /** State setters */
    setGridReady,
    /** Sync helpers */
    syncWebPlayer, syncActivePlayer, syncBombSelected, syncConnLost, syncRed, syncBlue,
    /** Game logic */
    showOverlay, hideOverlay,
    applyRevealedCell, applyStep,
    makeGameEndIfItEnds, resignProcess,
    /** Current cells snapshot (for active-check in onClick) */
    cells,
  } = useGame();

  const eventSourceRef = useRef(null);
  const rpcUsersRef = useRef(null);
  const stepCacheRef = useRef([]);

  /** HELPERS */

  const correctGridSize = () => {
    let $f = $('#mine-wrapper .grid');
    $f.height($f.width());
    $f = $('#mine-wrapper .grid .field-wrapper');
    $f.height($f.width());
    $('#mine-wrapper .grid .field-wrapper .field').height($f.width()).css('line-height', ($f.width() - 2) + 'px');
  };

  /** REST mutations / queries */

  const connectQuery = useQuery({
    queryKey: ['game-connect', gameAssoc],
    queryFn: () => fetch('/api/game/connect/' + gameAssoc)
      .then(r => r.text())
      .then(b64 => JSON.parse(window.atob(b64))),
    enabled: false,
    retry: false,
  });

  const startMutation = useMutation({
    mutationFn: () => fetch('/api/game/start', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ gameAssoc }),
    }),
  });

  const joinMutation = useMutation({
    mutationFn: () => fetch('/api/game/join/' + gameAssoc, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
    }).catch(e => isEnvDev && console.error('Join error', e)),
  });

  const stepMutation = useMutation({
    mutationFn: dataPack => fetch('/api/game/step/' + gameAssoc, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(dataPack),
    }).then(r => r.json()),
  });

  /** Game-start helpers (triggered by server events) */

  const wInit = (revealedCells = []) => {
    setGridReady(true);
    showOverlay('We are waiting for your opponent...', gameAssoc ? (
      <div>
        <h3>Share this unique link w/ your opponent</h3>
        <div className="clippy">
          <input id="foo" defaultValue={`${window.location.href}/${gameAssoc}`} />
        </div>
        <a href={`/play/${gameAssoc}`} target="_blank">Play w/ me!</a>
      </div>
    ) : '');

    setTimeout(() => revealedCells.forEach(cell => applyRevealedCell(cell, cell.player)), 0);
  };

  const makeGameStart = payload => {
    syncActivePlayer(1);
    syncRed(p => ({
      ...p,
      name: payload.users.red || payload.users.redAnon || p.name,
    }));
    syncBlue(p => ({
      ...p,
      name: payload.users.blue || payload.users.blueAnon || p.name,
      desc: 'blue' === webPlayerRef.current ? DESC.you : DESC.buddy,
      active: true,
    }));
    hideOverlay();
  };

  /** Mercure / SSE message handlers */

  const wSubscribe = (payload, rpcUsers = null) => {
    isEnvDev && console.info((payload.user ?? 'user') + ' subscribed');
    const firstUser = !rpcUsers;

    if (null === webPlayerRef.current) {
      const isBlue = payload.user === payload.users.blue
        || (firstUser ? '' !== payload.users.blueAnon : '' === rpcUsers.blueAnon && '' === rpcUsers.blue);
      syncWebPlayer(isBlue ? 'blue' : 'red');
    }

    900 > $(document).width() && correctGridSize();

    if (
      2 === payload.userCnt
      && (!connectionLostRef.current
        || (connectionLostRef.current && false === activePlayerRef.current && !endRef.current))
    ) {
      makeGameStart(payload);
    }
  };

  const wUnsubscribe = payload => {
    isEnvDev && console.info(payload.msg);
    showOverlay('The connection has been lost w/ your friend...', 'Please, restart the game!');
  };

  const wTopic = payload => {
    if (webPlayerRef.current !== payload.data.player) {
      if (null === payload.data.resign) {
        isEnvDev && console.warn(payload.user + ' stepped to ' + payload.data.coords.join(','));
        syncBombSelected(payload.data.bomb);
        applyStep(payload.data);
        makeGameEndIfItEnds(payload.data.bluePoints, payload.data.redPoints, false, payload.data.leftMines);
      } else {
        resignProcess(payload.data.resign);
      }
    }
  };

  const handleMercureMessage = payload => {
    if (undefined !== payload.data) {
      wTopic(payload);
    } else if (undefined === payload.msg) {
      wSubscribe(payload, rpcUsersRef.current);
    } else {
      wUnsubscribe(payload);
    }

    if (2 === payload.userCnt && connectionLostRef.current) {
      isEnvDev && console.info('Reconnection');
      stepCacheRef.current.forEach(item => stepMutation.mutate(item));
      stepCacheRef.current = [];
      syncConnLost(false);
    }
  };

  const openEventSource = () => {
    const wrapper = document.getElementById('mine-wrapper');
    const hubUrl = wrapper.dataset.mercureHubUrl;
    const subscriberJwt = wrapper.dataset.mercureSubscriberJwt;
    const url = new URL(hubUrl, window.location.origin);

    url.searchParams.append('topic', 'mineseeker/channel/' + gameAssoc);
    if (subscriberJwt) url.searchParams.append('authorization', subscriberJwt);

    if (eventSourceRef.current) eventSourceRef.current.close();

    const es = new EventSource(url.toString());
    es.onmessage = e => handleMercureMessage(JSON.parse(e.data));
    es.onopen = () => {
      isEnvDev && console.info('SSE opened');
      if (connectionLostRef.current) {
        isEnvDev && console.info('SSE reconnected');
        joinMutation.mutate();
      }
    };
    es.onerror = () => {
      isEnvDev && console.error('SSE error');
      syncConnLost(true);
    };
    eventSourceRef.current = es;
  };

  /** Initialization */

  useEffect(() => {
    (async () => {
      if (connectionLostRef.current) {
        openEventSource();
        return;
      }
      try {
        if (gameInherited) {
          const serverData = await connectQuery.refetch().then(r => {
            if (r.error) throw r.error;
            return r.data;
          });

          if ('undefined' === typeof serverData.users || null === serverData.users) {
            showOverlay('This channel does not exists!', <a href="/play" target="_self">Restart game!</a>);
            console.error('This channel does not exists!');
            return;
          }

          rpcUsersRef.current = serverData.users;
          openEventSource();
          wInit(serverData.revealedCells || []);
        } else {
          await startMutation.mutateAsync();
          openEventSource();
          wInit();
        }

        isEnvDev && console.info('Connection initialised — joining channel');
        await joinMutation.mutateAsync();
      } catch (e) {
        isEnvDev && console.error('Connection error', e);
        setTimeout(() => window.location.reload(), 500);
      }
    })();

    window.addEventListener('pagehide', () => navigator.sendBeacon('/api/game/leave/' + gameAssoc));
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  /** UI-facing callbacks */

  const onClick = async coords => {
    const activeColor = activePlayerRef.current ? 'blue' : 'red';
    if (activeColor !== webPlayerRef.current) return;

    const [r, c] = coords;
    if (cells[r]?.[c]?.active) return;

    const dataPack = { coords, player: activeColor, bomb: bombSelectedRef.current, resign: null };

    if (connectionLostRef.current) {
      stepCacheRef.current.push(dataPack);
      return;
    }

    try {
      const result = await stepMutation.mutateAsync(dataPack);
      applyStep(result);
      makeGameEndIfItEnds(result.bluePoints, result.redPoints, false, result.leftMines);
    } catch (e) {
      isEnvDev && console.error('Step error', e);
    }
  };

  const clickResign = () => {
    const color = activePlayerRef.current ? 'blue' : 'red';
    stepMutation.mutate({ resign: color });
    resignProcess(webPlayerRef.current);
  };

  const resign = () => {
    const activeColor = activePlayerRef.current ? 'blue' : 'red';
    if (webPlayerRef.current !== activeColor) return;
    showOverlay('Are u sure u want to resign?!', (
      <div className="resign">
        <a onClick={clickResign}>Yes</a>
        <a onClick={hideOverlay}>No!</a>
      </div>
    ));
  };

  return { onClick, resign };
};

export default useServerCommunication;
