/**
 * This file is part of the SplendidBear Websites' projects.
 *
 * Copyright (c) 2026 @ www.splendidbear.org
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

import React, { useEffect, useRef } from 'react';
import { useGame } from '@mine-contexts';
import { DESC, IMAGES } from '@mine-utils';
import { ChallengeCountdown, WaitingOverlayContent } from '@mine-components';
import { useGameDataProvider, useStepTimer } from '@mine-hooks';

const useServerCommunication = (gameAssoc, gameInherited, opponentName, isEnvDev) => {
  const {
    /** Async-safe refs */
    webPlayerRef, activePlayerRef, bombSelectedRef, connectionLostRef, lastClickedRef, endRef,
    /** State setters */
    setCells, setGridReady, setGameUuid,
    /** Sync helpers */
    syncWebPlayer, syncActivePlayer, syncBombSelected, syncConnLost, syncRed, syncBlue,
    /** Game logic */
    showOverlay, hideOverlay, applyStep, makeGameEndIfItEnds, resignProcess,
    /** Current cells snapshot (for active-check in onClick) */
    cells,
  } = useGame();

  /** Get all API queries and mutations from data provider */
  const {
    connectQuery,
    startMutation,
    joinMutation,
    stepMutation,
    heartbeatMutation,
    challengeRespondMutation,
    leaveMutation,
  } = useGameDataProvider(gameAssoc);

  const eventSourceRef = useRef(null);
  const rpcUsersRef = useRef(null);
  const stepCacheRef = useRef([]);
  const lastStepRef = useRef(null);
  const isGameFinishedRef = useRef(false);
  const { getStepElapsed, resetStepTimer, startNewTurn } = useStepTimer();
  const isGameRunningRef = useRef(false);
  const lastActivePlayerRef = useRef(null);
  const heartbeatPubIntervalRef = useRef(null);
  const opponentLastSeenRef = useRef(0);
  const isTrueRestoredRef = useRef(false);

  const HEARTBEAT_INTERVAL_MS = 1500;

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

  const wInit = (revealedCells = [], lastStep = {}, gameState = {}, isGameFinished = false) => {
    /** Detect if this is a restored game */
    const isRestoredGame = 0 < revealedCells.length;
    isTrueRestoredRef.current = isRestoredGame;

    /** Store game finished status */
    isGameFinishedRef.current = isGameFinished;

    /** Apply game state (points, bonus) immediately for restored games */
    if (0 < Object.keys(gameState).length) {
      const {
        redPoints = 0,
        bluePoints = 0,
        redBonusPoints = 0,
        blueBonusPoints = 0,
        redBonusStats = {},
        blueBonusStats = {},
      } = gameState;
      syncRed(p => ({
        ...p,
        mines: redPoints,
        bonusPoints: redBonusPoints,
        bonusStats: redBonusStats,
      }));
      syncBlue(p => ({
        ...p,
        mines: bluePoints,
        bonusPoints: blueBonusPoints,
        bonusStats: blueBonusStats,
      }));
    }

    /** Apply revealed cells immediately (not in setTimeout) */
    if (0 < revealedCells.length) {
      setCells(prev => {
        let next = prev.map(r => [...r]);
        revealedCells.forEach(({ row, col, value, player }) => {
          if (next[row][col].active) return;
          /** Check if this cell is the last step for either player */
          const isRedLastStep = lastStep.red && lastStep.red.player === player && lastStep.red.row === row && lastStep.red.col === col;
          const isBlueLastStep = lastStep.blue && lastStep.blue.player === player && lastStep.blue.row === row && lastStep.blue.col === col;
          const patch = 'm' === value
            ? { currentImage: IMAGES.flag(player), currentObj: 'm', active: true }
            : { currentImage: value, currentObj: value, active: true };
          if (isRedLastStep || isBlueLastStep) {
            patch.lastClickedRed = 'red' === player;
            patch.lastClickedBlue = 'blue' === player;
          }
          next[row][col] = { ...next[row][col], ...patch };
        });
        return next;
      });
    }

    /** Update the lastClickedRef so applyStep knows about it */
    if (lastStep.red) {
      lastClickedRef.current = {
        ...lastClickedRef.current,
        red: [lastStep.red.row, lastStep.red.col],
      };
    }
    if (lastStep.blue) {
      lastClickedRef.current = {
        ...lastClickedRef.current,
        blue: [lastStep.blue.row, lastStep.blue.col],
      };
    }

    /** Determine overlay message */
    let overlayTitle, overlaySubtitle;

    if (isGameFinished) {
      /** Game is finished - show game over message */
      const redPoints = gameState.redPoints ?? 0;
      const bluePoints = gameState.bluePoints ?? 0;
      const winner = redPoints > bluePoints ? 'Red' : 'Blue';
      overlayTitle = `${winner} wins the game!`;
      overlaySubtitle = 'Play again!';
      /** Mark the game as ended */
      endRef.current = true;
    } else if (isRestoredGame) {
      overlayTitle = 'Waiting for opponent to reconnect...';
      overlaySubtitle = gameAssoc ? (
        <WaitingOverlayContent
          shareUrl={`${window.location.origin}/play/${gameAssoc}`}
          currentGameAssoc={gameAssoc}
          opponentName={opponentName}
          inviteOnly
        />
      ) : (
        <div style={{ textAlign: 'center', padding: '20px' }}>
          <p>Waiting for opponent to join...</p>
        </div>
      );
    } else {
      overlayTitle = 'Choose an opponent!';
      overlaySubtitle = gameAssoc ? (
        <WaitingOverlayContent
          shareUrl={`${window.location.origin}/play/${gameAssoc}`}
          currentGameAssoc={gameAssoc}
        />
      ) : '';
    }

    showOverlay(overlayTitle, overlaySubtitle);

    /** Use Promise.resolve to defer setGridReady slightly to ensure overlay is rendered first */
    Promise.resolve().then(() => setGridReady(true));
  };

  const makeGameStart = payload => {
    /** Don't start a finished game */
    if (isGameFinishedRef.current) {
      return;
    }

    /** If game is being restored and has a most recent step, determine starter based on that */
    let starterIsBlue;

    /** lastStepRef contains the single most recent step from the server */
    if (lastStepRef.current && lastStepRef.current.player) {
      /** The NEXT player is opposite of who made the last step */
      starterIsBlue = 'red' === lastStepRef.current.player; // If red played last, blue plays next
    } else {
      /** New game: blue always starts */
      starterIsBlue = true;
    }

    const starterColor = starterIsBlue ? 'blue' : 'red';
    const starterVal = starterIsBlue ? 1 : 0;
    const starterDesc = starterColor === webPlayerRef.current ? DESC.you : DESC.buddy;
    syncActivePlayer(starterVal);
    syncRed(p => ({
      ...p,
      name: payload.users.red || payload.users.redAnon || p.name,
      registered: !!payload.users.red,
      avatar: payload.users.redAvatar ?? null,
      desc: 'red' === starterColor ? starterDesc : '',
      active: 'red' === starterColor,
    }));
    syncBlue(p => ({
      ...p,
      name: payload.users.blue || payload.users.blueAnon || p.name,
      registered: !!payload.users.blue,
      avatar: payload.users.blueAvatar ?? null,
      desc: 'blue' === starterColor ? starterDesc : '',
      active: 'blue' === starterColor,
    }));
    isGameRunningRef.current = true;
    lastActivePlayerRef.current = starterVal;
    startNewTurn();
    resetStepTimer();
    /**
     * For a truly restored game, keep the "Waiting for opponent..." overlay
     * up until we actually see a heartbeat from the other player.
     */
    if (!endRef.current && (!isTrueRestoredRef.current || 0 !== opponentLastSeenRef.current)) {
      hideOverlay();
    }
  };

  const publishHeartbeat = () => {
    const me = webPlayerRef.current;
    if (!me || endRef.current) return;
    heartbeatMutation.mutate(me);
  };

  const startHeartbeat = () => {
    if (heartbeatPubIntervalRef.current) return;
    publishHeartbeat();
    heartbeatPubIntervalRef.current = setInterval(publishHeartbeat, HEARTBEAT_INTERVAL_MS);
  };

  const stopHeartbeat = () => {
    if (heartbeatPubIntervalRef.current) {
      clearInterval(heartbeatPubIntervalRef.current);
      heartbeatPubIntervalRef.current = null;
    }
  };

  /** 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');
    }

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

  const wUnsubscribe = payload => {
    isEnvDev && console.info(payload.msg);
    const isAuthenticated = '1' === document.getElementById('mine-wrapper')?.dataset.isAuthenticated;
    const redirectPath = isAuthenticated ? '/profile' : '/';
    const buttonText = isAuthenticated ? 'My Profile' : 'Homepage';
    const buttonIcon = isAuthenticated ? 'fa-user' : 'fa-house';

    showOverlay(
      'The connection has been lost w/ your friend...',
      (
        <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '12px', width: '100%' }}>
          <p style={{ margin: 0 }}>Please, restart the game!</p>
          <a
            className="game-overlay-profile"
            href={redirectPath}
            title={isAuthenticated ? 'Go to your profile' : 'Go to homepage'}
            aria-label={isAuthenticated ? 'Go to your profile' : 'Go to homepage'}
          >
            <i className={`fa ${buttonIcon}`} />
            {buttonText}
          </a>
        </div>
      ),
    );
  };

  const wChallenge = payload => {
    const { challengerName, challengerGameAssoc } = payload;
    let declineTimeout = null;

    const handleAccept = () => {
      clearTimeout(declineTimeout);
      challengeRespondMutation.mutate(
        { challengerGameAssoc, accepted: true, targetGameAssoc: gameAssoc },
        {
          onSuccess: () => {
            showOverlay('Challenge accepted!', 'Waiting for the challenger to join...');
          },
        },
      );
    };

    const handleDecline = () => {
      clearTimeout(declineTimeout);
      challengeRespondMutation.mutate(
        { challengerGameAssoc, accepted: false, targetGameAssoc: gameAssoc },
        {
          onSuccess: () => {
            showOverlay('We are waiting for your opponent...', gameAssoc ? (
              <WaitingOverlayContent
                shareUrl={`${window.location.origin}/play/${gameAssoc}`}
                currentGameAssoc={gameAssoc}
              />
            ) : '');
          },
        },
      );
    };

    declineTimeout = setTimeout(handleDecline, 30000);

    showOverlay(
      challengerName + ' wants to challenge you!',
      <ChallengeCountdown onAccept={handleAccept} onDecline={handleDecline} />,
    );
  };

  const wChallengeResponse = payload => {
    if (payload.accepted) {
      window.location.href = '/play/' + payload.targetGameAssoc;
    } else {
      window.dispatchEvent(new CustomEvent('challenge-declined'));
    }
  };

  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);

        /**
         * Detect if turn switched (other player made a move)
         * After their move, it's now our turn (or the opposite player's turn)
         */
        if (lastActivePlayerRef.current !== activePlayerRef.current) {
          startNewTurn();
          lastActivePlayerRef.current = activePlayerRef.current;
        }

        applyStep(payload.data);
        if (payload.data.uuid && !endRef.current) {
          setGameUuid(payload.data.uuid);
        }
        makeGameEndIfItEnds(payload.data.bluePoints, payload.data.redPoints, false, payload.data.leftMines);
      } else {
        resignProcess(payload.data.resign, payload.data.uuid);
      }
    }
  };

  const handleMercureMessage = payload => {
    if (undefined !== payload.type) {
      if ('challenge' === payload.type) wChallenge(payload);
      else if ('challenge-response' === payload.type) wChallengeResponse(payload);
      else if ('heartbeat' === payload.type) {
        const me = webPlayerRef.current;
        if (me && payload.color && payload.color !== me) {
          const wasFirst = 0 === opponentLastSeenRef.current;
          opponentLastSeenRef.current = Date.now();
          if (wasFirst && isTrueRestoredRef.current && !endRef.current) {
            hideOverlay();
          }
        }
      }
      return;
    }

    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;
          lastStepRef.current = serverData.mostRecentStep || null;

          /** Pass game state (points, bonus) to wInit */
          const gameState = {
            redPoints: serverData.redPoints ?? 0,
            bluePoints: serverData.bluePoints ?? 0,
            redBonusPoints: serverData.redBonusPoints ?? 0,
            blueBonusPoints: serverData.blueBonusPoints ?? 0,
            redBonusStats: serverData.redBonusStats ?? {},
            blueBonusStats: serverData.blueBonusStats ?? {},
          };
          const isGameFinished = serverData.gameFinished ?? false;
          wInit(serverData.revealedCells || [], serverData.lastStep || {}, gameState, isGameFinished);

          /** Open event source after showing overlay */
          openEventSource();
        } else {
          const startResponse = await startMutation.mutateAsync();
          if (!startResponse?.success) {
            showOverlay('Error', 'Failed to start game. Please try again.');
            isEnvDev && console.error('Start game failed:', startResponse);
            return;
          }
          openEventSource();
          wInit();
        }

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

    window.addEventListener('pagehide', () => {
      leaveMutation.mutate();
    });

    return () => {
      stopHeartbeat();
    };
    // 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 stepElapsed = getStepElapsed(activePlayerRef.current, isGameRunningRef.current);
    const dataPack = { coords, player: activeColor, bomb: bombSelectedRef.current, resign: null, stepElapsed };

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

    try {
      const result = await stepMutation.mutateAsync(dataPack);
      applyStep(result);

      if (result.uuid && !endRef.current) {
        setGameUuid(result.uuid);
      }

      makeGameEndIfItEnds(result.bluePoints, result.redPoints, false, result.leftMines);
    } catch (e) {
      isEnvDev && console.error('Step error', e);
    }
  };

  const clickResign = () => {
    const color = activePlayerRef.current ? 'blue' : 'red';
    const stepElapsed = getStepElapsed(activePlayerRef.current, isGameRunningRef.current);

    stepMutation.mutate(
      { resign: color, stepElapsed },
      {
        onSuccess: result => {
          if (result?.uuid && !endRef.current) {
            resignProcess(webPlayerRef.current, result.uuid);
          }
        },
      },
    );
  };

  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;
