'use client';

import {
  createContext,
  useContext,
  useEffect,
  useState,
  useSyncExternalStore,
  type ReactNode,
} from 'react';
import { io, type Socket } from 'socket.io-client';

import { useSession } from '@/lib/auth/client';
import { usePortStore } from '@/stores/ui-store';

interface SocketContextValue {
  /** Stable socket instance reference. Persists across reconnects - socket.io's
   *  built-in reconnection re-establishes the underlying transport without
   *  changing the JS object, so this stays valid as long as the session and
   *  port are unchanged. Consumers should NOT null-check this for "is online";
   *  use `isConnected` instead. */
  socket: Socket | null;
  /** Live transport state. Flips false on disconnect and back to true on
   *  reconnect. Use this if you need to surface offline UX; the socket itself
   *  stays subscribed to the same event handlers. */
  isConnected: boolean;
}

const SocketContext = createContext<SocketContextValue>({ socket: null, isConnected: false });

/** Returns true once the component has mounted on the client. Avoids calling
 *  better-auth's `useSession()` (which dispatches React hooks via nanostores)
 *  during the SSR pass - that combo throws "Invalid hook call" intermittently
 *  in Next.js 15 dev mode. */
function useHasMounted(): boolean {
  return useSyncExternalStore(
    () => () => {},
    () => true,
    () => false,
  );
}

export function SocketProvider({ children }: { children: ReactNode }) {
  const hasMounted = useHasMounted();
  return hasMounted ? (
    <SocketProviderClient>{children}</SocketProviderClient>
  ) : (
    <SocketContext.Provider value={{ socket: null, isConnected: false }}>
      {children}
    </SocketContext.Provider>
  );
}

function SocketProviderClient({ children }: { children: ReactNode }) {
  const { data: session } = useSession();
  const currentPortId = usePortStore((s) => s.currentPortId);
  const [socket, setSocket] = useState<Socket | null>(null);
  const [isConnected, setIsConnected] = useState(false);

  useEffect(() => {
    if (!session?.user || !currentPortId) {
      // Tear down the socket on session/port loss - canonical
      // subscription-cleanup pattern.
      // eslint-disable-next-line react-hooks/set-state-in-effect
      setSocket(null);
      setIsConnected(false);
      return;
    }

    // Connect to whatever origin the page was loaded from - `io()` with
    // no URL defaults to window.location. This used to read
    // NEXT_PUBLIC_APP_URL, which baked the deploy-time canonical URL
    // (localhost in dev) and broke realtime when the same dev server
    // was hit from a LAN IP.
    const s = io({
      path: '/socket.io/',
      withCredentials: true,
      auth: { portId: currentPortId },
      transports: ['websocket', 'polling'],
    });

    // Set the socket reference immediately and keep it stable across the
    // session+port lifetime. socket.io reconnects internally; the same
    // instance survives transient drops, and any handlers registered via
    // `socket.on(...)` stay attached. Previously we set/unset `socket` on
    // connect/disconnect, which made the React context flip to null on every
    // network blip and silently killed every `useRealtimeInvalidation`
    // subscription session-wide.
    setSocket(s);

    s.on('connect', () => setIsConnected(true));
    s.on('disconnect', () => setIsConnected(false));

    return () => {
      s.disconnect();
      setSocket(null);
      setIsConnected(false);
    };
  }, [session?.user, currentPortId]);

  return (
    <SocketContext.Provider value={{ socket, isConnected }}>{children}</SocketContext.Provider>
  );
}

/** Returns the Socket.IO client instance. The reference is stable for the
 *  duration of a session+port, even across transient disconnects. */
export function useSocket(): Socket | null {
  return useContext(SocketContext).socket;
}

/** True while the socket transport is connected. Flips false on disconnect,
 *  back to true on reconnect. Useful for surfacing an "offline" indicator. */
export function useIsSocketConnected(): boolean {
  return useContext(SocketContext).isConnected;
}
