/**
 * WebSocket Service
 *
 * Manages WebSocket connection for real-time calendar updates.
 * Handles reconnection with exponential backoff.
 */

import { API_BASE_URL } from '../config/api';
import { getAuthToken } from './storage';
import type { JobListItem, JobDetail, JobStatus } from '../types';

// WebSocket URL (convert http(s) to ws(s))
function getWebSocketUrl(): string {
  const wsUrl = API_BASE_URL.replace(/^http/, 'ws');
  return `${wsUrl}/ws/calendar/`;
}

// Message types from server
export type WebSocketMessageType =
  | 'connection_established'
  | 'event_created'
  | 'event_updated'
  | 'event_deleted'
  | 'event_status_changed'
  | 'job_assigned'
  | 'job_unassigned'
  | 'subscribed'
  | 'pong';

export interface WebSocketMessage {
  type: WebSocketMessageType;
  event?: Partial<JobDetail>;
  event_id?: number;
  old_status?: JobStatus;
  new_status?: JobStatus;
  changed_fields?: string[];
  user_id?: number;
  groups?: string[];
}

export type WebSocketEventHandler = (message: WebSocketMessage) => void;

class WebSocketService {
  private ws: WebSocket | null = null;
  private reconnectAttempts = 0;
  private maxReconnectAttempts = 10;
  private reconnectDelay = 1000; // Start with 1 second
  private maxReconnectDelay = 30000; // Max 30 seconds
  private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
  private pingInterval: ReturnType<typeof setInterval> | null = null;
  private eventHandlers: Set<WebSocketEventHandler> = new Set();
  private isConnecting = false;
  private shouldReconnect = true;

  /**
   * Connect to WebSocket server
   */
  async connect(): Promise<void> {
    if (this.ws?.readyState === WebSocket.OPEN || this.isConnecting) {
      return;
    }

    this.isConnecting = true;
    this.shouldReconnect = true;

    try {
      const token = await getAuthToken();
      if (!token) {
        this.isConnecting = false;
        return;
      }

      const wsUrl = `${getWebSocketUrl()}?token=${token}`;
      this.ws = new WebSocket(wsUrl);

      this.ws.onopen = () => {
        this.isConnecting = false;
        this.reconnectAttempts = 0;
        this.reconnectDelay = 1000;
        this.startPingInterval();
      };

      this.ws.onmessage = (event) => {
        try {
          const message: WebSocketMessage = JSON.parse(event.data);
          this.notifyHandlers(message);
        } catch (error) {
          // Silently ignore parse errors
        }
      };

      this.ws.onerror = () => {
        this.isConnecting = false;
      };

      this.ws.onclose = (event) => {
        this.isConnecting = false;
        this.stopPingInterval();

        // Don't reconnect if we got 403 (invalid/expired token)
        if (event.reason?.includes('403') || event.reason?.includes('Access denied')) {
          this.shouldReconnect = false;
          return;
        }

        if (this.shouldReconnect) {
          this.scheduleReconnect();
        }
      };
    } catch (error) {
      this.isConnecting = false;
      this.scheduleReconnect();
    }
  }

  /**
   * Disconnect from WebSocket server
   */
  disconnect(): void {
    this.shouldReconnect = false;

    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer);
      this.reconnectTimer = null;
    }

    this.stopPingInterval();

    if (this.ws) {
      this.ws.close();
      this.ws = null;
    }
  }

  /**
   * Add event handler for WebSocket messages
   */
  addHandler(handler: WebSocketEventHandler): () => void {
    this.eventHandlers.add(handler);
    return () => this.eventHandlers.delete(handler);
  }

  /**
   * Remove event handler
   */
  removeHandler(handler: WebSocketEventHandler): void {
    this.eventHandlers.delete(handler);
  }

  /**
   * Subscribe to updates for a specific event
   */
  subscribeToEvent(eventId: number): void {
    this.send({ type: 'subscribe_event', event_id: eventId });
  }

  /**
   * Unsubscribe from updates for a specific event
   */
  unsubscribeFromEvent(eventId: number): void {
    this.send({ type: 'unsubscribe_event', event_id: eventId });
  }

  /**
   * Check if connected
   */
  isConnected(): boolean {
    return this.ws?.readyState === WebSocket.OPEN;
  }

  /**
   * Send message to server
   */
  private send(data: object): void {
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(data));
    }
  }

  /**
   * Notify all handlers of a message
   */
  private notifyHandlers(message: WebSocketMessage): void {
    this.eventHandlers.forEach((handler) => {
      try {
        handler(message);
      } catch (error) {
        // Silently ignore handler errors
      }
    });
  }

  /**
   * Schedule reconnection with exponential backoff
   */
  private scheduleReconnect(): void {
    if (!this.shouldReconnect) {
      return;
    }

    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
      return;
    }

    this.reconnectAttempts++;
    const delay = Math.min(
      this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1),
      this.maxReconnectDelay
    );

    this.reconnectTimer = setTimeout(() => {
      this.connect();
    }, delay);
  }

  /**
   * Start ping interval to keep connection alive
   */
  private startPingInterval(): void {
    this.stopPingInterval();
    this.pingInterval = setInterval(() => {
      if (this.ws?.readyState === WebSocket.OPEN) {
        this.send({ type: 'ping' });
      }
    }, 30000); // Ping every 30 seconds
  }

  /**
   * Stop ping interval
   */
  private stopPingInterval(): void {
    if (this.pingInterval) {
      clearInterval(this.pingInterval);
      this.pingInterval = null;
    }
  }
}

// Export singleton instance
export const websocketService = new WebSocketService();
export default websocketService;
