package com.petshop.backend.service;

import com.petshop.backend.dto.appointment.AppointmentRequest;
import com.petshop.backend.dto.appointment.AppointmentResponse;
import com.petshop.backend.dto.common.BulkDeleteRequest;
import com.petshop.backend.entity.Adoption;
import com.petshop.backend.entity.Appointment;
import com.petshop.backend.entity.Pet;
import com.petshop.backend.entity.StoreLocation;
import com.petshop.backend.entity.User;
import com.petshop.backend.exception.BusinessException;
import com.petshop.backend.exception.ResourceNotFoundException;
import com.petshop.backend.event.AdoptionReminderEvent;
import com.petshop.backend.event.AppointmentConfirmedEvent;
import com.petshop.backend.event.AppointmentReminderEvent;
import com.petshop.backend.repository.AdoptionRepository;
import com.petshop.backend.repository.AppointmentRepository;
import com.petshop.backend.repository.PetRepository;
import com.petshop.backend.repository.ServiceRepository;
import com.petshop.backend.repository.StoreRepository;
import com.petshop.backend.repository.UserRepository;
import com.petshop.backend.util.AuthenticationHelper;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

@Service
public class AppointmentService {

    private final AppointmentRepository appointmentRepository;
    private final ServiceRepository serviceRepository;
    private final PetRepository petRepository;
    private final StoreRepository storeRepository;
    private final UserRepository userRepository;
    private final AdoptionRepository adoptionRepository;
    private final ApplicationEventPublisher eventPublisher;

    public AppointmentService(AppointmentRepository appointmentRepository, ServiceRepository serviceRepository, PetRepository petRepository, StoreRepository storeRepository, UserRepository userRepository, AdoptionRepository adoptionRepository, ApplicationEventPublisher eventPublisher) {
        this.appointmentRepository = appointmentRepository;
        this.serviceRepository = serviceRepository;
        this.petRepository = petRepository;
        this.storeRepository = storeRepository;
        this.userRepository = userRepository;
        this.adoptionRepository = adoptionRepository;
        this.eventPublisher = eventPublisher;
    }

    @Transactional(readOnly = true)
    public Page<AppointmentResponse> getAllAppointments(
            String query,
            Long customerId,
            Long employeeId,
            Long storeId,
            String status,
            LocalDate date,
            Pageable pageable) {

        String normalizedQuery = normalizeFilter(query);
        String normalizedStatus = normalizeFilter(status);

        Page<Appointment> appointments = appointmentRepository.searchAppointments(
                normalizedQuery,
                customerId,
                employeeId,
                storeId,
                normalizedStatus,
                date,
                pageable
        );

        return appointments.map(this::mapToResponse);
    }

    @Transactional(readOnly = true)
    public AppointmentResponse getAppointmentById(Long id, Long customerId) {
        Appointment appointment = appointmentRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("Appointment not found with id: " + id));

        if (customerId != null && !appointment.getCustomer().getId().equals(customerId)) {
            throw new ResourceNotFoundException("You can only view your own appointments");
        }

        return mapToResponse(appointment);
    }

    @Transactional
    public AppointmentResponse createAppointment(AppointmentRequest request) {
        validateAppointmentRequest(request);

        User authenticatedUser = AuthenticationHelper.getAuthenticatedUser(userRepository);

        User customer = userRepository.findById(request.getCustomerId())
                .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId()));

        StoreLocation store = storeRepository.findById(request.getStoreId())
                .orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + request.getStoreId()));

        com.petshop.backend.entity.Service service = serviceRepository.findById(request.getServiceId())
                .orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + request.getServiceId()));

        Pet pet = request.getPetId() != null ? fetchPet(request.getPetId()) : null;
        User employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId());

        // Customers must supply a pet that is Adopted and owned by them
        if (User.Role.CUSTOMER.equals(authenticatedUser.getRole())) {
            if (pet == null) {
                throw new IllegalArgumentException("A pet must be selected for your appointment");
            }
            if (pet.getOwner() == null || !pet.getOwner().getId().equals(authenticatedUser.getId())) {
                throw new IllegalArgumentException("The selected pet does not belong to your account");
            }
            String petStatus = pet.getPetStatus();
            if (!"Owned".equalsIgnoreCase(petStatus) && !"Adopted".equalsIgnoreCase(petStatus)) {
                throw new IllegalArgumentException("Only your own pets can be booked for appointments");
            }
        }

        validateSpeciesServiceCompatibility(pet, service);

        validateStoreAccess(store.getStoreId(), authenticatedUser);
        validatePetServiceCompatibility(pet, service);
        validateAvailability(employee, service, request.getAppointmentDate(), request.getAppointmentTime(), null);
        validatePetAvailability(pet, service, request.getAppointmentDate(), request.getAppointmentTime(), null);

        Appointment appointment = new Appointment();
        appointment.setCustomer(customer);
        appointment.setStore(store);
        appointment.setService(service);
        appointment.setEmployee(employee);
        appointment.setAppointmentDate(request.getAppointmentDate());
        appointment.setAppointmentTime(request.getAppointmentTime());
        appointment.setAppointmentStatus(request.getAppointmentStatus());
        appointment.setPet(pet);

        appointment = appointmentRepository.save(appointment);
        eventPublisher.publishEvent(new AppointmentConfirmedEvent(appointment.getAppointmentId()));
        return mapToResponse(appointment);
    }

    @Transactional
    public AppointmentResponse updateAppointment(Long id, AppointmentRequest request) {
        validateAppointmentRequest(request);

        User authenticatedUser = AuthenticationHelper.getAuthenticatedUser(userRepository);

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

        User customer = userRepository.findById(request.getCustomerId())
                .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId()));

        StoreLocation store = storeRepository.findById(request.getStoreId())
                .orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + request.getStoreId()));

        com.petshop.backend.entity.Service service = serviceRepository.findById(request.getServiceId())
                .orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + request.getServiceId()));

        Pet pet = request.getPetId() != null ? fetchPet(request.getPetId()) : null;
        User employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId());

        validateStoreAccess(store.getStoreId(), authenticatedUser);
        validatePetServiceCompatibility(pet, service);
        validateAvailability(employee, service, request.getAppointmentDate(), request.getAppointmentTime(), id);
        validatePetAvailability(pet, service, request.getAppointmentDate(), request.getAppointmentTime(), id);

        appointment.setCustomer(customer);
        appointment.setStore(store);
        appointment.setService(service);
        appointment.setAppointmentDate(request.getAppointmentDate());
        appointment.setAppointmentTime(request.getAppointmentTime());
        appointment.setAppointmentStatus(request.getAppointmentStatus());
        appointment.setPet(pet);
        appointment.setEmployee(employee);

        appointment = appointmentRepository.save(appointment);
        eventPublisher.publishEvent(new AppointmentConfirmedEvent(appointment.getAppointmentId()));
        return mapToResponse(appointment);
    }

    @Transactional
    public AppointmentResponse cancelAppointment(Long appointmentId, Long requestingCustomerId) {
        Appointment appointment = appointmentRepository.findById(appointmentId)
                .orElseThrow(() -> new ResourceNotFoundException("Appointment not found with id: " + appointmentId));

        if (requestingCustomerId != null && !appointment.getCustomer().getId().equals(requestingCustomerId)) {
            throw new ResourceNotFoundException("Appointment not found");
        }

        if (!"Booked".equalsIgnoreCase(appointment.getAppointmentStatus())) {
            throw new IllegalArgumentException("Only booked appointments can be cancelled");
        }

        appointment.setAppointmentStatus("Cancelled");
        return mapToResponse(appointmentRepository.save(appointment));
    }

    @Transactional
    public void deleteAppointment(Long id) {
        if (!appointmentRepository.existsById(id)) {
            throw new ResourceNotFoundException("Appointment not found with id: " + id);
        }
        appointmentRepository.deleteById(id);
    }

    @Transactional
    public void bulkDeleteAppointments(BulkDeleteRequest request) {
        appointmentRepository.deleteAllById(request.getIds());
    }

    @Transactional(readOnly = true)
    public List<String> checkAvailability(Long storeId, Long serviceId, LocalDate date) {
        storeRepository.findById(storeId)
                .orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + storeId));

        com.petshop.backend.entity.Service service = serviceRepository.findById(serviceId)
                .orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + serviceId));

        List<User> assignableUsers = userRepository.findByPrimaryStoreStoreIdAndRoleAndActiveTrue(storeId, User.Role.STAFF);

        if (assignableUsers.isEmpty()) {
            return List.of();
        }

        List<Long> employeeIds = assignableUsers.stream().map(User::getId).collect(Collectors.toList());
        List<Appointment> allAppointments = appointmentRepository.findByEmployeeIdInAndAppointmentDate(employeeIds, date);

        java.util.Map<Long, List<Appointment>> appointmentsByEmployee = allAppointments.stream()
                .collect(Collectors.groupingBy(a -> a.getEmployee().getId()));

        List<String> availableSlots = new ArrayList<>();
        LocalTime startTime = LocalTime.of(9, 0);
        LocalTime endTime = LocalTime.of(17, 0);
        LocalTime latestStart = endTime.minusMinutes(service.getServiceDuration());

        LocalTime currentTime = startTime;
        while (!currentTime.isAfter(latestStart)) {
            final LocalTime slotTime = currentTime;
            boolean anyEmployeeAvailable = assignableUsers.stream().anyMatch(emp -> {
                List<Appointment> empAppointments = appointmentsByEmployee.getOrDefault(emp.getId(), List.of());
                return isSlotAvailable(empAppointments, service, slotTime, null);
            });

            if (anyEmployeeAvailable) {
                availableSlots.add(currentTime.toString());
            }
            currentTime = currentTime.plusMinutes(30);
        }

        return availableSlots;
    }

    //Update booked status to completed at every midnight, and send 24h reminders
    @Scheduled(cron = "0 0 0 * * ?")
    @Transactional
    public void updatePastAppointmentsStatus() {
        LocalDate currentDate = LocalDate.now();
        LocalTime currentTime = LocalTime.now();
        List<Appointment> pastBookedAppointments = appointmentRepository.findPastBookedAppointments(currentDate, currentTime);

        for (Appointment appointment : pastBookedAppointments) {
            appointment.setAppointmentStatus("Completed");
            appointmentRepository.save(appointment);
        }

        LocalDate tomorrow = currentDate.plusDays(1);

        List<Appointment> tomorrowAppointments = appointmentRepository
                .findByAppointmentDateAndAppointmentStatusIgnoreCase(tomorrow, "Booked");
        for (Appointment appointment : tomorrowAppointments) {
            eventPublisher.publishEvent(new AppointmentReminderEvent(appointment.getAppointmentId()));
        }

        List<Adoption> tomorrowAdoptions = adoptionRepository
                .findByAdoptionDateAndAdoptionStatusIgnoreCase(tomorrow, "Pending");
        for (Adoption adoption : tomorrowAdoptions) {
            eventPublisher.publishEvent(new AdoptionReminderEvent(adoption.getAdoptionId()));
        }
    }

    private String normalizeFilter(String value) {
        if (value == null) {
            return null;
        }
        String trimmed = value.trim();
        return trimmed.isEmpty() ? null : trimmed;
    }

    private void validatePetServiceCompatibility(Pet pet, com.petshop.backend.entity.Service service) {
        if (pet == null) return;
        Set<String> allowed = service.getSpecies();
        if (allowed == null || allowed.isEmpty()) return;
        boolean compatible = allowed.stream().anyMatch(s -> s.equalsIgnoreCase(pet.getPetSpecies()));
        if (!compatible) {
            throw new BusinessException(
                    "Service \"" + service.getServiceName() + "\" is not available for " + pet.getPetSpecies());
        }
    }

    private void validateAppointmentRequest(AppointmentRequest request) {
        if ("Booked".equalsIgnoreCase(request.getAppointmentStatus())) {
            LocalDateTime appointmentDateTime = LocalDateTime.of(request.getAppointmentDate(), request.getAppointmentTime());
            if (appointmentDateTime.isBefore(LocalDateTime.now())) {
                throw new IllegalArgumentException("Booked appointments must be scheduled in the future");
            }
        }
    }

    private Pet fetchPet(Long petId) {
        return petRepository.findById(petId)
                .orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + petId));
    }

    private AppointmentResponse mapToResponse(Appointment appointment) {
        Pet pet = appointment.getPet();

        AppointmentResponse response = new AppointmentResponse();
        response.setAppointmentId(appointment.getAppointmentId());
        response.setCustomerId(appointment.getCustomer().getId());
        response.setCustomerName(appointment.getCustomer().getFirstName() + " " + appointment.getCustomer().getLastName());
        response.setStoreId(appointment.getStore().getStoreId());
        response.setStoreName(appointment.getStore().getStoreName());
        response.setServiceId(appointment.getService().getServiceId());
        response.setServiceName(appointment.getService().getServiceName());
        response.setAppointmentDate(appointment.getAppointmentDate());
        response.setAppointmentTime(appointment.getAppointmentTime());
        response.setAppointmentStatus(appointment.getAppointmentStatus());
        response.setEmployeeId(appointment.getEmployee().getId());
        response.setEmployeeName(appointment.getEmployee().getFirstName() + " " + appointment.getEmployee().getLastName());
        response.setPetName(pet != null ? pet.getPetName() : null);
        response.setPetId(pet != null ? pet.getPetId() : null);
        response.setCreatedAt(appointment.getCreatedAt());
        response.setUpdatedAt(appointment.getUpdatedAt());

        return response;

    }

    private User resolveAppointmentEmployee(Long requestedEmployeeId, Long storeId) {
        List<User> assignableUsers = userRepository.findByPrimaryStoreStoreIdAndRoleAndActiveTrue(storeId, User.Role.STAFF);

        if (requestedEmployeeId != null) {
            User employee = userRepository.findById(requestedEmployeeId)
                    .orElseThrow(() -> new ResourceNotFoundException("Employee not found with id: " + requestedEmployeeId));
            boolean assignedToStore = assignableUsers.stream()
                    .anyMatch(u -> u.getId().equals(requestedEmployeeId));
            if (!assignedToStore) {
                throw new IllegalArgumentException("Selected employee is not assignable for the selected store");
            }
            return employee;
        }

        return assignableUsers.stream()
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("No assignable staff member is assigned to the selected store"));
    }

    private void validateAvailability(User employee, com.petshop.backend.entity.Service service, LocalDate date, LocalTime time, Long appointmentIdToIgnore) {
        List<Appointment> existingAppointments = appointmentRepository
                .findByEmployeeIdAndAppointmentDate(employee.getId(), date);
        if (!isSlotAvailable(existingAppointments, service, time, appointmentIdToIgnore)) {
            throw new IllegalArgumentException("The selected employee is already booked for this time slot");
        }
    }

    private boolean isSlotAvailable(List<Appointment> existingAppointments, com.petshop.backend.entity.Service requestedService, LocalTime requestedStart, Long appointmentIdToIgnore) {
        LocalTime requestedEnd = requestedStart.plusMinutes(requestedService.getServiceDuration());
        for (Appointment existingAppointment : existingAppointments) {
            if (appointmentIdToIgnore != null && appointmentIdToIgnore.equals(existingAppointment.getAppointmentId())) {
                continue;
            }
            LocalTime existingStart = existingAppointment.getAppointmentTime();
            LocalTime existingEnd = existingStart.plusMinutes(existingAppointment.getService().getServiceDuration());
            if (requestedStart.isBefore(existingEnd) && existingStart.isBefore(requestedEnd)) {
                return false;
            }
        }
        return true;
    }

    private void validatePetAvailability(Pet pet, com.petshop.backend.entity.Service service, LocalDate date, LocalTime time, Long appointmentIdToIgnore) {
        if (pet == null) return;
        List<Appointment> existingAppointments = appointmentRepository
                .findByPetIdAndAppointmentDate(pet.getPetId(), date);
        if (!isSlotAvailable(existingAppointments, service, time, appointmentIdToIgnore)) {
            throw new IllegalArgumentException("This pet already has an appointment during this time slot");
        }
    }

    private void validateSpeciesServiceCompatibility(Pet pet, com.petshop.backend.entity.Service service) {
        if (pet == null || service == null) return;
        String species = pet.getPetSpecies();
        if (species == null) return;
        String serviceName = service.getServiceName().toLowerCase();

        switch (species.toLowerCase()) {
            case "bird":
                if (!serviceName.contains("wing clipping") && !serviceName.contains("beak and nail")) {
                    throw new IllegalArgumentException(
                        "Service '" + service.getServiceName() + "' is not available for birds. " +
                        "Allowed services: Wing Clipping, Beak and Nail Care.");
                }
                break;
            case "fish":
                if (!serviceName.contains("aquarium health")) {
                    throw new IllegalArgumentException(
                        "Service '" + service.getServiceName() + "' is not available for fish. " +
                        "Allowed service: Aquarium Health Check.");
                }
                break;
            default:
                break;
        }
    }

    private void validateStoreAccess(Long requestedStoreId, User user) {
        if (user.getRole() != User.Role.STAFF) {
            return;
        }

        if (user.getPrimaryStore() == null || !user.getPrimaryStore().getStoreId().equals(requestedStoreId)) {
            throw new AccessDeniedException("Staff can only manage appointments for their assigned store");
        }
    }
}
