/**
 * React context provider and hooks for imks real-time messaging.
 */

import {
  createContext,
  type ReactNode,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';

import { AuthService } from '@/client/services/AuthService';
import { useUserContext } from '@/contexts/UserContext';

import { SocketClient } from './client';
import type { ClientEventMap, ConnectionState, ServerEventMap } from './types';

export interface SocketContextValue {
  /** Current connection state. */
  state: ConnectionState;
  /** Emit a client event to the server. */
  emit: <E extends keyof ClientEventMap>(event: E, ...args: ClientEventMap[E]) => void;
  /** Subscribe to server events. Returns unsubscribe function. */
  on: <E extends keyof ServerEventMap>(
    event: E,
    handler: (...args: ServerEventMap[E]) => void,
  ) => () => void;
}

const SocketContext = createContext<SocketContextValue | null>(null);

type AnyServerHandler = (...args: unknown[]) => void;

interface SocketProviderProps {
  children: ReactNode;
  /** imks server URL, e.g. http://localhost:3000 or ws://localhost:3000 */
  imksUrl: string;
  /** Engine.IO path. Default: /engine.io/ */
  enginePath?: string;
}

export function SocketProvider({ children, imksUrl, enginePath }: SocketProviderProps) {
  const { isAuthenticated } = useUserContext();
  const socketRef = useRef<SocketClient | null>(null);
  const handlersRef = useRef(new Map<string, Set<AnyServerHandler>>());
  const socketSubscriptionsRef = useRef<(() => void)[]>([]);
  const reconnectTimer = useRef<ReturnType<typeof setInterval> | undefined>(undefined);
  const [state, setState] = useState<ConnectionState>('disconnected');

  const clearSocketSubscriptions = useCallback(() => {
    for (const unsubscribe of socketSubscriptionsRef.current) {
      unsubscribe();
    }
    socketSubscriptionsRef.current = [];
  }, []);

  const attachRegisteredHandlers = useCallback(
    (socket: SocketClient) => {
      clearSocketSubscriptions();
      for (const [event, handlers] of handlersRef.current) {
        for (const handler of handlers) {
          const unsubscribe = socket.on(
            event as keyof ServerEventMap,
            handler as (...args: ServerEventMap[keyof ServerEventMap]) => void,
          );
          socketSubscriptionsRef.current.push(unsubscribe);
        }
      }
    },
    [clearSocketSubscriptions],
  );

  useEffect(() => {
    if (!isAuthenticated) {
      clearSocketSubscriptions();
      socketRef.current?.disconnect();
      socketRef.current = null;
      setState('disconnected');
      return;
    }

    let cancelled = false;

    async function connectWithToken() {
      try {
        const resp = await AuthService.authWsToken();
        const wsToken = resp.data.token;

        if (cancelled) return;

        const socket = new SocketClient({
          url: imksUrl,
          token: wsToken,
          enginePath,
        });

        socket.onStateChange((newState) => {
          if (!cancelled) setState(newState);
        });

        attachRegisteredHandlers(socket);
        socketRef.current = socket;
        await socket.connect();
      } catch (err) {
        console.error('[Socket] Connection failed:', err);
        if (!cancelled) setState('disconnected');
      }
    }

    connectWithToken();

    return () => {
      cancelled = true;
      clearSocketSubscriptions();
      socketRef.current?.disconnect();
      socketRef.current = null;
    };
  }, [isAuthenticated, imksUrl, enginePath, attachRegisteredHandlers, clearSocketSubscriptions]);

  useEffect(() => {
    if (!isAuthenticated || state !== 'connected') return;

    reconnectTimer.current = setInterval(
      async () => {
        try {
          const resp = await AuthService.authWsToken();
          const wsToken = resp.data.token;

          if (socketRef.current) {
            await socketRef.current.refreshToken(wsToken);
          }
        } catch {
          console.warn('[Socket] Token refresh failed, will retry');
        }
      },
      25 * 60 * 1000,
    );

    return () => {
      clearInterval(reconnectTimer.current);
      reconnectTimer.current = undefined;
    };
  }, [isAuthenticated, state]);

  const emit = useCallback(
    <E extends keyof ClientEventMap>(event: E, ...args: ClientEventMap[E]) => {
      socketRef.current?.emit(event, ...args);
    },
    [],
  );

  const on = useCallback(
    <E extends keyof ServerEventMap>(event: E, handler: (...args: ServerEventMap[E]) => void) => {
      const eventName = event as string;
      let handlers = handlersRef.current.get(eventName);
      if (!handlers) {
        handlers = new Set();
        handlersRef.current.set(eventName, handlers);
      }

      const anyHandler = handler as AnyServerHandler;
      handlers.add(anyHandler);
      const socketUnsubscribe = socketRef.current?.on(event, handler);

      return () => {
        handlers?.delete(anyHandler);
        if (handlers?.size === 0) {
          handlersRef.current.delete(eventName);
        }
        socketUnsubscribe?.();
      };
    },
    [],
  );

  const value = useMemo<SocketContextValue>(() => ({ state, emit, on }), [state, emit, on]);

  return <SocketContext.Provider value={value}>{children}</SocketContext.Provider>;
}

export function useSocket(): SocketContextValue {
  const ctx = useContext(SocketContext);
  if (!ctx) {
    throw new Error('useSocket must be used within a <SocketProvider>');
  }
  return ctx;
}
