package com.gnx.telemedicine.service;

import com.gnx.telemedicine.dto.message.ChatUserDto;
import com.gnx.telemedicine.dto.message.ConversationDto;
import com.gnx.telemedicine.dto.message.MessageRequestDto;
import com.gnx.telemedicine.dto.message.MessageResponseDto;
import com.gnx.telemedicine.mappers.MessageMapper;
import com.gnx.telemedicine.model.Doctor;
import com.gnx.telemedicine.model.Message;
import com.gnx.telemedicine.model.Patient;
import com.gnx.telemedicine.model.UserModel;
import com.gnx.telemedicine.model.UserBlock;
import com.gnx.telemedicine.repository.DoctorRepository;
import com.gnx.telemedicine.repository.MessageRepository;
import com.gnx.telemedicine.repository.PatientRepository;
import com.gnx.telemedicine.repository.UserBlockRepository;
import com.gnx.telemedicine.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
public class MessageService {

    private final MessageRepository messageRepository;
    private final UserRepository userRepository;
    private final DoctorRepository doctorRepository;
    private final PatientRepository patientRepository;
    private final UserBlockRepository userBlockRepository;
    private final MessageMapper messageMapper;
    private final SimpMessagingTemplate messagingTemplate;

    @Transactional
    public MessageResponseDto sendMessage(String senderEmail, MessageRequestDto requestDto) {
        UserModel sender = userRepository.findByEmail(senderEmail)
                .orElseThrow(() -> new RuntimeException("Sender not found"));

        UserModel receiver = userRepository.findById(requestDto.getReceiverId())
                .orElseThrow(() -> new RuntimeException("Receiver not found"));

        // Validate that sender and receiver are different
        if (sender.getId().equals(receiver.getId())) {
            throw new IllegalArgumentException("Cannot send message to yourself");
        }

        // Validate that sender is either doctor or patient, and receiver is the opposite role
        if (!((sender.getRole().name().equals("DOCTOR") && receiver.getRole().name().equals("PATIENT")) ||
              (sender.getRole().name().equals("PATIENT") && receiver.getRole().name().equals("DOCTOR")))) {
            throw new IllegalArgumentException("Messages can only be sent between doctors and patients");
        }

        // Check if sender is blocked by receiver or receiver is blocked by sender
        if (userBlockRepository.isBlocked(sender.getId(), receiver.getId())) {
            throw new IllegalArgumentException("You cannot send messages to this user. You have been blocked or you have blocked them.");
        }

        Message message = new Message();
        message.setSender(sender);
        message.setReceiver(receiver);
        message.setContent(requestDto.getContent());
        message.setIsRead(false);

        Message savedMessage = messageRepository.save(message);
        return messageMapper.toDto(savedMessage);
    }

    @Transactional(readOnly = true)
    public List<MessageResponseDto> getConversation(String userEmail, UUID otherUserId) {
        UserModel user = userRepository.findByEmail(userEmail)
                .orElseThrow(() -> new RuntimeException("User not found"));

        List<Message> messages = messageRepository.findConversationBetweenUsers(user.getId(), otherUserId, user.getId());
        return messageMapper.toDtoList(messages);
    }

    @Transactional(readOnly = true)
    public List<ConversationDto> getConversations(String userEmail) {
        UserModel user = userRepository.findByEmail(userEmail)
                .orElseThrow(() -> new RuntimeException("User not found"));

        List<UUID> partnerIds = messageRepository.findConversationPartners(user.getId());

        // Remove duplicates and sort partner IDs by last message time (most recent first)
        List<UUID> sortedPartnerIds = partnerIds.stream()
                .distinct() // Ensure no duplicates
                .sorted((id1, id2) -> {
                    java.time.LocalDateTime time1 = messageRepository.getLastMessageTime(user.getId(), id1, user.getId());
                    java.time.LocalDateTime time2 = messageRepository.getLastMessageTime(user.getId(), id2, user.getId());
                    if (time1 == null && time2 == null) return 0;
                    if (time1 == null) return 1;
                    if (time2 == null) return -1;
                    return time2.compareTo(time1); // Descending order
                })
                .collect(Collectors.toList());

        return sortedPartnerIds.stream().map(partnerId -> {
            UserModel partner = userRepository.findById(partnerId)
                    .orElseThrow(() -> new RuntimeException("Partner not found"));

            List<Message> messages = messageRepository.findConversationBetweenUsers(user.getId(), partnerId, user.getId());
            // Count unread messages from this specific partner
            Long unreadCount = messageRepository.countUnreadMessagesFromSender(user.getId(), partnerId);

            MessageResponseDto lastMessage = messages.isEmpty() ? null : messageMapper.toDto(messages.get(messages.size() - 1));

            return ConversationDto.builder()
                    .otherUserId(partner.getId())
                    .otherUserName(partner.getFirstName() + " " + partner.getLastName())
                    .otherUserRole(partner.getRole().name())
                    .isOnline(partner.getIsOnline() != null ? partner.getIsOnline() : false)
                    .otherUserStatus(partner.getUserStatus() != null ? partner.getUserStatus().name() : "OFFLINE")
                    .lastSeen(partner.getLastSeen())
                    .unreadCount(unreadCount)
                    .lastMessage(lastMessage)
                    .messages(messageMapper.toDtoList(messages))
                    .otherUserAvatarUrl(partner.getAvatarUrl())
                    .build();
        }).collect(Collectors.toList());
    }

    @Transactional
    public void markMessagesAsRead(String userEmail, UUID senderId) {
        UserModel user = userRepository.findByEmail(userEmail)
                .orElseThrow(() -> new RuntimeException("User not found"));
        messageRepository.markMessagesAsRead(user.getId(), senderId);
    }

    @Transactional(readOnly = true)
    public Long getUnreadMessageCount(String userEmail) {
        UserModel user = userRepository.findByEmail(userEmail)
                .orElseThrow(() -> new RuntimeException("User not found"));
        return messageRepository.countUnreadMessages(user.getId());
    }

    @Transactional
    public void updateOnlineStatus(String userEmail, boolean isOnline) {
        UserModel user = userRepository.findByEmail(userEmail)
                .orElseThrow(() -> new RuntimeException("User not found"));
        
        if (isOnline) {
            // When WebSocket connects, restore user's last status
            // If status is null, set to ONLINE as default
            // If status is OFFLINE (from logout), restore to ONLINE
            // If status is ONLINE or BUSY, restore it
            if (user.getUserStatus() == null || user.getUserStatus() == com.gnx.telemedicine.model.enums.UserStatus.OFFLINE) {
                // New user, never set status, or was OFFLINE -> set to ONLINE on reconnect
                user.setUserStatus(com.gnx.telemedicine.model.enums.UserStatus.ONLINE);
                user.setIsOnline(true);
            } else if (user.getUserStatus() == com.gnx.telemedicine.model.enums.UserStatus.ONLINE) {
                // User was ONLINE -> restore ONLINE status
                user.setIsOnline(true);
            } else if (user.getUserStatus() == com.gnx.telemedicine.model.enums.UserStatus.BUSY) {
                // User manually set to BUSY -> restore BUSY status
                user.setIsOnline(true);
            }
        } else {
            // If disconnecting (page refresh, network issue), only set isOnline=false
            // Don't change userStatus to preserve it for reconnection
            // Only set OFFLINE status when explicitly called from logout
            user.setIsOnline(false);
            // Don't change userStatus here - preserve ONLINE or BUSY for reconnect
        }
        user.setLastSeen(LocalDateTime.now());
        userRepository.save(user);
        
        // Broadcast online status change to all connected clients
        Map<String, Object> statusUpdate = new HashMap<>();
        statusUpdate.put("userId", user.getId().toString());
        statusUpdate.put("isOnline", user.getIsOnline());
        statusUpdate.put("status", user.getUserStatus().name());
        statusUpdate.put("lastSeen", user.getLastSeen());
        messagingTemplate.convertAndSend("/topic/online-status", statusUpdate);
    }

    @Transactional
    public void updateUserStatus(String userEmail, String status) {
        UserModel user = userRepository.findByEmail(userEmail)
                .orElseThrow(() -> new RuntimeException("User not found"));
        
        try {
            com.gnx.telemedicine.model.enums.UserStatus userStatus = 
                com.gnx.telemedicine.model.enums.UserStatus.valueOf(status.toUpperCase());
            
            user.setUserStatus(userStatus);
            // Update isOnline based on status
            user.setIsOnline(userStatus == com.gnx.telemedicine.model.enums.UserStatus.ONLINE || 
                            userStatus == com.gnx.telemedicine.model.enums.UserStatus.BUSY);
            user.setLastSeen(LocalDateTime.now());
            userRepository.save(user);
            
            // Broadcast status change to all connected clients
            Map<String, Object> statusUpdate = new HashMap<>();
            statusUpdate.put("userId", user.getId().toString());
            statusUpdate.put("isOnline", user.getIsOnline());
            statusUpdate.put("status", userStatus.name());
            statusUpdate.put("lastSeen", user.getLastSeen());
            messagingTemplate.convertAndSend("/topic/online-status", statusUpdate);
        } catch (IllegalArgumentException e) {
            throw new IllegalArgumentException("Invalid status: " + status + ". Valid values are: ONLINE, OFFLINE, BUSY");
        }
    }

    @Transactional(readOnly = true)
    public List<ChatUserDto> searchDoctors(String userEmail, String query) {
        UserModel currentUser = userRepository.findByEmail(userEmail)
                .orElseThrow(() -> new RuntimeException("User not found"));

        // Only patients can search for doctors
        if (!currentUser.getRole().name().equals("PATIENT")) {
            throw new IllegalArgumentException("Only patients can search for doctors");
        }

        String searchQuery = query == null || query.trim().isEmpty() ? "" : query.trim();
        // Use more lenient queries for chat search
        List<Doctor> doctors = searchQuery.isEmpty() 
                ? doctorRepository.findAllForChat()
                : doctorRepository.searchDoctorsForChat(searchQuery);

        // Filter out the current user if they happen to be a doctor (shouldn't happen, but safety check)
        // Also filter out blocked users
        return doctors.stream()
                .filter(doctor -> !doctor.getUser().getId().equals(currentUser.getId())) // Exclude self
                .filter(doctor -> doctor.getUser() != null && doctor.getUser().getFirstName() != null) // Safety checks
                .filter(doctor -> !userBlockRepository.isBlocked(currentUser.getId(), doctor.getUser().getId())) // Exclude blocked users
                .map(doctor -> ChatUserDto.builder()
                        .userId(doctor.getUser().getId())
                        .firstName(doctor.getUser().getFirstName())
                        .lastName(doctor.getUser().getLastName())
                        .specialization(doctor.getSpecialization())
                        .isVerified(doctor.getIsVerified())
                        .isOnline(doctor.getUser().getIsOnline() != null ? doctor.getUser().getIsOnline() : false)
                        .status(doctor.getUser().getUserStatus() != null ? doctor.getUser().getUserStatus().name() : "OFFLINE")
                        .avatarUrl(doctor.getUser().getAvatarUrl())
                        .build())
                .collect(Collectors.toList());
    }

    @Transactional(readOnly = true)
    public List<ChatUserDto> searchPatients(String userEmail, String query) {
        UserModel currentUser = userRepository.findByEmail(userEmail)
                .orElseThrow(() -> new RuntimeException("User not found"));

        // Only doctors can search for patients
        if (!currentUser.getRole().name().equals("DOCTOR")) {
            throw new IllegalArgumentException("Only doctors can search for patients");
        }

        String searchQuery = query == null || query.trim().isEmpty() ? "" : query.trim();
        // Use more lenient queries for chat search
        List<Patient> patients = searchQuery.isEmpty()
                ? patientRepository.findAllForChat()
                : patientRepository.searchPatientsForChat(searchQuery);

        // Filter out the current user and blocked users
        return patients.stream()
                .filter(patient -> !patient.getUser().getId().equals(currentUser.getId())) // Exclude self
                .filter(patient -> !userBlockRepository.isBlocked(currentUser.getId(), patient.getUser().getId())) // Exclude blocked users
                .map(patient -> ChatUserDto.builder()
                        .userId(patient.getUser().getId())
                        .firstName(patient.getUser().getFirstName())
                        .lastName(patient.getUser().getLastName())
                        .isOnline(patient.getUser().getIsOnline() != null ? patient.getUser().getIsOnline() : false)
                        .status(patient.getUser().getUserStatus() != null ? patient.getUser().getUserStatus().name() : "OFFLINE")
                        .avatarUrl(patient.getUser().getAvatarUrl())
                        .build())
                .collect(Collectors.toList());
    }

    @Transactional
    public void deleteMessage(String userEmail, UUID messageId) {
        UserModel user = userRepository.findByEmail(userEmail)
                .orElseThrow(() -> new RuntimeException("User not found"));

        // Verify message exists and user is either sender or receiver
        Message message = messageRepository.findById(messageId)
                .orElseThrow(() -> new RuntimeException("Message not found"));

        // Check if user is the sender or receiver
        boolean isSender = message.getSender().getId().equals(user.getId());
        boolean isReceiver = message.getReceiver().getId().equals(user.getId());

        if (!isSender && !isReceiver) {
            throw new IllegalArgumentException("You can only delete messages that you sent or received");
        }

        // Soft delete: set the appropriate flag based on whether user is sender or receiver
        if (isSender) {
            messageRepository.softDeleteMessageBySender(messageId, user.getId());
        }
        if (isReceiver) {
            messageRepository.softDeleteMessageByReceiver(messageId, user.getId());
        }
    }

    @Transactional
    public void deleteConversation(String userEmail, UUID otherUserId) {
        UserModel user = userRepository.findByEmail(userEmail)
                .orElseThrow(() -> new RuntimeException("User not found"));

        UserModel otherUser = userRepository.findById(otherUserId)
                .orElseThrow(() -> new RuntimeException("Other user not found"));

        // Soft delete all messages in the conversation for the current user
        // If user is sender: mark deleted_by_sender = true for messages they sent
        // If user is receiver: mark deleted_by_receiver = true for messages they received
        messageRepository.softDeleteConversationBySender(user.getId(), otherUser.getId());
        messageRepository.softDeleteConversationByReceiver(user.getId(), otherUser.getId());
    }

    @Transactional
    public void blockUser(String userEmail, UUID userIdToBlock) {
        UserModel blocker = userRepository.findByEmail(userEmail)
                .orElseThrow(() -> new RuntimeException("User not found"));

        UserModel blocked = userRepository.findById(userIdToBlock)
                .orElseThrow(() -> new RuntimeException("User to block not found"));

        // Validate that blocker and blocked are different
        if (blocker.getId().equals(blocked.getId())) {
            throw new IllegalArgumentException("Cannot block yourself");
        }

        // Validate that blocker and blocked are opposite roles (doctor/patient)
        if (!((blocker.getRole().name().equals("DOCTOR") && blocked.getRole().name().equals("PATIENT")) ||
              (blocker.getRole().name().equals("PATIENT") && blocked.getRole().name().equals("DOCTOR")))) {
            throw new IllegalArgumentException("Can only block users with opposite role (doctor/patient)");
        }

        // Check if already blocked
        if (userBlockRepository.findByBlockerIdAndBlockedId(blocker.getId(), blocked.getId()).isPresent()) {
            throw new IllegalArgumentException("User is already blocked");
        }

        // Create block relationship
        UserBlock userBlock = new UserBlock();
        userBlock.setBlocker(blocker);
        userBlock.setBlocked(blocked);
        userBlockRepository.save(userBlock);
    }

    @Transactional
    public void unblockUser(String userEmail, UUID userIdToUnblock) {
        UserModel blocker = userRepository.findByEmail(userEmail)
                .orElseThrow(() -> new RuntimeException("User not found"));

        UserModel blocked = userRepository.findById(userIdToUnblock)
                .orElseThrow(() -> new RuntimeException("User to unblock not found"));

        UserBlock userBlock = userBlockRepository.findByBlockerIdAndBlockedId(blocker.getId(), blocked.getId())
                .orElseThrow(() -> new IllegalArgumentException("User is not blocked"));

        userBlockRepository.delete(userBlock);
    }

    @Transactional(readOnly = true)
    public boolean isBlocked(String userEmail, UUID otherUserId) {
        UserModel user = userRepository.findByEmail(userEmail)
                .orElseThrow(() -> new RuntimeException("User not found"));

        return userBlockRepository.isBlocked(user.getId(), otherUserId);
    }

    @Transactional(readOnly = true)
    public List<UUID> getBlockedUsers(String userEmail) {
        UserModel user = userRepository.findByEmail(userEmail)
                .orElseThrow(() -> new RuntimeException("User not found"));

        return userBlockRepository.findAll().stream()
                .filter(block -> block.getBlocker().getId().equals(user.getId()))
                .map(block -> block.getBlocked().getId())
                .collect(Collectors.toList());
    }

    @Transactional(readOnly = true)
    public List<ChatUserDto> getBlockedUsersWithDetails(String userEmail) {
        UserModel user = userRepository.findByEmail(userEmail)
                .orElseThrow(() -> new RuntimeException("User not found"));

        // Get all blocked user IDs - use repository query to avoid lazy loading issues
        List<UUID> blockedUserIds = userBlockRepository.findBlockedUserIdsByBlockerId(user.getId());

        log.debug("Found {} blocked user IDs for user {}", blockedUserIds.size(), userEmail);

        if (blockedUserIds.isEmpty()) {
            return new ArrayList<>();
        }

        // Get blocked users' details based on current user's role
        if (user.getRole().name().equals("PATIENT")) {
            // If current user is a patient, blocked users are doctors
            // Find doctors by their user.id directly using repository query
            List<Doctor> doctors = doctorRepository.findByUserIds(blockedUserIds);
            
            log.debug("Found {} doctors for {} blocked user IDs", doctors.size(), blockedUserIds.size());
            
            return doctors.stream()
                    .filter(doctor -> doctor.getUser() != null && doctor.getUser().getFirstName() != null)
                    .map(doctor -> ChatUserDto.builder()
                            .userId(doctor.getUser().getId())
                            .firstName(doctor.getUser().getFirstName())
                            .lastName(doctor.getUser().getLastName())
                            .specialization(doctor.getSpecialization())
                            .isVerified(doctor.getIsVerified() != null ? doctor.getIsVerified() : false)
                            .isOnline(doctor.getUser().getIsOnline() != null ? doctor.getUser().getIsOnline() : false)
                            .status(doctor.getUser().getUserStatus() != null ? doctor.getUser().getUserStatus().name() : "OFFLINE")
                            .avatarUrl(doctor.getUser().getAvatarUrl())
                            .build())
                    .collect(Collectors.toList());
        } else if (user.getRole().name().equals("DOCTOR")) {
            // If current user is a doctor, blocked users are patients
            // Find patients by their user.id directly using repository query
            List<Patient> patients = patientRepository.findByUserIds(blockedUserIds);
            
            log.debug("Found {} patients for {} blocked user IDs", patients.size(), blockedUserIds.size());
            
            return patients.stream()
                    .filter(patient -> patient.getUser() != null && patient.getUser().getFirstName() != null)
                    .map(patient -> ChatUserDto.builder()
                            .userId(patient.getUser().getId())
                            .firstName(patient.getUser().getFirstName())
                            .lastName(patient.getUser().getLastName())
                            .isOnline(patient.getUser().getIsOnline() != null ? patient.getUser().getIsOnline() : false)
                            .status(patient.getUser().getUserStatus() != null ? patient.getUser().getUserStatus().name() : "OFFLINE")
                            .avatarUrl(patient.getUser().getAvatarUrl())
                            .build())
                    .collect(Collectors.toList());
        }

        return new ArrayList<>();
    }
}

