import { useCallback, useEffect, useRef, useState } from "react";
import { io } from "socket.io-client";
import { logger } from "~/lib/logger";

interface UseDraftSocketReturn {
  isConnected: boolean;
  connectionError: string | null;
  isReconnecting: boolean;
  reconnectCount: number;
  /** Increments each time a new socket instance is created. Include in
   *  useEffect deps so handler effects re-register on socket recreation. */
  socketVersion: number;
  // eslint-disable-next-line typescript/no-explicit-any -- socket.io callbacks are untyped at the hook level
  on: (event: string, callback: (...args: any[]) => void) => void;
  // eslint-disable-next-line typescript/no-explicit-any
  off: (event: string, callback?: (...args: any[]) => void) => void;
  emit: (event: string, ...args: unknown[]) => void;
}

export function useDraftSocket(seasonId: string, teamId?: string): UseDraftSocketReturn {
  const socketRef = useRef<ReturnType<typeof io> | null>(null);
  const hasConnectedOnce = useRef(false);
  const [isConnected, setIsConnected] = useState(false);
  const [connectionError, setConnectionError] = useState<string | null>(null);
  const [isReconnecting, setIsReconnecting] = useState(false);
  const [reconnectCount, setReconnectCount] = useState(0);
  const [socketVersion, setSocketVersion] = useState(0);

  useEffect(() => {
    // Reset per-socket state so a new seasonId/teamId doesn't inherit the previous
    // socket's connect history and falsely treat its first connect as a reconnect.
    hasConnectedOnce.current = false;

    const socket = io({
      path: "/socket.io",
      transports: ["websocket", "polling"],
      reconnectionAttempts: 10,
    });

    socketRef.current = socket;
    setSocketVersion((v) => v + 1);

    socket.on("connect", () => {
      logger.log("Connected to Socket.IO:", socket.id);
      const isReconnect = hasConnectedOnce.current;
      hasConnectedOnce.current = true;
      setIsConnected(true);
      setConnectionError(null);
      setIsReconnecting(false);
      socket.emit("join-draft", seasonId, teamId);
      if (isReconnect) {
        setReconnectCount((c) => c + 1);
      }
    });

    socket.on("disconnect", (reason) => {
      logger.log("Disconnected from Socket.IO:", reason);
      setIsConnected(false);
      if (reason === "io server disconnect") {
        setConnectionError("Server disconnected. Please refresh the page.");
      } else {
        setIsReconnecting(true);
      }
    });

    socket.on("connect_error", (error) => {
      logger.error("Socket.IO connection error:", error);
      // Don't set connectionError here — reconnect_attempt fires immediately after
      // and would clear it again, causing the error overlay to flicker on every
      // retry. Only show a hard error once all attempts are exhausted (reconnect_failed).
      // Ensure the "reconnecting" overlay is visible rather than the initial spinner.
      setIsReconnecting(true);
    });

    socket.io.on("reconnect_attempt", () => {
      logger.log("Attempting to reconnect...");
      setIsReconnecting(true);
      setConnectionError(null);
    });

    socket.io.on("reconnect_failed", () => {
      logger.error("Reconnection failed");
      setConnectionError("Failed to reconnect. Please refresh the page.");
      setIsReconnecting(false);
    });

    const handleOffline = () => {
      // Mark as reconnecting immediately — the OS fires this before Socket.IO's
      // heartbeat would detect the drop (~15s later). Note: "offline" can fire
      // spuriously on mobile; handleReturn corrects the state if the socket is
      // still alive when the network returns.
      setIsConnected(false);
      setIsReconnecting(true);
    };

    // Unified handler for both "network returned" (online event) and
    // "browser tab/app regained focus" (visibilitychange). Two cases:
    //   - Socket dropped: reconnect immediately, bypassing backoff.
    //   - Socket alive: restore UI state and rejoin the draft room so the page
    //     revalidates and picks up any events missed while the client was away.
    const handleReturn = () => {
      if (!socketRef.current?.connected) {
        socketRef.current?.connect();
      } else {
        setIsConnected(true);
        setIsReconnecting(false);
        socketRef.current.emit("join-draft", seasonId, teamId);
        setReconnectCount((c) => c + 1);
      }
    };

    // visibilitychange fires on both hide and show — only act on show.
    const handleVisibilityChange = () => {
      if (document.visibilityState === "visible") handleReturn();
    };

    window.addEventListener("offline", handleOffline);
    window.addEventListener("online", handleReturn);
    document.addEventListener("visibilitychange", handleVisibilityChange);

    return () => {
      socket.io.off("reconnect_attempt");
      socket.io.off("reconnect_failed");
      window.removeEventListener("offline", handleOffline);
      window.removeEventListener("online", handleReturn);
      document.removeEventListener("visibilitychange", handleVisibilityChange);
      logger.log("Leaving draft room:", seasonId);
      socket.emit("leave-draft", seasonId);
      socket.disconnect();
    };
  }, [seasonId, teamId]);

  // These read socketRef.current at call time, so calls made before the effect
  // has run (socketRef.current === null) are silently no-ops. Consumers should
  // only call them inside their own useEffect, not during render or synchronously
  // after mount.
  // eslint-disable-next-line typescript/no-explicit-any -- socket.io callbacks are untyped at the hook level
  const on = useCallback((event: string, callback: (...args: any[]) => void) => {
    socketRef.current?.on(event, callback);
  }, []);

  // eslint-disable-next-line typescript/no-explicit-any
  const off = useCallback((event: string, callback?: (...args: any[]) => void) => {
    socketRef.current?.off(event, callback);
  }, []);

  const emit = useCallback((event: string, ...args: unknown[]) => {
    socketRef.current?.emit(event, ...args);
  }, []);

  return {
    isConnected,
    connectionError,
    isReconnecting,
    reconnectCount,
    socketVersion,
    on,
    off,
    emit,
  };
}
