'use client';

import { useState, useEffect, useCallback, useRef } from 'react';
import { useSession } from 'next-auth/react';
import { socketService, messagesService } from '@/services';
import type {
  Conversation,
  Message,
  CreateMessageDto,
  TypingIndicator,
  UserStatusChange,
  MessagesReadEvent,
  MessageDeliveredEvent,
  MessageType,
  MessageStatus,
} from '@/types/messaging';

interface UseMessagingOptions {
  autoConnect?: boolean;
}

interface UseMessagingReturn {
  // State
  conversations: Conversation[];
  currentConversation: Conversation | null;
  messages: Message[];
  isConnected: boolean;
  isLoading: boolean;
  isLoadingMessages: boolean;
  typingUsers: Set<string>;
  unreadCount: number;

  // Actions
  connect: () => Promise<void>;
  disconnect: () => void;
  loadConversations: () => Promise<void>;
  selectConversation: (conversationId: string) => Promise<void>;
  startConversation: (body: { agentProfileId?: string; userId?: string }) => Promise<Conversation>;
  sendMessage: (content: string, messageType?: MessageType, fileFields?: { fileUrl?: string; mimeType?: string; fileName?: string; fileSize?: number }) => Promise<void>;
  loadMoreMessages: () => Promise<void>;
  startTyping: () => void;
  stopTyping: () => void;
  markAsRead: () => Promise<void>;
  markAllAsRead: () => Promise<void>;
  deleteConversation: (conversationId: string) => Promise<void>;
  clearMessages: () => void;
  updateUserStatus: (userId: string, isOnline: boolean) => void;
  toggleMute: (conversationId: string, muted: boolean) => Promise<void>;
  toggleFavorite: (conversationId: string, favorited: boolean) => Promise<void>;
  hasMoreMessages: boolean;
}

// Module-level cache to avoid re-fetching on tab switch
let _cachedConversations: Conversation[] | null = null;
let _cachedTimestamp = 0;
const CACHE_TTL = 30000; // 30 seconds

export function useMessaging(options: UseMessagingOptions = {}): UseMessagingReturn {
  const { autoConnect = true } = options;
  const { data: session } = useSession();

  // State — initialize from cache if available and fresh
  const hasFreshCache = _cachedConversations && (Date.now() - _cachedTimestamp < CACHE_TTL);
  const [conversations, setConversations] = useState<Conversation[]>(hasFreshCache ? _cachedConversations! : []);
  const [currentConversation, setCurrentConversation] = useState<Conversation | null>(null);
  const [messages, setMessages] = useState<Message[]>([]);
  const [isConnected, setIsConnected] = useState(false);
  const [isLoading, setIsLoading] = useState(!hasFreshCache);
  const [isLoadingMessages, setIsLoadingMessages] = useState(false);
  const [typingUsers, setTypingUsers] = useState<Set<string>>(new Set());
  const [unreadCount, setUnreadCount] = useState(0);
  const [currentPage, setCurrentPage] = useState(1);
  const [hasMoreMessages, setHasMoreMessages] = useState(true);

  // Refs
  const typingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
  const currentConversationIdRef = useRef<string | null>(null);
  const isInitializedRef = useRef(false);

  // Use localStorage token (same source as REST API, always fresh via interceptor)
  const getAccessToken = useCallback(() => {
    if (typeof window !== 'undefined') {
      return localStorage.getItem('accessToken');
    }
    return null;
  }, []);

  // Check if user is authenticated (session exists)
  const isAuthenticated = !!session?.user;

  // Connect to WebSocket
  const connect = useCallback(async () => {
    const token = getAccessToken();
    if (!token) {
      console.warn('No access token available for socket connection');
      return;
    }

    try {
      await socketService.connect(token);
    } catch (error) {
      console.error('Failed to connect to socket:', error);
    }
  }, [getAccessToken]);

  // Disconnect from WebSocket
  const disconnect = useCallback(() => {
    socketService.disconnect();
  }, []);

  // Load conversations
  const loadConversations = useCallback(async () => {
    if (!getAccessToken()) return;

    setIsLoading(true);
    try {
      const data = await messagesService.getConversations();
      setConversations(data);
      // Update module-level cache
      _cachedConversations = data;
      _cachedTimestamp = Date.now();
    } catch (error) {
      console.error('Failed to load conversations:', error);
    } finally {
      setIsLoading(false);
    }
  }, [getAccessToken]);

  // Select a conversation and load its messages
  const selectConversation = useCallback(
    async (conversationId: string) => {
      if (currentConversationIdRef.current === conversationId) return;

      // Leave previous conversation room
      if (currentConversationIdRef.current) {
        socketService.leaveConversation(currentConversationIdRef.current);
      }

      currentConversationIdRef.current = conversationId;
      setCurrentPage(1);
      setHasMoreMessages(true);
      setMessages([]);
      setTypingUsers(new Set());
      setIsLoadingMessages(true);

      try {
        // Get conversation details
        const conversation = await messagesService.getConversation(conversationId);
        setCurrentConversation(conversation);

        // Join WebSocket room
        if (isConnected) {
          await socketService.joinConversation(conversationId);
        }

        // Load messages
        const { messages: loadedMessages, pagination } =
          await messagesService.getMessages(conversationId, 1, 50);
        setMessages(loadedMessages);
        setHasMoreMessages(pagination.page < pagination.pages);

        // Mark as read (use socket so sender gets real-time blue tick)
        if (isConnected) {
          await socketService.markAsRead(conversationId);
        } else {
          await messagesService.markAsRead(conversationId);
        }

        // Update unread count in conversations list
        setConversations((prev) =>
          prev.map((c) => (c.id === conversationId ? { ...c, unreadCount: 0 } : c))
        );

        // Notify header to instantly refresh message + notification counts
        window.dispatchEvent(new Event('messages-read'));
      } catch (error) {
        console.error('Failed to load conversation:', error);
      } finally {
        setIsLoadingMessages(false);
      }
    },
    [isConnected]
  );

  // Start a new conversation with an agent (for users) or a user (for agents)
  const startConversation = useCallback(
    async (body: { agentProfileId?: string; userId?: string }): Promise<Conversation> => {
      const conversation = await messagesService.startConversation(body);

      // Add to conversations list if not exists
      setConversations((prev) => {
        const exists = prev.some((c) => c.id === conversation.id);
        if (exists) return prev;
        return [conversation, ...prev];
      });

      return conversation;
    },
    []
  );

  // Send a message
  const sendMessage = useCallback(
    async (content: string, messageType: MessageType = 'TEXT' as MessageType, fileFields?: { fileUrl?: string; mimeType?: string; fileName?: string; fileSize?: number }) => {
      if (!currentConversationIdRef.current) return;

      const dto: CreateMessageDto = {
        content,
        messageType,
        ...fileFields,
      };

      // Stop typing indicator
      if (typingTimeoutRef.current) {
        clearTimeout(typingTimeoutRef.current);
        typingTimeoutRef.current = null;
      }
      socketService.stopTyping(currentConversationIdRef.current);

      // Try WebSocket first, fallback to REST
      if (isConnected) {
        const result = await socketService.sendMessage(
          currentConversationIdRef.current,
          dto
        );
        if (result.success && result.message) {
          // Immediately update UI with the sent message (optimistic update)
          setMessages((prev) => {
            // Avoid duplicates (in case broadcast also arrives)
            if (prev.some((m) => m.id === result.message!.id)) return prev;
            return [...prev, result.message!];
          });
        } else if (!result.success) {
          // Fallback to REST
          const message = await messagesService.sendMessage(
            currentConversationIdRef.current,
            dto
          );
          setMessages((prev) => [...prev, message]);
        }
      } else {
        // REST fallback
        const message = await messagesService.sendMessage(
          currentConversationIdRef.current,
          dto
        );
        setMessages((prev) => [...prev, message]);
      }

      // Update conversation in list
      const conversationId = currentConversationIdRef.current;
      const previewText = messageType === ('IMAGE' as MessageType)
        ? 'Sent an image'
        : messageType === ('FILE' as MessageType)
          ? 'Sent a file'
          : content;
      setConversations((prev) =>
        prev.map((c) =>
          c.id === conversationId
            ? {
                ...c,
                lastMessageAt: new Date().toISOString(),
                lastMessageText:
                  previewText.length > 255 ? previewText.substring(0, 252) + '...' : previewText,
              }
            : c
        )
      );
    },
    [isConnected]
  );

  // Load more messages (pagination) with debouncing for smooth scrolling
  const loadMoreMessages = useCallback(async () => {
    if (!currentConversationIdRef.current || !hasMoreMessages || isLoadingMessages)
      return;

    setIsLoadingMessages(true);
    try {
      const nextPage = currentPage + 1;
      const { messages: moreMessages, pagination } = await messagesService.getMessages(
        currentConversationIdRef.current,
        nextPage,
        50
      );

      // Prepend older messages smoothly
      setMessages((prev) => [...moreMessages, ...prev]);
      setCurrentPage(nextPage);
      setHasMoreMessages(pagination.page < pagination.pages);
    } catch (error) {
      console.error('Failed to load more messages:', error);
    } finally {
      setIsLoadingMessages(false);
    }
  }, [currentPage, hasMoreMessages, isLoadingMessages]);

  // Start typing indicator
  const startTyping = useCallback(() => {
    if (!currentConversationIdRef.current) return;

    socketService.startTyping(currentConversationIdRef.current);

    // Clear previous timeout
    if (typingTimeoutRef.current) {
      clearTimeout(typingTimeoutRef.current);
    }

    // Auto-stop typing after 3 seconds
    typingTimeoutRef.current = setTimeout(() => {
      if (currentConversationIdRef.current) {
        socketService.stopTyping(currentConversationIdRef.current);
      }
    }, 3000);
  }, []);

  // Stop typing indicator
  const stopTyping = useCallback(() => {
    if (!currentConversationIdRef.current) return;

    if (typingTimeoutRef.current) {
      clearTimeout(typingTimeoutRef.current);
      typingTimeoutRef.current = null;
    }
    socketService.stopTyping(currentConversationIdRef.current);
  }, []);

  // Mark messages as read
  const markAsRead = useCallback(async () => {
    if (!currentConversationIdRef.current) return;

    if (isConnected) {
      await socketService.markAsRead(currentConversationIdRef.current);
    } else {
      await messagesService.markAsRead(currentConversationIdRef.current);
    }
    window.dispatchEvent(new Event('messages-read'));
  }, [isConnected]);

  // Mark all conversations as read
  const markAllAsRead = useCallback(async () => {
    const unreadConversations = conversations.filter((c) => c.unreadCount > 0);
    await Promise.all(
      unreadConversations.map((c) => messagesService.markAsRead(c.id))
    );
    setConversations((prev) =>
      prev.map((c) => ({ ...c, unreadCount: 0 }))
    );
    window.dispatchEvent(new Event('messages-read'));
  }, [conversations]);

  // Delete a conversation
  const deleteConversation = useCallback(
    async (conversationId: string) => {
      await messagesService.deleteConversation(conversationId);

      // Remove from list and invalidate cache
      _cachedConversations = null;
      _cachedTimestamp = 0;
      setConversations((prev) => prev.filter((c) => c.id !== conversationId));

      // Clear current if it was the deleted one
      if (currentConversationIdRef.current === conversationId) {
        currentConversationIdRef.current = null;
        setCurrentConversation(null);
        setMessages([]);
      }
    },
    []
  );

  // Clear messages from current conversation (persisted to backend)
  const clearMessages = useCallback(async () => {
    const convId = currentConversationIdRef.current;
    if (!convId) return;

    try {
      await messagesService.clearChat(convId);
      setMessages([]);
      // Update conversation preview in the list
      setConversations((prev) =>
        prev.map((c) =>
          c.id === convId
            ? { ...c, lastMessageText: null, lastMessageAt: null, unreadCount: 0, userUnreadCount: 0, agentUnreadCount: 0 }
            : c
        )
      );
      setCurrentConversation((prev) =>
        prev && prev.id === convId
          ? { ...prev, lastMessageText: null, lastMessageAt: null, unreadCount: 0, userUnreadCount: 0, agentUnreadCount: 0 }
          : prev
      );
    } catch (err) {
      console.error('Failed to clear chat:', err);
    }
  }, []);

  // Toggle mute status (only for current user's side)
  const toggleMute = useCallback(async (conversationId: string, muted: boolean) => {
    try {
      await messagesService.toggleMute(conversationId, muted);
      // Backend sets the correct field based on caller's role;
      // optimistically update both fields since we don't know which one the server set,
      // but the API response and next fetch will have the correct values.
      // For the current user's perspective, both fields reflect "is this muted for me"
      const update = (c: Conversation) => {
        if (c.id !== conversationId) return c;
        // Determine which field to update based on the user's role in the conversation
        const userId = (session?.user as any)?.id;
        const isUser = c.userId === userId;
        return isUser
          ? { ...c, userMuted: muted }
          : { ...c, agentMuted: muted };
      };
      setConversations((prev) => prev.map(update));
      setCurrentConversation((prev) => prev ? update(prev) : prev);
    } catch (err) {
      console.error('Failed to toggle mute:', err);
    }
  }, [session]);

  // Toggle favorite status (only for current user's side)
  const toggleFavorite = useCallback(async (conversationId: string, favorited: boolean) => {
    try {
      await messagesService.toggleFavorite(conversationId, favorited);
      const userId = (session?.user as any)?.id;
      const update = (c: Conversation) => {
        if (c.id !== conversationId) return c;
        const isUser = c.userId === userId;
        return isUser
          ? { ...c, userFavorited: favorited }
          : { ...c, agentFavorited: favorited };
      };
      setConversations((prev) => {
        const updated = prev.map(update);
        return updated.sort((a, b) => {
          const aFav = a.userId === userId ? a.userFavorited : a.agentFavorited;
          const bFav = b.userId === userId ? b.userFavorited : b.agentFavorited;
          if (aFav !== bFav) return aFav ? -1 : 1;
          const aTime = a.lastMessageAt ? new Date(a.lastMessageAt).getTime() : 0;
          const bTime = b.lastMessageAt ? new Date(b.lastMessageAt).getTime() : 0;
          return bTime - aTime;
        });
      });
      setCurrentConversation((prev) => prev ? update(prev) : prev);
    } catch (err) {
      console.error('Failed to toggle favorite:', err);
    }
  }, [session]);

  // Update user online status (called from status change events)
  const updateUserStatus = useCallback((userId: string, isOnline: boolean) => {
    setConversations((prev) =>
      prev.map((c) => {
        if (c.otherParty.userId === userId || c.otherParty.id === userId) {
          return {
            ...c,
            otherParty: { ...c.otherParty, isOnline },
          };
        }
        return c;
      })
    );

    // Update current conversation if applicable
    setCurrentConversation((prev) => {
      if (!prev) return prev;
      if (
        prev.otherParty.userId === userId ||
        prev.otherParty.id === userId
      ) {
        return {
          ...prev,
          otherParty: { ...prev.otherParty, isOnline },
        };
      }
      return prev;
    });
  }, []);

  // Set up socket event listeners
  useEffect(() => {
    const unsubscribeConnection = socketService.onConnectionChange((connected) => {
      setIsConnected(connected);
    });

    const unsubscribeMessage = socketService.onNewMessage((message) => {
      // Only add if it's for the current conversation
      if (message.conversationId === currentConversationIdRef.current) {
        setMessages((prev) => {
          // Avoid duplicates
          if (prev.some((m) => m.id === message.id)) return prev;
          return [...prev, message];
        });

        // Auto-mark as read if user is currently viewing this conversation
        // (the other user sent a message while we're looking at the chat)
        const currentUserId = (session?.user as any)?.id;
        if (message.senderId !== currentUserId) {
          if (isConnected) {
            socketService.markAsRead(message.conversationId);
          } else {
            messagesService.markAsRead(message.conversationId).catch(() => {});
          }
          // Notify header to refresh counts since we auto-read the message
          window.dispatchEvent(new Event('messages-read'));
        }
      }

      // Update conversation list
      setConversations((prev) =>
        prev.map((c) => {
          if (c.id === message.conversationId) {
            const isCurrentConversation =
              c.id === currentConversationIdRef.current;
            // Show friendly preview for non-text messages
            let previewText = message.content.substring(0, 255);
            if (message.messageType === 'IMAGE') {
              previewText = 'Sent an image';
            } else if (message.messageType === 'FILE') {
              previewText = 'Sent a file';
            }
            return {
              ...c,
              lastMessageAt: message.createdAt,
              lastMessageText: previewText,
              unreadCount: isCurrentConversation ? 0 : c.unreadCount + 1,
            };
          }
          return c;
        })
      );
    });

    const unsubscribeTypingStart = socketService.onTypingStart(
      (data: TypingIndicator) => {
        if (data.conversationId === currentConversationIdRef.current) {
          setTypingUsers((prev) => new Set(prev).add(data.userId));
        }
      }
    );

    const unsubscribeTypingStop = socketService.onTypingStop(
      (data: TypingIndicator) => {
        if (data.conversationId === currentConversationIdRef.current) {
          setTypingUsers((prev) => {
            const next = new Set(prev);
            next.delete(data.userId);
            return next;
          });
        }
      }
    );

    const unsubscribeStatus = socketService.onUserStatusChange(
      (data: UserStatusChange) => {
        updateUserStatus(data.userId, data.isOnline);
      }
    );

    const unsubscribeRead = socketService.onMessagesRead(
      (data: MessagesReadEvent) => {
        if (data.conversationId === currentConversationIdRef.current) {
          const currentUserId = (session?.user as any)?.id;
          // Only update status on messages SENT by current user (not received ones)
          setMessages((prev) =>
            prev.map((m) => {
              if (m.senderId === currentUserId && m.status !== 'READ') {
                return { ...m, status: 'READ' as MessageStatus, readAt: data.readAt };
              }
              return m;
            })
          );
        }
      }
    );

    const unsubscribeDelivered = socketService.onMessageDelivered(
      (data: MessageDeliveredEvent) => {
        if (data.conversationId === currentConversationIdRef.current) {
          // Update single message or batch of messages to DELIVERED
          const deliveredIds = data.messageIds || (data.messageId ? [data.messageId] : []);
          if (deliveredIds.length > 0) {
            setMessages((prev) =>
              prev.map((m) => {
                if (deliveredIds.includes(m.id) && m.status === 'SENT') {
                  return { ...m, status: 'DELIVERED' as MessageStatus, deliveredAt: data.deliveredAt };
                }
                return m;
              })
            );
          }
        }
      }
    );

    // Conversation deleted by the other party — remove from local state
    const unsubscribeConvDeleted = socketService.onConversationDeleted(
      (data: Record<string, unknown>) => {
        const convId = data.conversationId as string | undefined;
        if (!convId) return;
        setConversations((prev) => prev.filter((c) => c.id !== convId));
        if (currentConversationIdRef.current === convId) {
          currentConversationIdRef.current = null;
          setMessages([]);
        }
      }
    );

    // Auth error handling (token refresh + reconnect) is handled globally
    // by PresenceProvider — no duplicate handler needed here.

    return () => {
      unsubscribeConnection();
      unsubscribeMessage();
      unsubscribeTypingStart();
      unsubscribeTypingStop();
      unsubscribeStatus();
      unsubscribeRead();
      unsubscribeDelivered();
      unsubscribeConvDeleted();
      // Clean up typing timeout to prevent memory leak
      if (typingTimeoutRef.current) {
        clearTimeout(typingTimeoutRef.current);
        typingTimeoutRef.current = null;
      }
    };
  }, [updateUserStatus]);

  // Invalidate cache when session changes (logout/login)
  useEffect(() => {
    if (!isAuthenticated) {
      _cachedConversations = null;
      _cachedTimestamp = 0;
    }
  }, [isAuthenticated]);

  // Auto-connect on mount
  useEffect(() => {
    if (autoConnect && isAuthenticated && !isInitializedRef.current) {
      isInitializedRef.current = true;
      connect();

      // Only fetch conversations if cache is stale or empty
      if (!_cachedConversations || Date.now() - _cachedTimestamp >= CACHE_TTL) {
        loadConversations();
      } else {
        setIsLoading(false);
      }

      // Get initial unread count
      messagesService.getUnreadCount().then(setUnreadCount).catch(console.error);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [autoConnect, isAuthenticated]);

  // Re-join conversation room when reconnecting + fetch missed messages
  useEffect(() => {
    if (isConnected && currentConversationIdRef.current) {
      const conversationId = currentConversationIdRef.current;

      const rejoinAndCatchUp = async (attempt = 1) => {
        // Re-join the room
        const result = await socketService.joinConversation(conversationId);
        if (!result.success && attempt < 3 && currentConversationIdRef.current === conversationId) {
          console.warn(`Room join attempt ${attempt} failed for ${conversationId}, retrying...`);
          setTimeout(() => rejoinAndCatchUp(attempt + 1), 1000 * attempt);
          return;
        }

        // Fetch recent messages to catch any missed during disconnect
        if (currentConversationIdRef.current === conversationId) {
          try {
            const { messages: recentMessages } = await messagesService.getMessages(conversationId, 1, 20);
            setMessages((prev) => {
              const existingIds = new Set(prev.map((m) => m.id));
              const newMessages = recentMessages.filter((m) => !existingIds.has(m.id));
              if (newMessages.length === 0) return prev;
              // Merge and sort by creation date
              const merged = [...prev, ...newMessages].sort(
                (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
              );
              return merged;
            });
          } catch (err) {
            console.error('Failed to catch up messages after reconnect:', err);
          }
        }
      };

      rejoinAndCatchUp();
    }
  }, [isConnected]);

  return {
    conversations,
    currentConversation,
    messages,
    isConnected,
    isLoading,
    isLoadingMessages,
    typingUsers,
    unreadCount,
    connect,
    disconnect,
    loadConversations,
    selectConversation,
    startConversation,
    sendMessage,
    loadMoreMessages,
    startTyping,
    stopTyping,
    markAsRead,
    markAllAsRead,
    deleteConversation,
    clearMessages,
    updateUserStatus,
    toggleMute,
    toggleFavorite,
    hasMoreMessages,
  };
}
