package com.gnx.telemedicine.service;

import com.gnx.telemedicine.dto.appointment.AppointmentRequestDto;
import com.gnx.telemedicine.dto.appointment.AppointmentResponseDto;
import com.gnx.telemedicine.exception.AvailabilityException;
import com.gnx.telemedicine.mappers.AppointmentMapper;
import com.gnx.telemedicine.model.Appointment;
import com.gnx.telemedicine.model.Doctor;
import com.gnx.telemedicine.model.DoctorAvailability;
import com.gnx.telemedicine.model.enums.AppointmentStatus;
import com.gnx.telemedicine.model.enums.DayOfWeek;
import com.gnx.telemedicine.repository.AppointmentRepository;
import com.gnx.telemedicine.repository.DoctorRepository;
import com.gnx.telemedicine.repository.PatientRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

@Service
@RequiredArgsConstructor
@Slf4j
public class AppointmentService {
    private final AppointmentRepository appointmentRepository;
    private final AppointmentMapper appointmentMapper;
    private final EmailNotificationService emailService;
    private final PatientRepository patientRepository;
    private final DoctorRepository doctorRepository;
    private final DoctorAvailabilityService doctorAvailabilityService;
    private final com.gnx.telemedicine.metrics.TelemedicineMetrics telemedicineMetrics;

    @Cacheable(value = "appointmentsByPatient", key = "#id")
    public List<AppointmentResponseDto> getAppointmentsByPatientId(UUID id) {
        return appointmentRepository.findByPatientId(id).stream().map(appointmentMapper::toAppointmentResponseDto).toList();
    }

    @Cacheable(value = "appointmentsByDoctor", key = "#id")
    public List<AppointmentResponseDto> getAppointmentsByDoctorId(UUID id) {
        return appointmentRepository.findByDoctorId(id).stream().map(appointmentMapper::toAppointmentResponseDto).toList();
    }

    @Cacheable(value = "appointments")
    public List<AppointmentResponseDto> getAllAppointments() {
        return appointmentRepository.findAllWithRelations().stream().map(appointmentMapper::toAppointmentResponseDto).toList();
    }

    @Cacheable(value = "appointmentById", key = "#id")
    public AppointmentResponseDto getAppointmentById(UUID id) {
        Appointment appointment = appointmentRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("Appointment not found"));
        return appointmentMapper.toAppointmentResponseDto(appointment);
    }

    @Transactional(readOnly = true)
    public Appointment getAppointmentEntityById(UUID id) {
        Appointment appointment = appointmentRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("Appointment not found"));
        // Force initialization of lazy relationships while session is open
        if (appointment.getPatient() != null) {
            appointment.getPatient().getId(); // Force initialization
        }
        if (appointment.getDoctor() != null) {
            appointment.getDoctor().getId(); // Force initialization
        }
        return appointment;
    }

    @CacheEvict(value = {"appointments", "appointmentsByPatient", "appointmentsByDoctor"}, allEntries = true)
    public AppointmentResponseDto createAppointment(AppointmentRequestDto appointmentRequest) {
        // Start appointment creation timer
        io.micrometer.core.instrument.Timer.Sample timer = telemedicineMetrics.startAppointmentCreationTimer();
        
        try {
            // Validate durationMinutes is not null
            if (appointmentRequest.durationMinutes() == null) {
                throw new IllegalArgumentException("Duration in minutes cannot be null");
            }
        
        // Check if patient already has an appointment on this date
        List<Appointment> existingPatientAppointments = appointmentRepository.findByPatientIdAndDate(
                appointmentRequest.patientId(),
                appointmentRequest.scheduledDate()
        );
        
        if (!existingPatientAppointments.isEmpty()) {
            throw new AvailabilityException("You already have an appointment scheduled on this date. Each patient can have only one appointment per day.");
        }
        
        DayOfWeek dayOfWeek = DayOfWeek.valueOf(appointmentRequest.scheduledDate().getDayOfWeek().name());
        LocalTime endTime = appointmentRequest.scheduledTime().plusMinutes(appointmentRequest.durationMinutes());

        // Check if doctor has availability slot for this time
        boolean isAvailable = doctorAvailabilityService.isDoctorAvailable(
                appointmentRequest.doctorId(),
                dayOfWeek,
                appointmentRequest.scheduledTime(),
                endTime
        );

        if (!isAvailable) {
            throw new AvailabilityException("Doctor is not available at the requested time. Please check doctor's availability schedule.");
        }

        // Check for overlapping appointments
        List<Appointment> overlapping = appointmentRepository.findOverlappingAppointments(
                appointmentRequest.doctorId(),
                appointmentRequest.scheduledDate(),
                appointmentRequest.scheduledTime(),
                endTime
        );

        if (!overlapping.isEmpty()) {
            throw new AvailabilityException("Time slot is already booked. Please choose a different time.");
        }

        Appointment appointment = appointmentMapper.toAppointment(appointmentRequest);
        // Don't set ID manually - let Hibernate generate it via @GeneratedValue
        appointment.setPatient(patientRepository.findById(appointmentRequest.patientId()).orElseThrow());
        appointment.setDoctor(doctorRepository.findById(appointmentRequest.doctorId()).orElseThrow());
        
        // Auto-confirm appointment if slot is available (no overlapping appointments)
        // If slot is available and doctor has availability, auto-confirm
        if (overlapping.isEmpty() && isAvailable) {
            appointment.setStatus(AppointmentStatus.CONFIRMED);
        } else {
            appointment.setStatus(AppointmentStatus.SCHEDULED); // Request pending doctor confirmation
        }
        
        appointment.setCreatedAt(Instant.now());
        Appointment saved = appointmentRepository.save(appointment);
        
        // Reload appointment with relationships to avoid LazyInitializationException
        Appointment appointmentWithRelations = appointmentRepository.findById(saved.getId())
                .orElseThrow(() -> new RuntimeException("Failed to reload appointment"));
        
        // Send appropriate email notification based on status
        if (saved.getStatus() == AppointmentStatus.CONFIRMED) {
            emailService.sendAppointmentConfirmed(appointmentWithRelations);
        } else {
            emailService.sendAppointmentConfirmation(appointmentWithRelations);
        }
        
        // Record appointment creation metrics
        telemedicineMetrics.recordAppointmentCreated();
        telemedicineMetrics.recordAppointmentCreationTime(timer);
        
        return appointmentMapper.toAppointmentResponseDto(appointmentWithRelations);
        } catch (Exception e) {
            // Stop timer on exception (record time even on failure)
            telemedicineMetrics.recordAppointmentCreationTime(timer);
            throw e;
        }
    }

    @CacheEvict(value = {"appointments", "appointmentsByPatient", "appointmentsByDoctor", "appointmentById"}, allEntries = true)
    public void deleteAppointment(UUID id) {
        // Hard delete - only used by admins
        appointmentRepository.deleteById(id);
    }

    @CacheEvict(value = {"appointments", "appointmentsByPatient", "appointmentsByDoctor", "appointmentById"}, allEntries = true)
    public void softDeleteAppointmentByPatient(UUID id) {
        Appointment appointment = appointmentRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("Appointment not found"));
        appointment.setDeletedByPatient(true);
        appointmentRepository.save(appointment);
    }

    public void softDeleteAppointmentByDoctor(UUID id) {
        Appointment appointment = appointmentRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("Appointment not found"));
        appointment.setDeletedByDoctor(true);
        appointmentRepository.save(appointment);
    }
    
    /**
     * Remove patient from doctor's history by soft-deleting all appointments between them
     * This effectively hides the patient from the doctor's view
     */
    @Transactional(rollbackFor = {Exception.class})
    public void removePatientFromDoctorHistory(UUID doctorId, UUID patientId) {
        List<Appointment> appointments = appointmentRepository.findByDoctorIdAndPatientId(doctorId, patientId);
        for (Appointment appointment : appointments) {
            appointment.setDeletedByDoctor(true);
            appointmentRepository.save(appointment);
        }
    }
    
    /**
     * Remove doctor from patient's history by soft-deleting all appointments between them
     * This effectively hides the doctor from the patient's view
     */
    @Transactional(rollbackFor = {Exception.class})
    public void removeDoctorFromPatientHistory(UUID doctorId, UUID patientId) {
        List<Appointment> appointments = appointmentRepository.findByDoctorIdAndPatientId(doctorId, patientId);
        for (Appointment appointment : appointments) {
            appointment.setDeletedByPatient(true);
            appointmentRepository.save(appointment);
        }
    }

    @CacheEvict(value = {"appointments", "appointmentsByPatient", "appointmentsByDoctor", "appointmentById"}, allEntries = true)
    public AppointmentResponseDto changeAppointmentStatus(UUID id, AppointmentStatus status) {
        Appointment appointment = appointmentRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("Appointment cannot be null"));
        appointment.setStatus(status);
        Appointment saved = appointmentRepository.save(appointment);
        
        // Reload appointment with relationships to avoid LazyInitializationException
        Appointment appointmentWithRelations = appointmentRepository.findById(saved.getId())
                .orElseThrow(() -> new RuntimeException("Failed to reload appointment"));
        
        // Send email notifications based on status change
        if (status == AppointmentStatus.CANCELLED) {
            emailService.sendAppointmentCancellation(appointmentWithRelations);
        } else if (status == AppointmentStatus.CONFIRMED) {
            emailService.sendAppointmentConfirmed(appointmentWithRelations);
        } else if (status == AppointmentStatus.COMPLETED) {
            emailService.sendAppointmentCompleted(appointmentWithRelations);
        }
        
        return appointmentMapper.toAppointmentResponseDto(appointmentWithRelations);
    }

    public List<String> getAvailableTimeSlots(UUID doctorId, LocalDate date, Integer durationMinutes) {
        DayOfWeek dayOfWeek = DayOfWeek.valueOf(date.getDayOfWeek().name());
        
        // Get doctor's default duration if not provided
        Doctor doctor = doctorRepository.findById(doctorId).orElse(null);
        
        // Use provided durationMinutes if present, otherwise use doctor's default, fallback to 30
        int slotDuration;
        if (durationMinutes != null && durationMinutes > 0) {
            slotDuration = durationMinutes;
        } else if (doctor != null && doctor.getDefaultDurationMinutes() != null && doctor.getDefaultDurationMinutes() > 0) {
            slotDuration = doctor.getDefaultDurationMinutes();
        } else {
            slotDuration = 30; // Default fallback
        }
        
        // Log for debugging
        log.debug("getAvailableTimeSlots - doctorId: {}, provided durationMinutes: {}, doctor defaultDurationMinutes: {}, final slotDuration: {}", 
                  doctorId, durationMinutes, doctor != null ? doctor.getDefaultDurationMinutes() : "null", slotDuration);
        
        // Get doctor's availability slots for this day
        List<com.gnx.telemedicine.dto.availability.AvailabilityResponseDto> availabilitiesDto = doctorAvailabilityService.getDoctorAvailabilityByDay(doctorId, dayOfWeek);
        availabilitiesDto = availabilitiesDto.stream()
                .filter(av -> av.isAvailable() != null && av.isAvailable())
                .toList();
        
        if (availabilitiesDto.isEmpty()) {
            return new ArrayList<>();
        }
        
        // Get existing appointments for this doctor on this date
        List<Appointment> existingAppointments = appointmentRepository.findByDoctorIdAndDate(doctorId, date);
        
        List<String> availableSlots = new ArrayList<>();
        
        // Generate slots based on doctor's duration preference for each availability window
        for (com.gnx.telemedicine.dto.availability.AvailabilityResponseDto availability : availabilitiesDto) {
            LocalTime start = availability.startTime();
            LocalTime end = availability.endTime();
            
            LocalTime current = start;
            while (current.isBefore(end)) {
                final LocalTime slotStart = current;
                LocalTime slotEnd = current.plusMinutes(slotDuration);
                if (slotEnd.isAfter(end)) {
                    break;
                }
                final LocalTime slotEndFinal = slotEnd;
                
                // Check if this slot overlaps with any existing appointment
                // Two time ranges overlap if one starts before the other ends
                // They don't overlap if: slot ends <= appointment starts OR slot starts >= appointment ends
                boolean isBooked = existingAppointments.stream().anyMatch(apt -> {
                    if (apt.getStatus() == AppointmentStatus.CANCELLED) {
                        return false; // Cancelled appointments don't block slots
                    }
                    LocalTime aptStart = apt.getScheduledTime();
                    LocalTime aptEnd = aptStart.plusMinutes(apt.getDurationMinutes());
                    // Slots don't overlap if one completely ends before the other starts
                    // This means adjacent slots (9:00-9:30 and 9:30-10:00) are NOT overlapping
                    boolean noOverlap = slotEndFinal.isBefore(aptStart) || slotStart.isAfter(aptEnd) || 
                                       slotEndFinal.equals(aptStart) || slotStart.equals(aptEnd);
                    return !noOverlap; // If there's overlap, slot is booked
                });
                
                if (!isBooked) {
                    availableSlots.add(slotStart.format(DateTimeFormatter.ofPattern("HH:mm")));
                }
                
                current = current.plusMinutes(slotDuration);
            }
        }
        
        return availableSlots;
    }
}
