import { io, Socket } from 'socket.io-client';
import type {
  Message,
  TypingIndicator,
  UserStatusChange,
  MessagesReadEvent,
  MessageDeliveredEvent,
  CreateMessageDto,
} from '@/types/messaging';

type MessageHandler = (message: Message) => void;
type TypingHandler = (data: TypingIndicator) => void;
type StatusHandler = (data: UserStatusChange) => void;
type ReadHandler = (data: MessagesReadEvent) => void;
type DeliveredHandler = (data: MessageDeliveredEvent) => void;
type NotificationEventHandler = (data: Record<string, unknown>) => void;

class SocketService {
  private socket: Socket | null = null;
  private reconnectAttempts = 0;
  private maxReconnectAttempts = 5;
  private messageHandlers: Set<MessageHandler> = new Set();
  private typingStartHandlers: Set<TypingHandler> = new Set();
  private typingStopHandlers: Set<TypingHandler> = new Set();
  private statusHandlers: Set<StatusHandler> = new Set();
  private readHandlers: Set<ReadHandler> = new Set();
  private deliveredHandlers: Set<DeliveredHandler> = new Set();
  private connectionRequestHandlers: Set<NotificationEventHandler> = new Set();
  private connectionResponseHandlers: Set<NotificationEventHandler> = new Set();
  private connectionHandlers: Set<(connected: boolean) => void> = new Set();
  private authErrorHandlers: Set<() => void> = new Set();
  private conversationDeletedHandlers: Set<NotificationEventHandler> = new Set();
  private isConnecting = false;
  private currentToken: string | null = null;
  private listenersAttached = false;
  private connectPromise: Promise<void> | null = null;

  connect(token: string): Promise<void> {
    this.currentToken = token;

    // Already connected with same token
    if (this.socket?.connected) {
      return Promise.resolve();
    }

    // Already connecting — return the existing promise
    if (this.isConnecting && this.connectPromise) {
      return this.connectPromise;
    }

    this.isConnecting = true;

    // Clean up existing socket if any
    if (this.socket && !this.socket.connected) {
      this.socket.removeAllListeners();
      this.socket.disconnect();
      this.socket = null;
      this.listenersAttached = false;
    }

    // Extract base URL without /api/v1 path for Socket.io connection
    const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000';
    const baseUrl = apiUrl.replace(/\/api\/v1\/?$/, '');

    this.socket = io(baseUrl, {
      auth: { token },
      transports: ['websocket', 'polling'],
      reconnection: true,
      reconnectionAttempts: this.maxReconnectAttempts,
      reconnectionDelay: 1000,
      reconnectionDelayMax: 5000,
    });

    this.connectPromise = new Promise<void>((resolve, reject) => {
      this.socket!.on('connect', () => {
        console.log('Socket connected:', this.socket?.id);
        this.isConnecting = false;
        this.connectPromise = null;
        this.reconnectAttempts = 0;
        this.notifyConnectionHandlers(true);
        resolve();
      });

      this.socket!.on('disconnect', (reason) => {
        console.log('Socket disconnected:', reason);
        this.isConnecting = false;
        this.connectPromise = null;
        this.notifyConnectionHandlers(false);

        // "io server disconnect" means the server rejected us (likely auth failure)
        if (reason === 'io server disconnect') {
          console.warn('Socket: Server disconnected us (likely token expired). Requesting token refresh...');
          this.authErrorHandlers.forEach((handler) => handler());
        }
      });

      this.socket!.on('connect_error', (error) => {
        console.error('Socket connection error:', error.message);
        this.isConnecting = false;
        this.connectPromise = null;
        this.reconnectAttempts++;
        if (this.reconnectAttempts >= this.maxReconnectAttempts) {
          reject(new Error('Max reconnection attempts reached'));
        }
      });
    });

    // Attach event listeners only once per socket instance
    this.attachEventListeners();

    return this.connectPromise;
  }

  /**
   * Attach socket event listeners — called once per socket instance
   */
  private attachEventListeners(): void {
    if (this.listenersAttached || !this.socket) return;
    this.listenersAttached = true;

    this.socket.on('new_message', (message: Message) => {
      // Auto-ack delivery so sender gets real-time double-tick
      try {
        this.socket?.emit('message_received', {
          messageId: message.id,
          conversationId: message.conversationId,
        });
      } catch {
        // Ignore ack errors
      }
      this.messageHandlers.forEach((handler) => handler(message));
    });

    this.socket.on('typing_start', (data: TypingIndicator) => {
      this.typingStartHandlers.forEach((handler) => handler(data));
    });

    this.socket.on('typing_stop', (data: TypingIndicator) => {
      this.typingStopHandlers.forEach((handler) => handler(data));
    });

    this.socket.on('user_status_change', (data: UserStatusChange) => {
      this.statusHandlers.forEach((handler) => handler(data));
    });

    this.socket.on('messages_read', (data: MessagesReadEvent) => {
      this.readHandlers.forEach((handler) => handler(data));
    });

    this.socket.on('message_delivered', (data: MessageDeliveredEvent) => {
      this.deliveredHandlers.forEach((handler) => handler(data));
    });

    this.socket.on('connection_request', (data: Record<string, unknown>) => {
      this.connectionRequestHandlers.forEach((handler) => handler(data));
    });

    this.socket.on('connection_response', (data: Record<string, unknown>) => {
      this.connectionResponseHandlers.forEach((handler) => handler(data));
    });

    this.socket.on('conversation_deleted', (data: Record<string, unknown>) => {
      this.conversationDeletedHandlers.forEach((handler) => handler(data));
    });
  }

  // Reconnect with a fresh token (used when the previous token expired)
  reconnectWithToken(token: string): void {
    this.currentToken = token;
    if (this.socket) {
      // Ensure fully disconnected before reconnecting
      this.socket.removeAllListeners();
      this.socket.disconnect();
      this.socket = null;
      this.listenersAttached = false;
    }
    this.isConnecting = false;
    this.connectPromise = null;
    this.connect(token).catch((err) => {
      console.error('Socket reconnect with new token failed:', err);
    });
  }

  /**
   * Fully disconnect and clear every subscriber handler. Use ONLY on logout
   * — this invalidates every registered listener across the app.
   *
   * For transient disconnects (tab hidden, user idle), use `pause()` instead
   * so React-registered listeners survive the reconnect.
   */
  disconnect(): void {
    this.pause();
    this.currentToken = null;
    // Clear all handler sets — only safe on logout because every subscriber
    // must re-register after a fresh login.
    this.messageHandlers.clear();
    this.typingStartHandlers.clear();
    this.typingStopHandlers.clear();
    this.statusHandlers.clear();
    this.readHandlers.clear();
    this.deliveredHandlers.clear();
    this.connectionRequestHandlers.clear();
    this.connectionResponseHandlers.clear();
    this.conversationDeletedHandlers.clear();
    this.connectionHandlers.clear();
    this.authErrorHandlers.clear();
  }

  /**
   * Tear down the socket without clearing subscriber handler sets.
   * Used for idle/visibility disconnects so that when we reconnect,
   * the existing React hooks' handlers automatically resume receiving
   * events without needing to re-register.
   */
  pause(): void {
    if (this.socket) {
      this.socket.removeAllListeners();
      this.socket.disconnect();
      this.socket = null;
    }
    this.isConnecting = false;
    this.connectPromise = null;
    this.listenersAttached = false;
    // Intentionally keep: currentToken, messageHandlers, typingStart/StopHandlers,
    // statusHandlers, readHandlers, deliveredHandlers, connectionRequest/ResponseHandlers,
    // connectionHandlers, authErrorHandlers
  }

  isConnected(): boolean {
    return this.socket?.connected || false;
  }

  // Room management
  joinConversation(conversationId: string): Promise<{ success: boolean; error?: string }> {
    return new Promise((resolve) => {
      if (!this.socket) {
        resolve({ success: false, error: 'Not connected' });
        return;
      }

      const timeout = setTimeout(() => {
        resolve({ success: false, error: 'Socket timeout' });
      }, 10000);

      this.socket.emit('join_conversation', { conversationId }, (response: { success: boolean; error?: string }) => {
        clearTimeout(timeout);
        resolve(response);
      });
    });
  }

  leaveConversation(conversationId: string): void {
    if (this.socket) {
      this.socket.emit('leave_conversation', { conversationId });
    }
  }

  // Messaging
  sendMessage(
    conversationId: string,
    message: CreateMessageDto
  ): Promise<{ success: boolean; message?: Message; error?: string }> {
    return new Promise((resolve) => {
      if (!this.socket) {
        resolve({ success: false, error: 'Not connected' });
        return;
      }

      const timeout = setTimeout(() => {
        resolve({ success: false, error: 'Socket timeout' });
      }, 10000);

      this.socket.emit(
        'send_message',
        { conversationId, message },
        (response: { success: boolean; message?: Message; error?: string }) => {
          clearTimeout(timeout);
          resolve(response);
        }
      );
    });
  }

  // Typing indicators
  startTyping(conversationId: string): void {
    if (this.socket) {
      this.socket.emit('typing_start', { conversationId });
    }
  }

  stopTyping(conversationId: string): void {
    if (this.socket) {
      this.socket.emit('typing_stop', { conversationId });
    }
  }

  // Mark messages as read
  markAsRead(conversationId: string): Promise<{ success: boolean; error?: string }> {
    return new Promise((resolve) => {
      if (!this.socket) {
        resolve({ success: false, error: 'Not connected' });
        return;
      }

      const timeout = setTimeout(() => {
        resolve({ success: false, error: 'Socket timeout' });
      }, 10000);

      this.socket.emit('mark_read', { conversationId }, (response: { success: boolean; error?: string }) => {
        clearTimeout(timeout);
        resolve(response);
      });
    });
  }

  // Event handlers registration
  onNewMessage(handler: MessageHandler): () => void {
    this.messageHandlers.add(handler);
    return () => this.messageHandlers.delete(handler);
  }

  onTypingStart(handler: TypingHandler): () => void {
    this.typingStartHandlers.add(handler);
    return () => this.typingStartHandlers.delete(handler);
  }

  onTypingStop(handler: TypingHandler): () => void {
    this.typingStopHandlers.add(handler);
    return () => this.typingStopHandlers.delete(handler);
  }

  onUserStatusChange(handler: StatusHandler): () => void {
    this.statusHandlers.add(handler);
    return () => this.statusHandlers.delete(handler);
  }

  onMessagesRead(handler: ReadHandler): () => void {
    this.readHandlers.add(handler);
    return () => this.readHandlers.delete(handler);
  }

  onMessageDelivered(handler: DeliveredHandler): () => void {
    this.deliveredHandlers.add(handler);
    return () => this.deliveredHandlers.delete(handler);
  }

  onConnectionRequest(handler: NotificationEventHandler): () => void {
    this.connectionRequestHandlers.add(handler);
    return () => this.connectionRequestHandlers.delete(handler);
  }

  onConnectionResponse(handler: NotificationEventHandler): () => void {
    this.connectionResponseHandlers.add(handler);
    return () => this.connectionResponseHandlers.delete(handler);
  }

  onConversationDeleted(handler: NotificationEventHandler): () => void {
    this.conversationDeletedHandlers.add(handler);
    return () => this.conversationDeletedHandlers.delete(handler);
  }

  onConnectionChange(handler: (connected: boolean) => void): () => void {
    this.connectionHandlers.add(handler);
    return () => this.connectionHandlers.delete(handler);
  }

  onAuthError(handler: () => void): () => void {
    this.authErrorHandlers.add(handler);
    return () => this.authErrorHandlers.delete(handler);
  }

  private notifyConnectionHandlers(connected: boolean): void {
    this.connectionHandlers.forEach((handler) => handler(connected));
  }
}

// Singleton instance
export const socketService = new SocketService();
