/**
 * WebSocket hook for real-time job status updates
 * 
 * Provides WebSocket connections for:
 * 1. Individual job status updates: useJobStatusWebSocket(jobId)
 * 2. Job list updates: useJobStatusWebSocket() without jobId
 */
import { useEffect, useState, useRef, useCallback } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { useAuthStore } from '../lib/auth';
import { apiClient } from '../lib/api';

export interface JobStatusUpdate {
  job_id: string;
  status: string;
  updated_at: string;
  job_title?: string;
  message?: string;
  progress?: number;
  metadata?: Record<string, unknown>;
}

export interface WebSocketMessage {
  type: 'connection_established' | 'job_status_update' | 'job_list_update'
      | 'language_qc_comment' | 'language_editing_started' | 'language_qc_assigned';
  data?: JobStatusUpdate & Record<string, unknown>;
  job_id?: string;
  lang?: string;
  scope?: string;
  timestamp?: string;
}

export type ConnectionStatus = 'connecting' | 'connected' | 'disconnected' | 'error';

interface UseJobStatusWebSocketOptions {
  /**
   * Whether to automatically reconnect on connection loss
   * @default true
   */
  autoReconnect?: boolean;
  
  /**
   * Maximum number of reconnection attempts
   * @default 5
   */
  maxReconnectAttempts?: number;
  
  /**
   * Initial reconnection delay in milliseconds
   * @default 1000
   */
  reconnectDelay?: number;
  
  /**
   * Whether to log debug information
   * @default false
   */
  debug?: boolean;
  
  
  /**
   * Custom toast handler function for status updates
   */
  onStatusUpdate?: (update: JobStatusUpdate) => void;
  
  /**
   * Toast handler for connection status changes
   */
  onConnectionChange?: (status: ConnectionStatus) => void;

  /**
   * Raw message handler — called for every parsed WS message regardless of type
   */
  onRawMessage?: (msg: WebSocketMessage) => void;

  /**
   * Called when the WebSocket closes with a terminal code (1000, 4001, 4003, 4004, 4403).
   * Use this to surface the close reason to the user.
   */
  onTerminalClose?: (code: number, reason: string) => void;
}

interface UseJobStatusWebSocketReturn {
  /** Current connection status */
  connectionStatus: ConnectionStatus;
  
  /** Latest received message */
  lastMessage: WebSocketMessage | null;
  
  /** Latest job status update */
  lastUpdate: JobStatusUpdate | null;
  
  /** Manual reconnect function */
  reconnect: () => void;
  
  /** Disconnect function */
  disconnect: () => void;
  
  /** Send message (for heartbeat, etc.) */
  sendMessage: (message: string) => void;
}

/**
 * WebSocket hook for job status updates
 * 
 * @param jobId - Optional job ID for specific job updates. If not provided, subscribes to all job updates
 * @param options - Configuration options
 */
export function useJobStatusWebSocket(
  jobId?: string,
  options: UseJobStatusWebSocketOptions = {}
): UseJobStatusWebSocketReturn {
  const {
    autoReconnect = true,
    maxReconnectAttempts = 15,
    reconnectDelay = 1000,
    debug = false,
    onStatusUpdate,
    onConnectionChange,
    onRawMessage,
    onTerminalClose,
  } = options;

  const queryClient = useQueryClient();
  const { isAuthenticated } = useAuthStore();
  
  // Get access token from API client instead of auth store
  const accessToken = apiClient.getAccessToken();
  
  const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus>('disconnected');
  const [lastMessage, setLastMessage] = useState<WebSocketMessage | null>(null);
  const [lastUpdate, setLastUpdate] = useState<JobStatusUpdate | null>(null);
  
  const wsRef = useRef<WebSocket | null>(null);
  const reconnectAttemptsRef = useRef(0);
  const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
  const heartbeatIntervalRef = useRef<NodeJS.Timeout | null>(null);
  const mountedRef = useRef(true);
  
  // Cache recent updates to prevent duplicates
  const recentUpdatesRef = useRef<Map<string, { status: string; timestamp: number }>>(new Map());

  const log = useCallback((...args: unknown[]) => {
    if (debug) {
      console.log('[WebSocket]', ...args);
    }
  }, [debug]);

  const getWebSocketUrl = useCallback(() => {
    // Get API base URL from environment
    const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000';
    const apiUrl = new URL(apiBaseUrl);

    // Use wss:// for https, ws:// for http
    const protocol = apiUrl.protocol === 'https:' ? 'wss:' : 'ws:';
    const host = apiUrl.host; // Use backend host, not window.location.host

    // Include pathname from API base URL (e.g., /video-accessibility-back)
    const apiPathname = apiUrl.pathname.replace(/\/$/, ''); // Remove trailing slash
    const wsPath = `/api/v1/ws/jobs`;
    const fullPath = jobId ? `${apiPathname}${wsPath}/${jobId}` : `${apiPathname}${wsPath}`;
    const token = encodeURIComponent(accessToken || '');
    return `${protocol}//${host}${fullPath}?token=${token}`;
  }, [jobId, accessToken]);

  const handleStatusUpdate = useCallback((update: JobStatusUpdate) => {
    // Check for duplicate status updates within the last 5 seconds
    const updateKey = `${update.job_id}:${update.status}`;
    const now = Date.now();
    const recent = recentUpdatesRef.current.get(updateKey);
    
    if (recent && (now - recent.timestamp) < 5000) {
      // Skip duplicate status update within 5 seconds
      log('Skipping duplicate status update:', updateKey);
      return;
    }
    
    // Store this update
    recentUpdatesRef.current.set(updateKey, { status: update.status, timestamp: now });
    
    // Clean up old entries (older than 30 seconds)
    const cutoff = now - 30000;
    for (const [key, value] of recentUpdatesRef.current.entries()) {
      if (value.timestamp < cutoff) {
        recentUpdatesRef.current.delete(key);
      }
    }
    
    // Call custom handler if provided
    if (onStatusUpdate) {
      onStatusUpdate(update);
    }
  }, [onStatusUpdate, log]);

  const handleConnectionChange = useCallback((status: ConnectionStatus) => {
    // Call custom handler if provided
    if (onConnectionChange) {
      onConnectionChange(status);
    }
  }, [onConnectionChange]);

  const updateQueryCache = useCallback((update: JobStatusUpdate) => {
    // Handle asset_ready events specially
    if (update.status === 'asset_ready') {
      // Invalidate downloads cache to refetch signed URLs
      if (update.job_id) {
        queryClient.invalidateQueries({
          queryKey: ['jobs', update.job_id, 'downloads']
        });
        // Also invalidate job details to get latest outputs
        queryClient.invalidateQueries({
          queryKey: ['jobs', update.job_id]
        });
      }
      return; // Don't update status cache for asset events
    }

    // For status updates, invalidate ALL job queries to ensure filtered lists refresh
    if (update.job_id) {
      // Invalidate specific job
      queryClient.invalidateQueries({
        queryKey: ['jobs', update.job_id]
      });

      // Invalidate ALL job list queries (including filtered ones like QC, Final Review)
      // This ensures QCList and FinalList refresh when jobs change status
      queryClient.invalidateQueries({
        queryKey: ['jobs'],
        exact: false // Match all queries starting with ['jobs']
      });
    }
  }, [queryClient]);

  const handleMessage = useCallback((event: MessageEvent) => {
    try {
      // Silently discard server heartbeat/keepalive frames
      if (typeof event.data === 'string' && (event.data === 'pong' || event.data === 'keepalive')) {
        log('Received heartbeat frame:', event.data);
        return;
      }
      
      const message: WebSocketMessage = JSON.parse(event.data);
      log('Received message:', message);
      
      setLastMessage(message);
      onRawMessage?.(message);

      if (message.type === 'job_status_update' || message.type === 'job_list_update') {
        if (message.data) {
          setLastUpdate(message.data as JobStatusUpdate);
          updateQueryCache(message.data as JobStatusUpdate);
          handleStatusUpdate(message.data as JobStatusUpdate);
        }
      }
    } catch (error) {
      console.error('[WebSocket] Failed to parse WebSocket message:', error, 'Raw data:', event.data);
    }
  }, [log, onRawMessage, updateQueryCache, handleStatusUpdate]);

  const handleOpen = useCallback(() => {
    console.log('[WebSocket] Connected successfully!');
    log('WebSocket connected');
    setConnectionStatus('connected');
    handleConnectionChange('connected');
    reconnectAttemptsRef.current = 0;
    
    // Start heartbeat — must be < Apache mod_proxy_wstunnel idle timeout.
    // Mod Comms incident 2026-03-18: 25 s was insufficient; 20 s is safe.
    heartbeatIntervalRef.current = setInterval(() => {
      if (wsRef.current?.readyState === WebSocket.OPEN) {
        wsRef.current.send('ping');
      }
    }, 20000);
  }, [log, handleConnectionChange]);

  const handleClose = useCallback((event: CloseEvent) => {
    log('WebSocket closed:', event.code, event.reason);
    setConnectionStatus('disconnected');
    handleConnectionChange('disconnected');
    
    // Clear heartbeat
    if (heartbeatIntervalRef.current) {
      clearInterval(heartbeatIntervalRef.current);
      heartbeatIntervalRef.current = null;
    }
    
    // Terminal codes = permanent auth/permission failure; do NOT retry.
    // 4001=user not found, 4003=role denied, 4004=job not found, 4403=org denied.
    const isTerminal = [1000, 4001, 4003, 4004, 4403].includes(event.code);
    if (isTerminal && event.code !== 1000 && onTerminalClose) {
      onTerminalClose(event.code, event.reason);
    }

    // Attempt to reconnect if enabled and component is still mounted
    if (
      autoReconnect &&
      mountedRef.current &&
      reconnectAttemptsRef.current < maxReconnectAttempts &&
      !isTerminal
    ) {
      const delay = Math.min(reconnectDelay * Math.pow(2, reconnectAttemptsRef.current), 60000);
      log(`Reconnecting in ${delay}ms (attempt ${reconnectAttemptsRef.current + 1}/${maxReconnectAttempts})`);
      
      reconnectTimeoutRef.current = setTimeout(() => {
        if (mountedRef.current) {
          reconnectAttemptsRef.current++;
          connect();
        }
      }, delay);
    }
  }, [log, autoReconnect, maxReconnectAttempts, reconnectDelay, handleConnectionChange, onTerminalClose]);

  const handleError = useCallback((error: Event) => {
    console.error('WebSocket error:', error);
    setConnectionStatus('error');
    handleConnectionChange('error');
  }, [handleConnectionChange]);

  const connect = useCallback(() => {
    if (!accessToken) {
      log('No access token available, skipping WebSocket connection');
      return;
    }

    if (wsRef.current?.readyState === WebSocket.CONNECTING || 
        wsRef.current?.readyState === WebSocket.OPEN) {
      log('WebSocket already connecting or connected');
      return;
    }

    try {
      setConnectionStatus('connecting');
      handleConnectionChange('connecting');
      
      const url = getWebSocketUrl();
      console.log('[WebSocket] Attempting to connect to:', url.replace(/token=[^&]+/, 'token=***'));
      log('Connecting to:', url.replace(/token=[^&]+/, 'token=***'));
      
      wsRef.current = new WebSocket(url);
      wsRef.current.addEventListener('open', handleOpen);
      wsRef.current.addEventListener('message', handleMessage);
      wsRef.current.addEventListener('close', handleClose);
      wsRef.current.addEventListener('error', handleError);
      
    } catch (error) {
      console.error('[WebSocket] Failed to create WebSocket connection:', error);
      setConnectionStatus('error');
      handleConnectionChange('error');
    }
  }, [accessToken, getWebSocketUrl, handleOpen, handleMessage, handleClose, handleError, log, handleConnectionChange]);

  const disconnect = useCallback(() => {
    log('Manually disconnecting WebSocket');
    
    // Clear reconnection timeout
    if (reconnectTimeoutRef.current) {
      clearTimeout(reconnectTimeoutRef.current);
      reconnectTimeoutRef.current = null;
    }
    
    // Clear heartbeat
    if (heartbeatIntervalRef.current) {
      clearInterval(heartbeatIntervalRef.current);
      heartbeatIntervalRef.current = null;
    }
    
    // Close WebSocket
    if (wsRef.current) {
      wsRef.current.removeEventListener('open', handleOpen);
      wsRef.current.removeEventListener('message', handleMessage);
      wsRef.current.removeEventListener('close', handleClose);
      wsRef.current.removeEventListener('error', handleError);
      
      if (wsRef.current.readyState === WebSocket.OPEN || 
          wsRef.current.readyState === WebSocket.CONNECTING) {
        wsRef.current.close(1000, 'Manual disconnect');
      }
      wsRef.current = null;
    }
    
    setConnectionStatus('disconnected');
    handleConnectionChange('disconnected');
  }, [log, handleOpen, handleMessage, handleClose, handleError, handleConnectionChange]);

  const reconnect = useCallback(() => {
    log('Manual reconnect requested');
    disconnect();
    reconnectAttemptsRef.current = 0;
    setTimeout(connect, 100);
  }, [log, disconnect]); // Exclude connect to prevent dependency loops

  const sendMessage = useCallback((message: string) => {
    if (wsRef.current?.readyState === WebSocket.OPEN) {
      wsRef.current.send(message);
      log('Sent message:', message);
    } else {
      console.warn('WebSocket not connected, cannot send message');
    }
  }, [log]);

  // Connect on mount and when dependencies change
  useEffect(() => {
    const currentToken = apiClient.getAccessToken();
    if (currentToken && isAuthenticated) {
      connect();
    }

    return () => {
      mountedRef.current = false;
      disconnect();
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isAuthenticated, jobId]); // Reconnect when auth state or jobId changes

  // Reconnect WebSocket when tab becomes visible again (Safari suspends background tabs)
  useEffect(() => {
    const handleVisibilityChange = () => {
      if (document.visibilityState === 'visible' && isAuthenticated) {
        const ws = wsRef.current;
        const isDisconnected = !ws ||
          ws.readyState === WebSocket.CLOSED ||
          ws.readyState === WebSocket.CLOSING;
        if (isDisconnected) {
          log('Tab became visible, reconnecting WebSocket');
          reconnectAttemptsRef.current = 0;
          connect();
          // Immediately refresh job data in case we missed status updates
          if (jobId) {
            queryClient.invalidateQueries({ queryKey: ['jobs', jobId] });
          } else {
            queryClient.invalidateQueries({ queryKey: ['jobs'] });
          }
        }
      }
    };

    document.addEventListener('visibilitychange', handleVisibilityChange);
    return () => {
      document.removeEventListener('visibilitychange', handleVisibilityChange);
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isAuthenticated, jobId]);

  // Cleanup on unmount
  useEffect(() => {
    return () => {
      mountedRef.current = false;
    };
  }, []);

  return {
    connectionStatus,
    lastMessage,
    lastUpdate,
    reconnect,
    disconnect,
    sendMessage
  };
}