import 'dart:async';

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logger/logger.dart';
import 'package:real_estate_mobile/features/auth/presentation/providers/auth_provider.dart';
import 'package:real_estate_mobile/features/messaging/data/models/messaging_models.dart';
import 'package:real_estate_mobile/features/messaging/data/messaging_repository.dart';
import 'package:real_estate_mobile/features/messaging/data/socket_service.dart';

final _log = Logger(printer: PrettyPrinter(methodCount: 0));

class MessagingState {
  final List<Conversation> conversations;
  final Map<String, List<ChatMessage>> messages;
  final Map<String, PaginationInfo> pagination;
  final Set<String> typingUsers;
  final bool isLoadingConversations;
  final bool isLoadingMessages;
  final bool isSending;
  final String? error;
  final String? activeConversationId;
  final int unreadCount;

  const MessagingState({
    this.conversations = const [],
    this.messages = const {},
    this.pagination = const {},
    this.typingUsers = const {},
    this.isLoadingConversations = false,
    this.isLoadingMessages = false,
    this.isSending = false,
    this.error,
    this.activeConversationId,
    this.unreadCount = 0,
  });

  MessagingState copyWith({
    List<Conversation>? conversations,
    Map<String, List<ChatMessage>>? messages,
    Map<String, PaginationInfo>? pagination,
    Set<String>? typingUsers,
    bool? isLoadingConversations,
    bool? isLoadingMessages,
    bool? isSending,
    String? error,
    String? activeConversationId,
    int? unreadCount,
    bool clearError = false,
    bool clearActiveConversation = false,
  }) {
    return MessagingState(
      conversations: conversations ?? this.conversations,
      messages: messages ?? this.messages,
      pagination: pagination ?? this.pagination,
      typingUsers: typingUsers ?? this.typingUsers,
      isLoadingConversations:
          isLoadingConversations ?? this.isLoadingConversations,
      isLoadingMessages: isLoadingMessages ?? this.isLoadingMessages,
      isSending: isSending ?? this.isSending,
      error: clearError ? null : (error ?? this.error),
      activeConversationId: clearActiveConversation
          ? null
          : (activeConversationId ?? this.activeConversationId),
      unreadCount: unreadCount ?? this.unreadCount,
    );
  }
}

class MessagingNotifier extends StateNotifier<MessagingState> {
  final MessagingRepository _repository;
  final Ref _ref;
  final SocketService _socket = SocketService();
  final List<StreamSubscription> _subscriptions = [];

  MessagingNotifier(this._repository, this._ref)
      : super(const MessagingState()) {
    _initSocketListeners();
    // Defer to avoid modifying provider during widget tree build
    Future.microtask(() => loadUnreadCount());
  }

  String? get _currentUserId => _ref.read(authProvider).user?.id;

  void _initSocketListeners() {
    // Online/offline status changes
    _subscriptions.add(_socket.onStatusChange.listen((data) {
      final userId = data['userId'] as String?;
      final isOnline = data['isOnline'] as bool? ?? false;
      final lastSeenAt = data['lastSeenAt'] as String?;
      if (userId != null) {
        updateUserStatus(userId, isOnline, lastSeenAt: lastSeenAt);
      }
    }));

    // Incoming messages from other users
    _subscriptions.add(_socket.onNewMessage.listen((message) {
      if (!mounted) return;
      final convId = message.conversationId;
      final currentMessages =
          List<ChatMessage>.from(state.messages[convId] ?? []);

      // Avoid duplicates (broadcast may arrive after ack for sender's own message)
      if (currentMessages.any((m) => m.id == message.id)) return;

      // Ignore own messages — the sender path handles these via sendMessage()
      final isMine = message.senderId == _currentUserId;
      if (isMine) return;

      // Immediately ack delivery to backend so sender's tick goes single → double.
      // This works regardless of Redis presence state and avoids races on send.
      _socket.ackMessageReceived(convId, message.id);

      currentMessages.insert(0, message);
      final updatedMessages =
          Map<String, List<ChatMessage>>.from(state.messages)
            ..[convId] = currentMessages;

      // Show friendly preview for non-text messages (matching web)
      String previewText = message.content;
      if (message.messageType == MessageType.image) {
        previewText = 'Sent an image';
      } else if (message.messageType == MessageType.file) {
        previewText = 'Sent a file';
      } else if (_isGifUrl(message.content)) {
        previewText = 'GIF';
      }

      // Update conversation's last message and unread count
      final updatedConversations = state.conversations.map((c) {
        if (c.id == convId) {
          final isActive = state.activeConversationId == convId;
          return c.copyWith(
            lastMessageText: previewText,
            lastMessageAt: message.createdAt,
            unreadCount: isActive ? c.unreadCount : c.unreadCount + 1,
          );
        }
        return c;
      }).toList();

      // Recalculate total unread count
      final totalUnread = updatedConversations.fold<int>(
        0, (sum, c) => sum + c.unreadCount,
      );

      state = state.copyWith(
        messages: updatedMessages,
        conversations: updatedConversations,
        unreadCount: totalUnread,
      );

      // Auto-mark as read if user is currently viewing this conversation
      final isActive = state.activeConversationId == convId;
      if (isActive) {
        markAsRead(convId);
      }
    }));

    // Typing indicators
    _subscriptions.add(_socket.onTypingStart.listen((data) {
      if (!mounted) return;
      final userId = data['userId'] as String?;
      if (userId != null && userId != _currentUserId) {
        state = state.copyWith(
          typingUsers: {...state.typingUsers, userId},
        );
      }
    }));

    _subscriptions.add(_socket.onTypingStop.listen((data) {
      if (!mounted) return;
      final userId = data['userId'] as String?;
      if (userId != null) {
        final updated = Set<String>.from(state.typingUsers)..remove(userId);
        state = state.copyWith(typingUsers: updated);
      }
    }));

    // Delivered receipts — update single tick → double tick
    _subscriptions.add(_socket.onMessageDelivered.listen((data) {
      if (!mounted) return;
      final convId = data['conversationId'] as String?;
      final messageId = data['messageId'] as String?;
      final deliveredAt = data['deliveredAt'] as String?;
      if (convId == null) return;

      final currentMessages = List<ChatMessage>.from(state.messages[convId] ?? []);
      bool changed = false;

      final updatedMessages = currentMessages.map((m) {
        // Update specific message or all sent messages in this conversation
        if (messageId != null && m.id == messageId && m.status == MessageStatus.sent) {
          changed = true;
          return m.copyWith(status: MessageStatus.delivered, deliveredAt: deliveredAt);
        }
        // If no specific messageId, mark all sent messages as delivered
        if (messageId == null && m.senderId == _currentUserId && m.status == MessageStatus.sent) {
          changed = true;
          return m.copyWith(status: MessageStatus.delivered, deliveredAt: deliveredAt);
        }
        return m;
      }).toList();

      if (changed) {
        final newMessages = Map<String, List<ChatMessage>>.from(state.messages)
          ..[convId] = updatedMessages;
        state = state.copyWith(messages: newMessages);
      }
    }));

    // Read receipts — update message ticks and conversation unread count
    _subscriptions.add(_socket.onMessagesRead.listen((data) {
      if (!mounted) return;
      final convId = data['conversationId'] as String?;
      final readAt = data['readAt'] as String?;
      if (convId == null) return;

      // Update conversation unread count
      final updatedConversations = state.conversations.map((c) {
        if (c.id == convId) return c.copyWith(unreadCount: 0);
        return c;
      }).toList();

      // Update message statuses — mark current user's sent messages as READ
      final currentMessages = List<ChatMessage>.from(state.messages[convId] ?? []);
      final currentUserId = _currentUserId;
      bool messagesChanged = false;
      final updatedMessages = currentMessages.map((m) {
        if (m.senderId == currentUserId && m.status != MessageStatus.read) {
          messagesChanged = true;
          return m.copyWith(status: MessageStatus.read, readAt: readAt);
        }
        return m;
      }).toList();

      final newMessages = messagesChanged
          ? (Map<String, List<ChatMessage>>.from(state.messages)..[convId] = updatedMessages)
          : state.messages;

      state = state.copyWith(conversations: updatedConversations, messages: newMessages);
    }));

    // Conversation deleted by other party — remove from local state
    _subscriptions.add(_socket.onConversationDeleted.listen((data) {
      if (!mounted) return;
      final convId = data['conversationId'] as String?;
      if (convId == null) return;

      final updatedMessages =
          Map<String, List<ChatMessage>>.from(state.messages)..remove(convId);
      final updatedConversations =
          state.conversations.where((c) => c.id != convId).toList();
      final totalUnread = updatedConversations.fold<int>(
        0, (sum, c) => sum + c.unreadCount,
      );
      state = state.copyWith(
        messages: updatedMessages,
        conversations: updatedConversations,
        unreadCount: totalUnread,
        clearActiveConversation: state.activeConversationId == convId,
      );
    }));

    // Reconnection: re-join active conversation room and catch up missed messages
    _subscriptions.add(_socket.onConnectionChange.listen((connected) {
      if (!mounted || !connected) return;
      final activeConvId = state.activeConversationId;
      if (activeConvId != null) {
        _rejoinAndCatchUp(activeConvId);
      }
      // Refresh conversation list to catch any missed updates
      loadConversations();
    }));
  }

  /// Re-join socket room and fetch missed messages after reconnection (matching web)
  Future<void> _rejoinAndCatchUp(String conversationId, {int attempt = 1}) async {
    try {
      await _socket.joinConversation(conversationId);
    } catch (_) {
      if (attempt < 3 && mounted && state.activeConversationId == conversationId) {
        await Future.delayed(Duration(seconds: attempt));
        return _rejoinAndCatchUp(conversationId, attempt: attempt + 1);
      }
      return;
    }

    // Fetch recent messages to catch any missed during disconnect
    if (!mounted || state.activeConversationId != conversationId) return;
    try {
      final result = await _repository.getMessages(conversationId, page: 1);
      final recentMessages = result.messages.reversed.toList();
      final currentMessages = List<ChatMessage>.from(state.messages[conversationId] ?? []);
      final existingIds = currentMessages.map((m) => m.id).toSet();
      final newMessages = recentMessages.where((m) => !existingIds.contains(m.id)).toList();

      if (newMessages.isNotEmpty && mounted) {
        currentMessages.insertAll(0, newMessages);
        // Sort newest first
        currentMessages.sort((a, b) => b.createdAt.compareTo(a.createdAt));
        final updatedMessages = Map<String, List<ChatMessage>>.from(state.messages)
          ..[conversationId] = currentMessages;
        state = state.copyWith(messages: updatedMessages);
      }
    } catch (e) {
      _log.e('Failed to catch up messages after reconnect', error: e);
    }
  }

  static bool _isGifUrl(String text) {
    final lower = text.trim().toLowerCase();
    return lower.endsWith('.gif') ||
        lower.contains('giphy.com/') ||
        lower.contains('giphy.gif') ||
        lower.contains('tenor.com/');
  }

  /// Update online status for a user across all conversations (matching web)
  void updateUserStatus(String userId, bool isOnline, {String? lastSeenAt}) {
    if (!mounted) return;
    final updatedConversations = state.conversations.map((c) {
      if (c.otherParty.userId == userId || c.otherParty.id == userId) {
        return c.copyWith(
          otherParty: c.otherParty.copyWith(
            isOnline: isOnline,
            lastSeenAt: lastSeenAt,
          ),
        );
      }
      return c;
    }).toList();

    state = state.copyWith(conversations: updatedConversations);
  }

  @override
  void dispose() {
    for (final sub in _subscriptions) {
      sub.cancel();
    }
    _subscriptions.clear();
    super.dispose();
  }

  Future<void> loadConversations() async {
    state = state.copyWith(isLoadingConversations: true, clearError: true);
    try {
      final result = await _repository.getConversations();
      // Sort: favorites first, then by lastMessageAt
      final currentUserId = _currentUserId;
      result.sort((a, b) {
        final aFav = a.userId == currentUserId ? a.userFavorited : a.agentFavorited;
        final bFav = b.userId == currentUserId ? b.userFavorited : b.agentFavorited;
        if (aFav != bFav) return aFav ? -1 : 1;
        final aTime = a.lastMessageAt ?? '';
        final bTime = b.lastMessageAt ?? '';
        return bTime.compareTo(aTime);
      });
      state = state.copyWith(
        conversations: result,
        isLoadingConversations: false,
      );
    } catch (e, stack) {
      _log.e('Failed to load conversations', error: e, stackTrace: stack);
      state = state.copyWith(
        isLoadingConversations: false,
        error: 'Failed to load conversations',
      );
    }
  }

  Future<void> loadMessages(
    String conversationId, {
    bool loadMore = false,
  }) async {
    if (loadMore) {
      final existing = state.pagination[conversationId];
      if (existing != null && !existing.hasMore) return;
    }

    state = state.copyWith(isLoadingMessages: true, clearError: true);

    try {
      final nextPage =
          loadMore ? ((state.pagination[conversationId]?.page ?? 0) + 1) : 1;

      final result = await _repository.getMessages(
        conversationId,
        page: nextPage,
      );

      // API returns chronological order (oldest first), but our ListView is
      // reverse: true so we need newest-first order (index 0 = newest).
      final newMessages = result.messages.reversed.toList();
      final paginationInfo = result.pagination;

      final existingMessages =
          loadMore ? (state.messages[conversationId] ?? []) : <ChatMessage>[];
      // When loading more (older messages), append to end (older = higher index)
      final mergedMessages =
          loadMore ? [...existingMessages, ...newMessages] : newMessages;

      final updatedMessages =
          Map<String, List<ChatMessage>>.from(state.messages)
            ..[conversationId] = mergedMessages;

      final updatedPagination =
          Map<String, PaginationInfo>.from(state.pagination)
            ..[conversationId] = paginationInfo;

      state = state.copyWith(
        messages: updatedMessages,
        pagination: updatedPagination,
        isLoadingMessages: false,
      );
    } catch (_) {
      state = state.copyWith(
        isLoadingMessages: false,
        error: 'Failed to load messages',
      );
    }
  }

  Future<void> sendMessage(String conversationId, String content) async {
    final currentUserId = _currentUserId;
    if (currentUserId == null) return;
    if (state.isSending) return; // Prevent double-send

    final tempId = 'temp_${DateTime.now().millisecondsSinceEpoch}';
    final now = DateTime.now().toIso8601String();

    final tempMessage = ChatMessage(
      id: tempId,
      conversationId: conversationId,
      senderId: currentUserId,
      content: content,
      messageType: MessageType.text,
      status: MessageStatus.sent,
      createdAt: now,
      updatedAt: now,
      sender: MessageSender(
        id: currentUserId,
        role: 'user',
      ),
    );

    // Add optimistic message at the beginning (newest-first)
    final currentMessages =
        List<ChatMessage>.from(state.messages[conversationId] ?? []);
    currentMessages.insert(0, tempMessage);

    final updatedMessages =
        Map<String, List<ChatMessage>>.from(state.messages)
          ..[conversationId] = currentMessages;

    state = state.copyWith(
      messages: updatedMessages,
      isSending: true,
      clearError: true,
    );

    try {
      ChatMessage? realMessage;

      // Try socket first (enables real-time delivery to other user)
      if (_socket.isConnected) {
        realMessage = await _socket.sendMessage(conversationId, {
          'content': content,
          'messageType': 'TEXT',
        });
      }

      // Fallback to REST if socket failed or not connected
      if (realMessage == null) {
        try {
          realMessage = await _repository.sendMessage(
            conversationId,
            content: content,
          );
        } catch (restError) {
          _log.e('REST send also failed', error: restError);
          rethrow;
        }
      }

      if (!mounted) return;

      final messagesAfterSend =
          List<ChatMessage>.from(state.messages[conversationId] ?? []);
      final tempIndex = messagesAfterSend.indexWhere((m) => m.id == tempId);
      if (tempIndex != -1) {
        messagesAfterSend[tempIndex] = realMessage;
      } else {
        // Temp message was removed (e.g., by a reload) — add the real one
        messagesAfterSend.insert(0, realMessage);
      }

      // Deduplicate — socket broadcast might have already added this message
      final seen = <String>{};
      messagesAfterSend.removeWhere((m) => !seen.add(m.id));

      final finalMessages =
          Map<String, List<ChatMessage>>.from(state.messages)
            ..[conversationId] = messagesAfterSend;

      // Update conversation's last message with friendly preview
      final sent = realMessage;
      String sentPreview = sent.content;
      if (sent.messageType == MessageType.image) {
        sentPreview = 'Sent an image';
      } else if (sent.messageType == MessageType.file) {
        sentPreview = 'Sent a file';
      } else if (_isGifUrl(sent.content)) {
        sentPreview = 'GIF';
      }

      final updatedConversations = state.conversations.map((c) {
        if (c.id == conversationId) {
          return c.copyWith(
            lastMessageText: sentPreview,
            lastMessageAt: sent.createdAt,
          );
        }
        return c;
      }).toList();

      state = state.copyWith(
        messages: finalMessages,
        conversations: updatedConversations,
        isSending: false,
      );
    } catch (e) {
      if (!mounted) return;

      final messagesAfterError =
          List<ChatMessage>.from(state.messages[conversationId] ?? []);
      messagesAfterError.removeWhere((m) => m.id == tempId);

      final errorMessages =
          Map<String, List<ChatMessage>>.from(state.messages)
            ..[conversationId] = messagesAfterError;

      final errorMsg = e.toString().contains('connection')
          ? 'You must be connected to send messages'
          : 'Failed to send message';

      state = state.copyWith(
        messages: errorMessages,
        isSending: false,
        error: errorMsg,
      );
    }
  }

  Future<void> markAsRead(String conversationId) async {
    try {
      await _repository.markAsRead(conversationId);
      if (!mounted) return;

      final updatedConversations = state.conversations.map((c) {
        if (c.id == conversationId) {
          return c.copyWith(unreadCount: 0);
        }
        return c;
      }).toList();

      final totalUnread = updatedConversations.fold<int>(
        0,
        (sum, c) => sum + c.unreadCount,
      );

      state = state.copyWith(
        conversations: updatedConversations,
        unreadCount: totalUnread,
      );
    } catch (_) {}
  }

  void setActiveConversation(String? conversationId) {
    if (!mounted) return;
    if (conversationId == null) {
      state = state.copyWith(clearActiveConversation: true);
    } else {
      state = state.copyWith(activeConversationId: conversationId);
    }
  }

  Future<void> loadUnreadCount() async {
    try {
      final count = await _repository.getUnreadCount();
      if (mounted) state = state.copyWith(unreadCount: count);
    } catch (_) {}
  }

  Future<String?> startConversation(String agentProfileId) async {
    if (!mounted) return null;
    state = state.copyWith(isLoadingConversations: true, clearError: true);
    try {
      final conversation =
          await _repository.startConversation(agentProfileId);

      if (!mounted) return conversation.id;

      final exists = state.conversations.any((c) => c.id == conversation.id);
      final updatedConversations = exists
          ? state.conversations
          : [conversation, ...state.conversations];

      state = state.copyWith(
        conversations: updatedConversations,
        isLoadingConversations: false,
      );
      return conversation.id;
    } catch (_) {
      if (mounted) {
        state = state.copyWith(
          isLoadingConversations: false,
          error: 'Failed to start conversation',
        );
      }
      return null;
    }
  }

  Future<String?> startConversationWithUser(String userId) async {
    if (!mounted) return null;
    state = state.copyWith(isLoadingConversations: true, clearError: true);
    try {
      final conversation =
          await _repository.startConversationWithUser(userId);

      if (!mounted) return conversation.id;

      final exists = state.conversations.any((c) => c.id == conversation.id);
      final updatedConversations = exists
          ? state.conversations
          : [conversation, ...state.conversations];

      state = state.copyWith(
        conversations: updatedConversations,
        isLoadingConversations: false,
      );
      return conversation.id;
    } catch (_) {
      if (mounted) {
        state = state.copyWith(
          isLoadingConversations: false,
          error: 'Failed to start conversation',
        );
      }
      return null;
    }
  }

  Future<void> clearChat(String conversationId) async {
    try {
      await _repository.clearChat(conversationId);
      // Clear local messages
      final updatedMessages =
          Map<String, List<ChatMessage>>.from(state.messages)
            ..[conversationId] = [];
      // Update conversation preview
      final updatedConversations = state.conversations.map((c) {
        if (c.id == conversationId) {
          return c.copyWith(lastMessageText: '', unreadCount: 0);
        }
        return c;
      }).toList();
      state = state.copyWith(
        messages: updatedMessages,
        conversations: updatedConversations,
      );
    } catch (_) {
      state = state.copyWith(error: 'Failed to clear chat');
    }
  }

  Future<void> deleteConversation(String conversationId) async {
    try {
      await _repository.deleteConversation(conversationId);
      // Remove from local state
      final updatedMessages =
          Map<String, List<ChatMessage>>.from(state.messages)
            ..remove(conversationId);
      final updatedConversations =
          state.conversations.where((c) => c.id != conversationId).toList();
      final totalUnread = updatedConversations.fold<int>(
        0, (sum, c) => sum + c.unreadCount,
      );
      state = state.copyWith(
        messages: updatedMessages,
        conversations: updatedConversations,
        unreadCount: totalUnread,
        clearActiveConversation: true,
      );
    } catch (_) {
      state = state.copyWith(error: 'Failed to delete conversation');
    }
  }

  Future<void> toggleMute(String conversationId, bool muted) async {
    try {
      await _repository.toggleMute(conversationId, muted);
      final currentUserId = _ref.read(currentUserIdProvider);
      final updatedConversations = state.conversations.map((c) {
        if (c.id == conversationId) {
          final isUser = c.userId == currentUserId;
          return isUser
              ? c.copyWith(userMuted: muted)
              : c.copyWith(agentMuted: muted);
        }
        return c;
      }).toList();
      state = state.copyWith(conversations: updatedConversations);
    } catch (_) {
      state = state.copyWith(error: 'Failed to toggle mute');
    }
  }

  Future<void> toggleFavorite(String conversationId, bool favorited) async {
    try {
      await _repository.toggleFavorite(conversationId, favorited);
      final currentUserId = _ref.read(currentUserIdProvider);
      var updatedConversations = state.conversations.map((c) {
        if (c.id == conversationId) {
          final isUser = c.userId == currentUserId;
          return isUser
              ? c.copyWith(userFavorited: favorited)
              : c.copyWith(agentFavorited: favorited);
        }
        return c;
      }).toList();
      // Sort: favorites first, then by lastMessageAt
      updatedConversations.sort((a, b) {
        final aFav = a.userId == currentUserId ? a.userFavorited : a.agentFavorited;
        final bFav = b.userId == currentUserId ? b.userFavorited : b.agentFavorited;
        if (aFav != bFav) {
          return aFav ? -1 : 1;
        }
        final aTime = a.lastMessageAt ?? '';
        final bTime = b.lastMessageAt ?? '';
        return bTime.compareTo(aTime);
      });
      state = state.copyWith(conversations: updatedConversations);
    } catch (_) {
      state = state.copyWith(error: 'Failed to toggle favorite');
    }
  }
}

final messagingProvider =
    StateNotifierProvider<MessagingNotifier, MessagingState>((ref) {
  return MessagingNotifier(ref.watch(messagingRepositoryProvider), ref);
});

final currentUserIdProvider = Provider<String?>((ref) {
  return ref.watch(authProvider).user?.id;
});
