PetService.java

package org.petify.shelter.service;

import org.petify.shelter.dto.PetImageResponse;
import org.petify.shelter.dto.PetRequest;
import org.petify.shelter.dto.PetResponse;
import org.petify.shelter.dto.PetResponseWithImages;
import org.petify.shelter.dto.SwipeResponse;
import org.petify.shelter.enums.PetType;
import org.petify.shelter.exception.PetNotFoundException;
import org.petify.shelter.exception.ShelterNotFoundException;
import org.petify.shelter.mapper.PetMapper;
import org.petify.shelter.model.Pet;
import org.petify.shelter.model.Shelter;
import org.petify.shelter.repository.FavoritePetRepository;
import org.petify.shelter.repository.PetRepository;
import org.petify.shelter.repository.ShelterRepository;
import org.petify.shelter.specification.PetSpecification;

import lombok.AllArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@AllArgsConstructor
@Service
public class PetService {
    private final PetRepository petRepository;
    private final ShelterRepository shelterRepository;
    private final FavoritePetRepository favoritePetRepository;
    private final PetMapper petMapper;
    private final StorageService storageService;

    public String getOwnerUsernameByPetId(Long petId) {
        Pet pet = petRepository.findById(petId)
                .orElseThrow(() -> new PetNotFoundException(petId));
        return pet.getShelter().getOwnerUsername();
    }

    public List<PetResponseWithImages> getAllPets() {
        return petRepository.findAll()
                .stream()
                .map(petMapper::toDtoWithImages)
                .collect(Collectors.toList());
    }

    public List<Long> getPetIdsByShelterId(Long shelterId) {
        return petRepository.findByShelterId(shelterId)
                .orElse(Collections.emptyList())
                .stream()
                .map(Pet::getId)
                .collect(Collectors.toList());
    }

    public Page<PetResponseWithImages> getPets(Pageable pageable) {
        return petRepository.findAll(pageable)
                .map(petMapper::toDtoWithImages);
    }

    @Transactional(readOnly = true)
    public SwipeResponse getFilteredPetsWithCursor(Boolean vaccinated, Boolean urgent, Boolean sterilized,
                                                   Boolean kidFriendly, Integer minAge, Integer maxAge,
                                                   PetType type, Double userLat, Double userLng, Double radiusKm,
                                                   Long cursor, int limit, String username) {

        List<Long> favoritePetIds = favoritePetRepository.findByUsername(username)
                .stream()
                .map(fp -> fp.getPet().getId())
                .toList();

        Specification<Pet> spec = buildPetSpecification(vaccinated, urgent, sterilized, kidFriendly, minAge, maxAge,
                type, favoritePetIds, cursor);

        Pageable pageable = PageRequest.of(0, limit);
        Page<Pet> page = petRepository.findAll(spec, pageable);

        List<PetResponseWithImages> results = filterAndMapPets(page.getContent(), userLat, userLng, radiusKm);

        Long nextCursor = results.isEmpty() ? null : page.getContent().getLast().getId();

        return new SwipeResponse(results, nextCursor);
    }

    private Specification<Pet> buildPetSpecification(Boolean vaccinated, Boolean urgent, Boolean sterilized,
                                                     Boolean kidFriendly, Integer minAge, Integer maxAge,
                                                     PetType type, List<Long> favoritePetIds, Long cursor) {
        Specification<Pet> spec = Specification.where(PetSpecification.hasVaccinated(vaccinated))
                .and(PetSpecification.isUrgent(urgent))
                .and(PetSpecification.isSterilized(sterilized))
                .and(PetSpecification.isKidFriendly(kidFriendly))
                .and(PetSpecification.ageBetween(minAge, maxAge))
                .and(PetSpecification.hasType(type))
                .and(PetSpecification.isNotArchived())
                .and(PetSpecification.hasActiveShelter())
                .and(PetSpecification.notInFavorites(favoritePetIds))
                .and(PetSpecification.randomOrder());

        if (cursor != null) {
            spec = spec.and((root, query, cb) -> cb.greaterThan(root.get("id"), cursor));
        }

        return spec;
    }

    private List<PetResponseWithImages> filterAndMapPets(List<Pet> pets, Double userLat, Double userLng, Double radiusKm) {
        Stream<Pet> stream = pets.stream();

        if (userLat != null && userLng != null && radiusKm != null) {
            stream = stream.filter(p -> {
                Shelter s = p.getShelter();
                if (s == null || s.getLatitude() == null || s.getLongitude() == null) {
                    return false;
                }
                return distance(userLat, userLng, s.getLatitude(), s.getLongitude()) <= radiusKm;
            });
        }

        return stream.map(pet -> {
            // Oblicz odległość dla każdego pet-a jeśli mamy współrzędne użytkownika
            Double calculatedDistance = null;
            if (userLat != null && userLng != null) {
                Shelter shelter = pet.getShelter();
                if (shelter != null && shelter.getLatitude() != null && shelter.getLongitude() != null) {
                    calculatedDistance = distance(userLat, userLng, shelter.getLatitude(), shelter.getLongitude());
                }
            }

            // Użyj nowej metody mapper-a która przyjmuje odległość
            return petMapper.toDtoWithImagesAndDistance(pet, calculatedDistance);
        }).toList();
    }

    private double distance(double lat1, double lon1, double lat2, double lon2) {
        final int R = 6371; // promień Ziemi w km
        double lat = Math.toRadians(lat2 - lat1);
        double lon = Math.toRadians(lon2 - lon1);
        double a = Math.sin(lat / 2) * Math.sin(lat / 2)
                + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
                * Math.sin(lon / 2) * Math.sin(lon / 2);
        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
        return R * c;
    }

    public PetResponseWithImages getPetById(Long petId) {
        return petRepository.findById(petId)
                .map(petMapper::toDtoWithImages)
                .orElseThrow(() -> new PetNotFoundException(petId));
    }

    public List<Long> getAllPetIds() {
        return petRepository.findAll()
                .stream()
                .map(Pet::getId)
                .collect(Collectors.toList());
    }

    public boolean isPetArchived(Long petId) {
        Pet pet = petRepository.findById(petId)
                .orElseThrow(() -> new PetNotFoundException(petId));
        return pet.isArchived();
    }

    public Page<PetResponseWithImages> getAllShelterPets(Long shelterId, Pageable pageable) {
        return petRepository.findByShelterId(shelterId, pageable)
                .map(petMapper::toDtoWithImages);
    }

    @Transactional
    public PetResponse createPet(PetRequest petRequest, Long shelterId, MultipartFile imageFile) throws IOException {
        Shelter shelter = shelterRepository.findById(shelterId)
                .orElseThrow(() -> new ShelterNotFoundException(shelterId));

        Pet pet = petMapper.toEntityWithShelter(petRequest, shelter);

        if (imageFile != null && !imageFile.isEmpty()) {
            String imageName = storageService.uploadImage(imageFile);
            pet.setImageName(imageName);
        }

        Pet savedPet = petRepository.save(pet);

        return petMapper.toDto(savedPet);
    }

    @Transactional
    public PetImageResponse getPetImage(Long id) {
        Optional<Pet> pet = petRepository.findById(id);
        if (pet.isPresent()) {
            return new PetImageResponse(pet.get().getImageName());
        } else {
            throw new PetNotFoundException(id);
        }
    }

    @Transactional
    public PetResponse updatePet(PetRequest petRequest, Long petId, Long shelterId, MultipartFile imageFile) throws IOException {
        Pet existingPet = petRepository.findById(petId)
                .orElseThrow(() -> new PetNotFoundException(petId));

        Shelter shelter = shelterRepository.findById(shelterId)
                .orElseThrow(() -> new ShelterNotFoundException(shelterId));

        existingPet.setName(petRequest.name());
        existingPet.setType(petRequest.type());
        existingPet.setBreed(petRequest.breed());
        existingPet.setAge(petRequest.age());
        existingPet.setDescription(petRequest.description());
        existingPet.setShelter(shelter);
        existingPet.setKidFriendly(petRequest.kidFriendly());
        existingPet.setSterilized(petRequest.sterilized());
        existingPet.setUrgent(petRequest.urgent());
        existingPet.setVaccinated(petRequest.vaccinated());
        existingPet.setGender(petRequest.gender());
        existingPet.setSize(petRequest.size());

        if (imageFile != null && !imageFile.isEmpty()) {
            String imageName = storageService.uploadImage(imageFile);
            existingPet.setImageName(imageName);
        }

        Pet updatedPet = petRepository.save(existingPet);

        return petMapper.toDto(updatedPet);
    }

    @Transactional
    public void deletePet(Long petId) {
        Pet pet = petRepository.findById(petId)
                .orElseThrow(() -> new PetNotFoundException(petId));

        petRepository.delete(pet);
    }

    @Transactional
    public PetResponse archivePet(Long petId) {
        Pet pet = petRepository.findById(petId)
                .orElseThrow(() -> new PetNotFoundException(petId));

        pet.setArchived(true);
        Pet savedPet = petRepository.save(pet);

        return petMapper.toDto(savedPet);
    }

    public HttpStatus validatePetForDonations(Long shelterId, Long petId) {
        try {
            Shelter shelter = shelterRepository.findById(shelterId)
                    .orElseThrow(() -> new ShelterNotFoundException(shelterId));

            if (!shelter.getIsActive()) {
                return HttpStatus.FORBIDDEN;
            }

            PetResponseWithImages pet = getPetById(petId);

            if (!pet.shelterId().equals(shelterId)) {
                return HttpStatus.NOT_FOUND;
            }

            if (pet.archived()) {
                return HttpStatus.GONE;
            }

            return HttpStatus.OK;

        } catch (ShelterNotFoundException | PetNotFoundException ex) {
            return HttpStatus.NOT_FOUND;
        }
    }
}