package com.petshop.backend.service;

import com.petshop.backend.dto.common.BulkDeleteRequest;
import com.petshop.backend.dto.pet.MyPetRequest;
import com.petshop.backend.dto.pet.MyPetResponse;
import com.petshop.backend.dto.pet.PetRequest;
import com.petshop.backend.dto.pet.PetResponse;
import com.petshop.backend.entity.Adoption;
import com.petshop.backend.entity.Pet;
import com.petshop.backend.entity.StoreLocation;
import com.petshop.backend.entity.User;
import com.petshop.backend.exception.ResourceNotFoundException;
import com.petshop.backend.security.AppPrincipal;
import com.petshop.backend.repository.AdoptionRepository;
import com.petshop.backend.repository.AppointmentRepository;
import com.petshop.backend.repository.PetRepository;
import com.petshop.backend.repository.StoreRepository;
import com.petshop.backend.repository.UserRepository;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.List;
import java.util.Locale;

@Service
public class PetService {

    private final PetRepository petRepository;
    private final AdoptionRepository adoptionRepository;
    private final AppointmentRepository appointmentRepository;
    private final UserRepository userRepository;
    private final StoreRepository storeRepository;
    private final CatalogImageStorageService catalogImageStorageService;

    public PetService(PetRepository petRepository, AdoptionRepository adoptionRepository, AppointmentRepository appointmentRepository, UserRepository userRepository, StoreRepository storeRepository, CatalogImageStorageService catalogImageStorageService) {
        this.petRepository = petRepository;
        this.adoptionRepository = adoptionRepository;
        this.appointmentRepository = appointmentRepository;
        this.userRepository = userRepository;
        this.storeRepository = storeRepository;
        this.catalogImageStorageService = catalogImageStorageService;
    }

    @Transactional(readOnly = true)
    public Page<PetResponse> getAllPets(String query, String species, String breed, String status, Long storeId, Long customerId, Pageable pageable) {
        String normalizedQuery = normalizeFilter(query);
        String normalizedSpecies = normalizeFilter(species);
        String normalizedBreed = normalizeFilter(breed);
        String normalizedStatus = normalizeFilter(status);
        CurrentViewer viewer = getCurrentViewer();

        Page<Pet> pets;
        if (viewer == null) {
            if (!isAllowedPublicStatus(normalizedStatus)) {
                return new PageImpl<>(java.util.List.of(), pageable, 0);
            }
            pets = petRepository.searchPublicPets(normalizedQuery, normalizedSpecies, normalizedBreed, storeId, pageable);
        } else if (viewer.role() == User.Role.STAFF || viewer.role() == User.Role.ADMIN) {
            pets = petRepository.searchPets(normalizedQuery, normalizedSpecies, normalizedBreed, normalizedStatus, storeId, customerId, pageable);
        } else if (viewer.role() == User.Role.CUSTOMER) {
            if (!isAllowedCustomerStatus(normalizedStatus)) {
                return new PageImpl<>(java.util.List.of(), pageable, 0);
            }
            pets = petRepository.searchCustomerVisiblePets(viewer.userId(), normalizedQuery, normalizedSpecies, normalizedBreed, normalizedStatus, pageable);
        } else {
            pets = petRepository.searchPublicPets(normalizedQuery, normalizedSpecies, normalizedBreed, storeId, pageable);
        }

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

    @Transactional(readOnly = true)
    public PetResponse getPetById(Long id) {
        Pet pet = petRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + id));
        if (!canViewPet(pet, getCurrentViewer())) {
            throw new ResourceNotFoundException("Pet not found with id: " + id);
        }
        return mapToResponse(pet);
    }

    @Transactional(readOnly = true)
    public List<MyPetResponse> getMyPets(Long ownerUserId, String status) {
        return petRepository.findAllByOwner_IdOrderByPetNameAsc(ownerUserId).stream()
                .filter(p -> status == null || status.isBlank() || status.equalsIgnoreCase(p.getPetStatus()))
                .map(this::mapToMyPetResponse)
                .toList();
    }

    @Transactional
    public MyPetResponse createMyPet(Long ownerUserId, MyPetRequest request) {
        User owner = userRepository.findById(ownerUserId)
                .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + ownerUserId));
        Pet pet = new Pet();
        pet.setOwner(owner);
        pet.setStore(null);
        pet.setPetStatus("Owned");
        applyMyPetRequest(pet, request);
        return mapToMyPetResponse(petRepository.save(pet));
    }

    @Transactional
    public MyPetResponse updateMyPet(Long ownerUserId, Long petId, MyPetRequest request) {
        Pet pet = findOwnedPet(ownerUserId, petId);
        pet.setPetStatus("Owned");
        pet.setStore(null);
        applyMyPetRequest(pet, request);
        return mapToMyPetResponse(petRepository.save(pet));
    }

    @Transactional
    public void deleteMyPet(Long ownerUserId, Long petId) {
        Pet pet = findOwnedPet(ownerUserId, petId);
        List<com.petshop.backend.entity.Appointment> linkedAppointments = appointmentRepository.findByPet_Id(petId);
        boolean hasBooked = linkedAppointments.stream()
                .anyMatch(a -> "Booked".equalsIgnoreCase(a.getAppointmentStatus()));
        if (hasBooked) {
            throw new IllegalArgumentException(
                "Your pet has a booked appointment. Please cancel the appointment before removing your pet from our database.");
        }
        // Nullify the pet reference on non-booked appointments to avoid FK constraint violations
        for (com.petshop.backend.entity.Appointment appt : linkedAppointments) {
            appt.setPet(null);
        }
        appointmentRepository.saveAll(linkedAppointments);
        deleteStoredImageIfPresent(pet.getImageUrl());
        petRepository.delete(pet);
    }

    @Transactional
    public MyPetResponse uploadMyPetImage(Long ownerUserId, Long petId, MultipartFile file) throws IOException {
        validateImageFile(file);
        Pet pet = findOwnedPet(ownerUserId, petId);
        deleteStoredImageIfPresent(pet.getImageUrl());
        pet.setImageUrl(catalogImageStorageService.storePetImage(file));
        return mapToMyPetResponse(petRepository.save(pet));
    }

    @Transactional
    public PetResponse createPet(PetRequest request) {
        Pet pet = new Pet();
        pet.setPetName(request.getPetName());
        pet.setPetSpecies(request.getPetSpecies());
        pet.setPetBreed(request.getPetBreed());
        pet.setPetAge(request.getPetAge());
        pet.setPetStatus(request.getPetStatus());
        pet.setPetPrice(request.getPetPrice());
        applyOwnerAndStore(pet, request);

        pet = petRepository.save(pet);
        return mapToResponse(pet);
    }

    @Transactional
    public PetResponse updatePet(Long id, PetRequest request) {
        Pet pet = petRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + id));

        pet.setPetName(request.getPetName());
        pet.setPetSpecies(request.getPetSpecies());
        pet.setPetBreed(request.getPetBreed());
        pet.setPetAge(request.getPetAge());
        pet.setPetStatus(request.getPetStatus());
        pet.setPetPrice(request.getPetPrice());
        applyOwnerAndStore(pet, request);

        pet = petRepository.save(pet);
        return mapToResponse(pet);
    }

    @Transactional
    public void deletePet(Long id) {
        Pet pet = petRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + id));
        deleteStoredImageIfPresent(pet.getImageUrl());
        petRepository.delete(pet);
    }

    @Transactional
    public void bulkDeletePets(BulkDeleteRequest request) {
        petRepository.findAllById(request.getIds()).forEach(pet -> deleteStoredImageIfPresent(pet.getImageUrl()));
        petRepository.deleteAllById(request.getIds());
    }

    @Transactional
    public PetResponse uploadPetImage(Long id, MultipartFile file) throws IOException {
        validateImageFile(file);
        Pet pet = findPet(id);
        deleteStoredImageIfPresent(pet.getImageUrl());
        pet.setImageUrl(catalogImageStorageService.storePetImage(file));
        return mapToResponse(petRepository.save(pet));
    }

    @Transactional
    public PetResponse deletePetImage(Long id) {
        Pet pet = findPet(id);
        deleteStoredImageIfPresent(pet.getImageUrl());
        pet.setImageUrl(null);
        return mapToResponse(petRepository.save(pet));
    }

    public ImagePayload loadPetImage(Long id, Long requesterUserId, User.Role requesterRole) {
        Pet pet = findPet(id);
        if (pet.getImageUrl() == null || pet.getImageUrl().isBlank()) {
            throw new ResourceNotFoundException("Pet image not found for id: " + id);
        }
        if (!canViewPet(pet, new CurrentViewer(requesterUserId, requesterRole))) {
            throw new ForbiddenImageAccessException();
        }
        Resource resource = catalogImageStorageService.loadPetImage(pet.getImageUrl());
        MediaType mediaType = catalogImageStorageService.resolveMediaType(resource);
        return new ImagePayload(resource, mediaType);
    }

    public boolean isPubliclyVisible(Pet pet) {
        return "available".equalsIgnoreCase(normalizeStatus(pet.getPetStatus()));
    }

    private boolean canViewPet(Pet pet, CurrentViewer viewer) {
        if (isPubliclyVisible(pet)) {
            return true;
        }
        if (viewer != null && (viewer.role() == User.Role.STAFF || viewer.role() == User.Role.ADMIN)) {
            return true;
        }
        if (viewer == null || viewer.userId() == null) {
            return false;
        }
        if (isOwnedByUser(pet, viewer.userId())) {
            return true;
        }
        return isAdoptedByUser(pet, viewer.userId());
    }

    private boolean isAdoptedByUser(Pet pet, Long userId) {
        if (userId == null) {
            return false;
        }
        if (!"adopted".equalsIgnoreCase(normalizeStatus(pet.getPetStatus()))) {
            return false;
        }
        return adoptionRepository.findFirstByPet_IdAndAdoptionStatusOrderByAdoptionDateDesc(pet.getPetId(), "Completed")
                .map(Adoption::getCustomer)
                .map(customer -> userId.equals(customer.getId()))
                .orElse(false);
    }

    private CurrentViewer getCurrentViewer() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null || !authentication.isAuthenticated()) {
            return null;
        }
        Object principal = authentication.getPrincipal();
        if (principal instanceof AppPrincipal appPrincipal) {
            return new CurrentViewer(appPrincipal.getUserId(), appPrincipal.getRole());
        }
        String username = authentication.getName();
        if (username == null || username.isBlank() || "anonymousUser".equalsIgnoreCase(username)) {
            return null;
        }
        return userRepository.findByUsername(username)
                .map(user -> new CurrentViewer(user.getId(), user.getRole()))
                .orElse(null);
    }

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

    private void validateImageFile(MultipartFile file) {
        if (file == null || file.isEmpty()) {
            throw new IllegalArgumentException("Please select an image to upload");
        }
        if (file.getSize() > 5 * 1024 * 1024) {
            throw new IllegalArgumentException("Image file size must be less than 5MB");
        }
        String contentType = file.getContentType();
        if (contentType == null) {
            throw new IllegalArgumentException("Only JPG, PNG, and GIF images are allowed");
        }
        String normalized = contentType.toLowerCase(Locale.ROOT);
        if (!normalized.equals("image/jpeg") && !normalized.equals("image/png") && !normalized.equals("image/gif")) {
            throw new IllegalArgumentException("Only JPG, PNG, and GIF images are allowed");
        }
    }

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

    private void deleteStoredImageIfPresent(String storedImagePath) {
        if (storedImagePath == null || storedImagePath.isBlank()) {
            return;
        }
        try {
            catalogImageStorageService.deletePetImage(storedImagePath);
        } catch (IOException | IllegalArgumentException ignored) {
        }
    }

    private String normalizeStatus(String status) {
        return status == null ? "" : status.trim();
    }

    private boolean isAllowedPublicStatus(String status) {
        return status == null || "available".equalsIgnoreCase(status);
    }

    private boolean isAllowedCustomerStatus(String status) {
        return status == null || "available".equalsIgnoreCase(status) || "adopted".equalsIgnoreCase(status) || "owned".equalsIgnoreCase(status);
    }

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

    private PetResponse mapToResponse(Pet pet) {
        User owner = pet.getOwner();
        StoreLocation store = pet.getStore();
        return new PetResponse(
            pet.getPetId(),
            pet.getPetName(),
            pet.getPetSpecies(),
            pet.getPetBreed(),
            pet.getPetAge(),
            pet.getPetStatus(),
            pet.getPetPrice(),
            pet.getImageUrl() != null && !pet.getImageUrl().isBlank() ? "/api/v1/pets/" + pet.getPetId() + "/image" : null,
            pet.getCreatedAt(),
            pet.getUpdatedAt(),
            owner != null ? owner.getId() : null,
            owner != null ? owner.getFirstName() + " " + owner.getLastName() : null,
            store != null ? store.getStoreId() : null,
            store != null ? store.getStoreName() : null
        );
    }

    private MyPetResponse mapToMyPetResponse(Pet pet) {
        return new MyPetResponse(
                pet.getPetId(),
                pet.getPetName(),
                pet.getPetSpecies(),
                pet.getPetBreed(),
                pet.getPetAge(),
                pet.getImageUrl() != null && !pet.getImageUrl().isBlank() ? "/api/v1/pets/" + pet.getPetId() + "/image" : null,
                pet.getPetStatus()
        );
    }

    private void applyMyPetRequest(Pet pet, MyPetRequest request) {
        pet.setPetName(request.getPetName().trim());
        pet.setPetSpecies(request.getSpecies().trim());
        pet.setPetBreed(normalizeOptional(request.getBreed()));
        pet.setPetAge(request.getPetAge());
        pet.setPetPrice(null);
    }

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

    private void applyOwnerAndStore(Pet pet, PetRequest request) {
        if ("owned".equalsIgnoreCase(request.getPetStatus())) {
            if (request.getCustomerId() != null) {
                User owner = userRepository.findById(request.getCustomerId())
                        .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId()));
                pet.setOwner(owner);
            } else {
                pet.setOwner(null);
            }
            pet.setStore(null);
        } else if ("available".equalsIgnoreCase(request.getPetStatus()) || "unadopted".equalsIgnoreCase(request.getPetStatus())) {
            if (request.getStoreId() != null) {
                StoreLocation store = storeRepository.findById(request.getStoreId())
                        .orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + request.getStoreId()));
                pet.setStore(store);
            } else {
                pet.setStore(null);
            }
            pet.setOwner(null);
        } else {
            if (request.getCustomerId() != null) {
                User owner = userRepository.findById(request.getCustomerId())
                        .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId()));
                pet.setOwner(owner);
            } else {
                pet.setOwner(null);
            }
            if (request.getStoreId() != null) {
                StoreLocation store = storeRepository.findById(request.getStoreId())
                        .orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + request.getStoreId()));
                pet.setStore(store);
            } else {
                pet.setStore(null);
            }
        }
    }

    private boolean isOwnedByUser(Pet pet, Long userId) {
        if (!"owned".equalsIgnoreCase(normalizeStatus(pet.getPetStatus()))) {
            return false;
        }
        User owner = pet.getOwner();
        return owner != null && userId.equals(owner.getId());
    }

    public record ImagePayload(Resource resource, MediaType mediaType) {
    }

    private record CurrentViewer(Long userId, User.Role role) {
    }

    public static class ForbiddenImageAccessException extends RuntimeException {
    }
}
