/**
 * WebSocket hook for real-time appointment updates.
 * Connects to the backend WebSocket and updates React Query cache.
 */

import { useEffect, useRef, useCallback, useState } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { getCookie } from '../utils/cookies';
import { getSubdomain } from '../api/config';
import { getWebSocketUrl } from '../utils/domain';
import { Appointment } from '../types';

interface WebSocketEvent {
  id: number;
  title: string;
  start_time: string;
  end_time: string;
  status: string;
  notes: string;
  service?: {
    id: number;
    name: string;
    duration: number;
    price: string;
  };
}

interface WebSocketMessage {
  type: 'connection_established' | 'event_created' | 'event_updated' | 'event_deleted' | 'event_status_changed' | 'pong';
  event?: WebSocketEvent;
  event_id?: number;
  old_status?: string;
  new_status?: string;
  changed_fields?: string[];
  message?: string;
  user_id?: number;
  groups?: string[];
}

interface UseAppointmentWebSocketOptions {
  enabled?: boolean;
  onConnected?: () => void;
  onDisconnected?: () => void;
  onError?: (error: Event) => void;
}

// Enable WebSocket for real-time calendar updates
const WEBSOCKET_ENABLED = true;

/**
 * Transform backend event format to frontend appointment format
 */
function transformEvent(data: WebSocketEvent): Partial<Appointment> | null {
  if (!data) return null;

  const startTime = new Date(data.start_time);
  const endTime = new Date(data.end_time);
  const durationMinutes = Math.round((endTime.getTime() - startTime.getTime()) / 60000);

  return {
    id: String(data.id),
    startTime,
    durationMinutes,
    status: data.status as Appointment['status'],
    notes: data.notes,
    serviceId: data.service ? String(data.service.id) : undefined,
  };
}

/**
 * Hook for real-time appointment updates via WebSocket.
 * Handles React StrictMode's double-effect invocation gracefully.
 */
export function useAppointmentWebSocket(options: UseAppointmentWebSocketOptions = {}) {
  const { enabled = true, onConnected, onDisconnected, onError } = options;

  // Early return if WebSocket is globally disabled
  const effectivelyEnabled = enabled && WEBSOCKET_ENABLED;
  const queryClient = useQueryClient();
  const wsRef = useRef<WebSocket | null>(null);
  const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
  const pingIntervalRef = useRef<NodeJS.Timeout | null>(null);
  const reconnectAttemptsRef = useRef(0);
  const isCleaningUpRef = useRef(false);
  const maxReconnectAttempts = 5;
  const [isConnected, setIsConnected] = useState(false);

  // Store callbacks in refs to avoid effect re-runs
  const onConnectedRef = useRef(onConnected);
  const onDisconnectedRef = useRef(onDisconnected);
  const onErrorRef = useRef(onError);

  useEffect(() => {
    onConnectedRef.current = onConnected;
    onDisconnectedRef.current = onDisconnected;
    onErrorRef.current = onError;
  }, [onConnected, onDisconnected, onError]);

  // Get WebSocket URL - not a callback to avoid recreating
  const getWsUrl = () => {
    const token = getCookie('access_token');
    const subdomain = getSubdomain();

    if (!token) {
      return null;
    }

    // Use the getWebSocketUrl utility from domain.ts
    // Backend uses /ws/calendar/ endpoint with token query param
    const baseUrl = getWebSocketUrl();
    let url = `${baseUrl}calendar/?token=${token}`;
    if (subdomain) {
      url += `&subdomain=${subdomain}`;
    }
    return url;
  };

  const updateQueryCache = useCallback((message: WebSocketMessage) => {
    const queryCache = queryClient.getQueryCache();
    const appointmentQueries = queryCache.findAll({ queryKey: ['appointments'] });

    appointmentQueries.forEach((query) => {
      queryClient.setQueryData<Appointment[]>(query.queryKey, (old) => {
        if (!old) return old;

        switch (message.type) {
          case 'event_created': {
            if (!message.event) return old;
            const eventUpdate = transformEvent(message.event);
            if (!eventUpdate) return old;
            const eventId = String(message.event.id);
            // Check if appointment already exists (avoid duplicates)
            if (old.some(apt => apt.id === eventId)) {
              return old;
            }
            // Note: We only have partial data from WebSocket, so we trigger a refetch
            // to get full appointment data (customer info, etc.)
            queryClient.invalidateQueries({ queryKey: ['appointments'] });
            return old;
          }

          case 'event_updated':
          case 'event_status_changed': {
            if (!message.event) return old;
            const eventUpdate = transformEvent(message.event);
            if (!eventUpdate) return old;
            const eventId = String(message.event.id);

            // Update the matching appointment with new data
            const updated = old.map(apt => {
              if (apt.id === eventId) {
                return {
                  ...apt,
                  ...eventUpdate,
                };
              }
              return apt;
            });

            return updated;
          }

          case 'event_deleted': {
            if (!message.event_id) return old;
            const eventId = String(message.event_id);
            return old.filter(apt => apt.id !== eventId);
          }

          default:
            return old;
        }
      });
    });
  }, [queryClient]);

  // Main effect to manage WebSocket connection
  // Only depends on `enabled` - other values are read from refs or called as functions
  useEffect(() => {
    if (!effectivelyEnabled) {
      return;
    }

    // Reset cleanup flag at start of effect
    isCleaningUpRef.current = false;

    // Track the current effect's abort controller to handle StrictMode
    let effectAborted = false;

    const connect = () => {
      // Don't connect if effect was aborted or we're cleaning up
      if (effectAborted || isCleaningUpRef.current) {
        return;
      }

      const url = getWsUrl();
      if (!url) {
        return;
      }

      // Close existing connection if any
      if (wsRef.current && wsRef.current.readyState !== WebSocket.CLOSED) {
        wsRef.current.close();
      }

      const ws = new WebSocket(url);

      ws.onopen = () => {
        // Don't process if effect was aborted or cleaning up
        if (effectAborted || isCleaningUpRef.current) {
          ws.close();
          return;
        }

        reconnectAttemptsRef.current = 0;
        setIsConnected(true);
        onConnectedRef.current?.();

        // Start ping interval to keep connection alive
        if (pingIntervalRef.current) {
          clearInterval(pingIntervalRef.current);
        }
        pingIntervalRef.current = setInterval(() => {
          if (ws.readyState === WebSocket.OPEN && !effectAborted) {
            ws.send(JSON.stringify({ type: 'ping' }));
          }
        }, 30000); // Ping every 30 seconds
      };

      ws.onmessage = (event) => {
        // Ignore messages if effect was aborted
        if (effectAborted) return;

        try {
          const message: WebSocketMessage = JSON.parse(event.data);

          switch (message.type) {
            case 'connection_established':
            case 'pong':
              // Heartbeat/connection responses, ignore
              break;
            case 'event_created':
            case 'event_updated':
            case 'event_deleted':
            case 'event_status_changed':
              updateQueryCache(message);
              break;
            default:
              // Unknown message type, ignore
              break;
          }
        } catch (err) {
          // Silently ignore parse errors
        }
      };

      ws.onerror = (error) => {
        // Only call error callback if not aborted
        if (!effectAborted) {
          onErrorRef.current?.(error);
        }
      };

      ws.onclose = (event) => {
        // Don't handle if effect was aborted (expected during StrictMode)
        if (effectAborted) {
          return;
        }

        setIsConnected(false);
        onDisconnectedRef.current?.();

        // Clear ping interval
        if (pingIntervalRef.current) {
          clearInterval(pingIntervalRef.current);
          pingIntervalRef.current = null;
        }

        // Only attempt reconnection if not cleaning up
        if (!isCleaningUpRef.current && reconnectAttemptsRef.current < maxReconnectAttempts) {
          const delay = Math.min(1000 * Math.pow(2, reconnectAttemptsRef.current), 30000);

          reconnectTimeoutRef.current = setTimeout(() => {
            reconnectAttemptsRef.current++;
            connect();
          }, delay);
        }
      };

      wsRef.current = ws;
    };

    connect();

    // Cleanup function
    return () => {
      effectAborted = true;
      isCleaningUpRef.current = true;

      // Clear reconnect timeout
      if (reconnectTimeoutRef.current) {
        clearTimeout(reconnectTimeoutRef.current);
        reconnectTimeoutRef.current = null;
      }

      // Clear ping interval
      if (pingIntervalRef.current) {
        clearInterval(pingIntervalRef.current);
        pingIntervalRef.current = null;
      }

      // Close WebSocket
      if (wsRef.current) {
        wsRef.current.close();
        wsRef.current = null;
      }

      setIsConnected(false);
    };
  }, [effectivelyEnabled]); // Only re-run when enabled changes

  const reconnect = useCallback(() => {
    isCleaningUpRef.current = false;
    reconnectAttemptsRef.current = 0;

    // Close existing connection
    if (wsRef.current) {
      wsRef.current.close();
    }

    // Connection will be re-established by the effect when we force re-render
    // For now, we'll rely on the onclose handler to trigger reconnection
  }, []);

  const disconnect = useCallback(() => {
    isCleaningUpRef.current = true;

    // Clear reconnect timeout
    if (reconnectTimeoutRef.current) {
      clearTimeout(reconnectTimeoutRef.current);
      reconnectTimeoutRef.current = null;
    }

    // Clear ping interval
    if (pingIntervalRef.current) {
      clearInterval(pingIntervalRef.current);
      pingIntervalRef.current = null;
    }

    // Close WebSocket
    if (wsRef.current) {
      wsRef.current.close();
      wsRef.current = null;
    }

    setIsConnected(false);
  }, []);

  return {
    isConnected,
    reconnect,
    disconnect,
  };
}
