package com.saas.tenant.service;

import com.saas.tenant.dto.request.CreateAppointmentRequest;
import com.saas.tenant.dto.request.UpdateAppointmentRequest;
import com.saas.tenant.dto.response.AppointmentResponse;
import com.saas.tenant.entity.*;
import com.saas.tenant.repository.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
@Slf4j
public class AppointmentService {

    private final AppointmentRepository appointmentRepository;
    private final PatientRepository patientRepository;
    private final DoctorRepository doctorRepository;
    private final PhysicalResourceRepository resourceRepository;
    private final MedicalServiceRepository medicalServiceRepository;

    @Transactional
    public AppointmentResponse createAppointment(CreateAppointmentRequest request) {
        log.info("Creating new appointment for patient ID: {}", request.getPatientId());

        // Validate entities exist
        Patient patient = patientRepository.findById(request.getPatientId())
                .orElseThrow(() -> new RuntimeException("Patient not found with ID: " + request.getPatientId()));

        Doctor doctor = doctorRepository.findById(request.getDoctorId())
                .orElseThrow(() -> new RuntimeException("Doctor not found with ID: " + request.getDoctorId()));

        MedicalService medicalService = medicalServiceRepository.findById(request.getMedicalServiceId())
                .orElseThrow(() -> new RuntimeException(
                        "Medical service not found with ID: " + request.getMedicalServiceId()));

        PhysicalResource resource = null;
        if (request.getResourceId() != null) {
            resource = resourceRepository.findById(request.getResourceId())
                    .orElseThrow(() -> new RuntimeException(
                            "Physical resource not found with ID: " + request.getResourceId()));
        }

        // Check for conflicts
        LocalDateTime endTime = request.getAppointmentDateTime().plusMinutes(request.getDurationMinutes());
        checkForConflicts(request.getDoctorId(), request.getResourceId(),
                request.getAppointmentDateTime(), endTime);

        // Create appointment
        Appointment appointment = new Appointment();
        appointment.setPatient(patient);
        appointment.setDoctor(doctor);
        appointment.setResource(resource);
        appointment.setMedicalService(medicalService);
        appointment.setAppointmentDateTime(request.getAppointmentDateTime());
        appointment.setDurationMinutes(request.getDurationMinutes());
        appointment.setStatus(AppointmentStatus.PENDING);
        appointment.setBookingChannel(request.getBookingChannel());
        appointment.setPatientNotes(request.getPatientNotes());
        appointment.setIsAiScheduled(false);

        Appointment savedAppointment = appointmentRepository.save(appointment);
        log.info("Appointment created successfully with ID: {}", savedAppointment.getId());

        return mapToResponse(savedAppointment);
    }

    @Transactional
    public AppointmentResponse updateAppointment(Long id, UpdateAppointmentRequest request) {
        log.info("Updating appointment with ID: {}", id);

        Appointment appointment = appointmentRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("Appointment not found with ID: " + id));

        // If rescheduling, check for conflicts
        if (request.getAppointmentDateTime() != null || request.getDurationMinutes() != null) {
            LocalDateTime newDateTime = request.getAppointmentDateTime() != null ? request.getAppointmentDateTime()
                    : appointment.getAppointmentDateTime();
            Integer newDuration = request.getDurationMinutes() != null ? request.getDurationMinutes()
                    : appointment.getDurationMinutes();
            LocalDateTime endTime = newDateTime.plusMinutes(newDuration);

            Long doctorId = request.getDoctorId() != null ? request.getDoctorId() : appointment.getDoctor().getId();
            Long resourceId = request.getResourceId() != null ? request.getResourceId()
                    : (appointment.getResource() != null ? appointment.getResource().getId() : null);

            checkForConflicts(doctorId, resourceId, newDateTime, endTime, id);
        }

        // Update fields
        if (request.getPatientId() != null) {
            Patient patient = patientRepository.findById(request.getPatientId())
                    .orElseThrow(() -> new RuntimeException("Patient not found"));
            appointment.setPatient(patient);
        }

        if (request.getDoctorId() != null) {
            Doctor doctor = doctorRepository.findById(request.getDoctorId())
                    .orElseThrow(() -> new RuntimeException("Doctor not found"));
            appointment.setDoctor(doctor);
        }

        if (request.getResourceId() != null) {
            PhysicalResource resource = resourceRepository.findById(request.getResourceId())
                    .orElseThrow(() -> new RuntimeException("Resource not found"));
            appointment.setResource(resource);
        }

        if (request.getMedicalServiceId() != null) {
            MedicalService medicalService = medicalServiceRepository.findById(request.getMedicalServiceId())
                    .orElseThrow(() -> new RuntimeException("Medical service not found"));
            appointment.setMedicalService(medicalService);
        }

        if (request.getAppointmentDateTime() != null)
            appointment.setAppointmentDateTime(request.getAppointmentDateTime());
        if (request.getDurationMinutes() != null)
            appointment.setDurationMinutes(request.getDurationMinutes());
        if (request.getStatus() != null) {
            appointment.setStatus(request.getStatus());

            // Update status timestamps
            if (request.getStatus() == AppointmentStatus.CONFIRMED) {
                appointment.setConfirmedAt(LocalDateTime.now());
            } else if (request.getStatus() == AppointmentStatus.CANCELED) {
                appointment.setCanceledAt(LocalDateTime.now());
            } else if (request.getStatus() == AppointmentStatus.COMPLETED) {
                appointment.setCompletedAt(LocalDateTime.now());
            }
        }
        if (request.getCancellationReason() != null)
            appointment.setCancellationReason(request.getCancellationReason());
        if (request.getPatientNotes() != null)
            appointment.setPatientNotes(request.getPatientNotes());
        if (request.getDoctorNotes() != null)
            appointment.setDoctorNotes(request.getDoctorNotes());

        Appointment updatedAppointment = appointmentRepository.save(appointment);
        log.info("Appointment updated successfully: {}", updatedAppointment.getId());

        return mapToResponse(updatedAppointment);
    }

    @Transactional(readOnly = true)
    public AppointmentResponse getAppointmentById(Long id) {
        log.info("Fetching appointment with ID: {}", id);
        Appointment appointment = appointmentRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("Appointment not found with ID: " + id));
        return mapToResponse(appointment);
    }

    @Transactional(readOnly = true)
    public List<AppointmentResponse> getAllAppointments() {
        log.info("Fetching all appointments");
        return appointmentRepository.findAll().stream()
                .map(this::mapToResponse)
                .collect(Collectors.toList());
    }

    @Transactional(readOnly = true)
    public List<AppointmentResponse> getAppointmentsByPatient(Long patientId) {
        log.info("Fetching appointments for patient ID: {}", patientId);
        return appointmentRepository.findByPatientId(patientId).stream()
                .map(this::mapToResponse)
                .collect(Collectors.toList());
    }

    @Transactional(readOnly = true)
    public List<AppointmentResponse> getAppointmentsByDoctor(Long doctorId) {
        log.info("Fetching appointments for doctor ID: {}", doctorId);
        return appointmentRepository.findByDoctorId(doctorId).stream()
                .map(this::mapToResponse)
                .collect(Collectors.toList());
    }

    @Transactional(readOnly = true)
    public List<AppointmentResponse> getAppointmentsByDateRange(LocalDateTime startDate, LocalDateTime endDate) {
        log.info("Fetching appointments between {} and {}", startDate, endDate);
        return appointmentRepository.findByDateRange(startDate, endDate).stream()
                .map(this::mapToResponse)
                .collect(Collectors.toList());
    }

    @Transactional(readOnly = true)
    public List<AppointmentResponse> getDoctorSchedule(Long doctorId, LocalDateTime startDate, LocalDateTime endDate) {
        log.info("Fetching doctor schedule for doctor ID: {} between {} and {}", doctorId, startDate, endDate);
        return appointmentRepository.findDoctorSchedule(doctorId, startDate, endDate).stream()
                .map(this::mapToResponse)
                .collect(Collectors.toList());
    }

    @Transactional
    public AppointmentResponse cancelAppointment(Long id, String reason) {
        log.info("Canceling appointment with ID: {}", id);

        Appointment appointment = appointmentRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("Appointment not found with ID: " + id));

        appointment.setStatus(AppointmentStatus.CANCELED);
        appointment.setCancellationReason(reason);
        appointment.setCanceledAt(LocalDateTime.now());

        Appointment canceledAppointment = appointmentRepository.save(appointment);
        log.info("Appointment canceled successfully: {}", id);

        return mapToResponse(canceledAppointment);
    }

    @Transactional
    public AppointmentResponse confirmAppointment(Long id) {
        log.info("Confirming appointment with ID: {}", id);

        Appointment appointment = appointmentRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("Appointment not found with ID: " + id));

        appointment.setStatus(AppointmentStatus.CONFIRMED);
        appointment.setConfirmedAt(LocalDateTime.now());

        Appointment confirmedAppointment = appointmentRepository.save(appointment);
        log.info("Appointment confirmed successfully: {}", id);

        return mapToResponse(confirmedAppointment);
    }

    @Transactional
    public void deleteAppointment(Long id) {
        log.info("Deleting appointment with ID: {}", id);
        appointmentRepository.deleteById(id);
        log.info("Appointment deleted successfully: {}", id);
    }

    private void checkForConflicts(Long doctorId, Long resourceId,
            LocalDateTime startTime, LocalDateTime endTime) {
        checkForConflicts(doctorId, resourceId, startTime, endTime, null);
    }

    private void checkForConflicts(Long doctorId, Long resourceId,
            LocalDateTime startTime, LocalDateTime endTime, Long excludeAppointmentId) {
        // Check doctor conflicts
        List<Appointment> doctorConflicts = appointmentRepository.findConflictingAppointments(
                doctorId, startTime, endTime);

        if (excludeAppointmentId != null) {
            doctorConflicts = doctorConflicts.stream()
                    .filter(a -> !a.getId().equals(excludeAppointmentId))
                    .collect(Collectors.toList());
        }

        if (!doctorConflicts.isEmpty()) {
            throw new RuntimeException("Doctor is not available at the requested time");
        }

        // Check resource conflicts if resource is specified
        if (resourceId != null) {
            List<Appointment> resourceConflicts = appointmentRepository.findResourceSchedule(
                    resourceId, startTime, endTime);

            if (excludeAppointmentId != null) {
                resourceConflicts = resourceConflicts.stream()
                        .filter(a -> !a.getId().equals(excludeAppointmentId))
                        .collect(Collectors.toList());
            }

            if (!resourceConflicts.isEmpty()) {
                throw new RuntimeException("Resource is not available at the requested time");
            }
        }
    }

    private AppointmentResponse mapToResponse(Appointment appointment) {
        return AppointmentResponse.builder()
                .id(appointment.getId())
                .patientId(appointment.getPatient().getId())
                .patientName(appointment.getPatient().getFirstName() + " " + appointment.getPatient().getLastName())
                .doctorId(appointment.getDoctor().getId())
                .doctorName(appointment.getDoctor().getFirstName() + " " + appointment.getDoctor().getLastName())
                .resourceId(appointment.getResource() != null ? appointment.getResource().getId() : null)
                .resourceName(appointment.getResource() != null ? appointment.getResource().getName() : null)
                .medicalServiceId(appointment.getMedicalService().getId())
                .medicalServiceName(appointment.getMedicalService().getName())
                .appointmentDateTime(appointment.getAppointmentDateTime())
                .durationMinutes(appointment.getDurationMinutes())
                .status(appointment.getStatus())
                .bookingChannel(appointment.getBookingChannel())
                .aiSuggestedAlternatives(appointment.getAiSuggestedAlternatives())
                .isAiScheduled(appointment.getIsAiScheduled())
                .cancellationReason(appointment.getCancellationReason())
                .patientNotes(appointment.getPatientNotes())
                .doctorNotes(appointment.getDoctorNotes())
                .reminderSent(appointment.getReminderSent())
                .confirmedAt(appointment.getConfirmedAt())
                .canceledAt(appointment.getCanceledAt())
                .completedAt(appointment.getCompletedAt())
                .createdAt(appointment.getCreatedAt())
                .updatedAt(appointment.getUpdatedAt())
                .build();
    }
}
