package de.oaa.xxx.dating;

import de.oaa.xxx.user.Geschlecht;
import de.oaa.xxx.user.Neigung;
import de.oaa.xxx.user.UserEntity;
import de.oaa.xxx.user.UserRepository;
import de.oaa.xxx.vorlieben.UserVorliebeEntity;
import de.oaa.xxx.vorlieben.UserVorliebeRepository;
import de.oaa.xxx.vorlieben.VorliebeBewertung;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

@Service
public class DatingService {

    private static final Set<VorliebeBewertung> POSITIVE_RATINGS = Set.of(
            VorliebeBewertung.MAG_ICH,
            VorliebeBewertung.UNBEDINGT,
            VorliebeBewertung.WILL_AUSPROBIEREN
    );

    private final UserRepository userRepository;
    private final UserVorliebeRepository userVorliebeRepository;
    private final DatingLikeRepository likeRepository;
    private final DatingMatchRepository matchRepository;
    private final DatingPassRepository passRepository;

    public DatingService(UserRepository userRepository,
                         UserVorliebeRepository userVorliebeRepository,
                         DatingLikeRepository likeRepository,
                         DatingMatchRepository matchRepository,
                         DatingPassRepository passRepository) {
        this.userRepository = userRepository;
        this.userVorliebeRepository = userVorliebeRepository;
        this.likeRepository = likeRepository;
        this.matchRepository = matchRepository;
        this.passRepository = passRepository;
    }

    // ── Records ──────────────────────────────────────────────────────────────

    record DatingFilter(
            int maxDistanceKm,
            int minAge,
            int maxAge,
            List<Geschlecht> geschlechter,
            List<Neigung> neigungen,
            List<UUID> vorliebenIds,
            boolean vorliebenUnd
    ) {}

    record DatingProfileDto(
            UUID userId,
            String name,
            Integer alter,
            double distanzKm,
            String profilePicture,
            String profilePictureHq,
            String neigung,
            String geschlecht,
            String datingStadt,
            String beschreibung,
            boolean likedByMe,
            List<UUID> matchingVorliebenIds
    ) {}

    record LikeResult(boolean liked, boolean newMatch) {}

    record LikerDto(UUID userId, String name, String profilePicture, LocalDateTime likedAt) {}

    record WhoLikesMeResult(boolean premium, int total, List<LikerDto> likers) {}

    record MatchDto(UUID userId, String name, String profilePicture, LocalDateTime matchedAt) {}

    record IdsResult(List<UUID> ids, int total) {}

    // ── Public API ────────────────────────────────────────────────────────────

    /**
     * Liefert alle passenden User-IDs, nach Entfernung sortiert.
     * Kein Paging – die Liste wird komplett zurückgegeben, das Frontend
     * holt Profildetails nachgelagert in Batches.
     */
    public IdsResult findSortedIds(UUID currentUserId, DatingFilter filter,
                                   double myLat, double myLon) {
        List<UserEntity> candidates = userRepository.findByDatingAktiv(true).stream()
                .filter(u -> !u.getUserId().equals(currentUserId))
                .filter(u -> u.getDatingLat() != null && u.getDatingLon() != null)
                .toList();

        candidates = filterByDistance(candidates, myLat, myLon, filter.maxDistanceKm());
        candidates = filterByAge(candidates, filter.minAge(), filter.maxAge());

        if (!filter.geschlechter().isEmpty()) {
            candidates = candidates.stream()
                    .filter(u -> u.getGeschlecht() != null && filter.geschlechter().contains(u.getGeschlecht()))
                    .toList();
        }
        if (!filter.neigungen().isEmpty()) {
            candidates = candidates.stream()
                    .filter(u -> u.getNeigung() != null && filter.neigungen().contains(u.getNeigung()))
                    .toList();
        }
        if (!filter.vorliebenIds().isEmpty()) {
            candidates = filterByVorlieben(candidates, filter.vorliebenIds(), filter.vorliebenUnd());
        }

        List<UUID> sorted = candidates.stream()
                .sorted(Comparator.comparingDouble(u ->
                        haversineKm(myLat, myLon, u.getDatingLat(), u.getDatingLon())))
                .map(UserEntity::getUserId)
                .toList();

        return new IdsResult(sorted, sorted.size());
    }

    /**
     * Lädt Profildetails für die übergebenen IDs und behält deren Reihenfolge bei.
     */
    public List<DatingProfileDto> getProfilesByIds(List<UUID> ids, UUID currentUserId, double myLat, double myLon) {
        if (ids == null || ids.isEmpty()) return List.of();

        Map<UUID, UserEntity> byId = userRepository.findAllById(ids).stream()
                .collect(Collectors.toMap(UserEntity::getUserId, Function.identity()));

        Set<UUID> likedByMe = likeRepository.findByLikerId(currentUserId).stream()
                .map(DatingLikeEntity::getLikedId)
                .collect(Collectors.toSet());

        // Meine positiven Vorlieben
        Set<UUID> myPositive = userVorliebeRepository.findByUserId(currentUserId).stream()
                .filter(uv -> POSITIVE_RATINGS.contains(uv.getBewertung()))
                .map(UserVorliebeEntity::getItemId)
                .collect(Collectors.toSet());

        // Positive Vorlieben aller Kandidaten (Batch-Query)
        Set<UUID> candidateIds = ids.stream().filter(byId::containsKey).collect(Collectors.toSet());
        Map<UUID, Set<UUID>> candidatePositive = myPositive.isEmpty()
                ? Map.of()
                : userVorliebeRepository.findByUserIdIn(candidateIds).stream()
                        .filter(uv -> POSITIVE_RATINGS.contains(uv.getBewertung()))
                        .collect(Collectors.groupingBy(
                                UserVorliebeEntity::getUserId,
                                Collectors.mapping(UserVorliebeEntity::getItemId, Collectors.toSet())
                        ));

        return ids.stream()
                .map(byId::get)
                .filter(Objects::nonNull)
                .map(u -> {
                    Set<UUID> theirPositive = candidatePositive.getOrDefault(u.getUserId(), Set.of());
                    List<UUID> matching = myPositive.stream()
                            .filter(theirPositive::contains)
                            .limit(5)
                            .toList();
                    return toDto(u, currentUserId, likedByMe, myLat, myLon, matching);
                })
                .toList();
    }

    // ── Like / Match ──────────────────────────────────────────────────────────

    @Transactional
    public LikeResult toggleLike(UUID likerId, UUID likedId) {
        if (likeRepository.existsByLikerIdAndLikedId(likerId, likedId)) {
            likeRepository.deleteByLikerIdAndLikedId(likerId, likedId);
            matchRepository.deleteByUsers(likerId, likedId);
            return new LikeResult(false, false);
        }

        DatingLikeEntity like = new DatingLikeEntity();
        like.setLikeId(UUID.randomUUID());
        like.setLikerId(likerId);
        like.setLikedId(likedId);
        like.setLikedAt(LocalDateTime.now());
        likeRepository.save(like);

        boolean mutual = likeRepository.existsByLikerIdAndLikedId(likedId, likerId);
        boolean newMatch = false;
        if (mutual && !matchRepository.existsByUsers(likerId, likedId)) {
            DatingMatchEntity match = new DatingMatchEntity();
            match.setMatchId(UUID.randomUUID());
            match.setUser1Id(likerId);
            match.setUser2Id(likedId);
            match.setMatchedAt(LocalDateTime.now());
            matchRepository.save(match);
            newMatch = true;
        }
        return new LikeResult(true, newMatch);
    }

    public WhoLikesMeResult whoLikesMe(UUID userId, boolean isPremium) {
        List<DatingLikeEntity> likes = likeRepository.findByLikedId(userId);
        int total = likes.size();

        if (total == 0) return new WhoLikesMeResult(isPremium, 0, List.of());

        Map<UUID, UserEntity> byId = userRepository.findAllById(
                likes.stream().map(DatingLikeEntity::getLikerId).toList()).stream()
                .collect(Collectors.toMap(UserEntity::getUserId, Function.identity()));

        List<LikerDto> likers = likes.stream()
                .sorted(Comparator.comparing(DatingLikeEntity::getLikedAt).reversed())
                .map(l -> {
                    UserEntity u = byId.get(l.getLikerId());
                    if (u == null) return null;
                    return new LikerDto(
                            isPremium ? u.getUserId() : null,
                            isPremium ? u.getName()   : null,
                            u.getProfilePicture(),
                            l.getLikedAt());
                })
                .filter(Objects::nonNull)
                .toList();

        return new WhoLikesMeResult(isPremium, total, likers);
    }

    public List<MatchDto> getMatches(UUID userId) {
        List<DatingMatchEntity> matches = matchRepository.findByUser(userId);
        // PartnerId ermitteln
        Map<UUID, UserEntity> byId = userRepository.findAllById(
                matches.stream()
                        .map(m -> m.getUser1Id().equals(userId) ? m.getUser2Id() : m.getUser1Id())
                        .toList()).stream()
                .collect(Collectors.toMap(UserEntity::getUserId, Function.identity()));

        return matches.stream()
                .sorted(Comparator.comparing(DatingMatchEntity::getMatchedAt).reversed())
                .map(m -> {
                    UUID partnerId = m.getUser1Id().equals(userId) ? m.getUser2Id() : m.getUser1Id();
                    UserEntity u = byId.get(partnerId);
                    if (u == null) return null;
                    return new MatchDto(u.getUserId(), u.getName(), u.getProfilePicture(), m.getMatchedAt());
                })
                .filter(Objects::nonNull)
                .toList();
    }

    public boolean isMatch(UUID userId, UUID otherId) {
        return matchRepository.existsByUsers(userId, otherId);
    }

    public void unmatch(UUID userId, UUID otherId) {
        matchRepository.deleteByUsers(userId, otherId);
        // Gegenseitige Likes entfernen, damit kein Re-Match beim nächsten Like entsteht
        likeRepository.deleteByLikerIdAndLikedId(userId, otherId);
        likeRepository.deleteByLikerIdAndLikedId(otherId, userId);
    }

    public List<UUID> getLikedByMe(UUID userId) {
        return likeRepository.findByLikerId(userId).stream()
                .map(DatingLikeEntity::getLikedId)
                .toList();
    }

    /**
     * Liefert zufällig sortierte IDs für den Discovery-Modus (Match-Tab).
     * Bedingung: gegenseitige Geschlechter-Kompatibilität, Distanz, noch nicht geliked.
     */
    public List<UUID> findDiscoveryIds(UserEntity me) {
        Set<String> myGeschlechter = me.getDatingGeschlechter() != null && !me.getDatingGeschlechter().isBlank()
                ? Set.of(me.getDatingGeschlechter().split(","))
                : Set.of();

        List<UserEntity> candidates = userRepository.findByDatingAktiv(true).stream()
                .filter(u -> !u.getUserId().equals(me.getUserId()))
                .filter(u -> u.getDatingLat() != null && u.getDatingLon() != null)
                .toList();

        // Ich muss an deren Geschlecht interessiert sein
        if (!myGeschlechter.isEmpty()) {
            candidates = candidates.stream()
                    .filter(u -> u.getGeschlecht() != null
                            && myGeschlechter.contains(u.getGeschlecht().name()))
                    .toList();
        }

        // Sie müssen an meinem Geschlecht interessiert sein (oder keine Einschränkung haben)
        if (me.getGeschlecht() != null) {
            final String myGender = me.getGeschlecht().name();
            candidates = candidates.stream()
                    .filter(u -> {
                        if (u.getDatingGeschlechter() == null || u.getDatingGeschlechter().isBlank()) return true;
                        return Set.of(u.getDatingGeschlechter().split(",")).contains(myGender);
                    })
                    .toList();
        }

        // Distanz: mein gespeicherter Max-Wert, Fallback 50 km
        int maxDist = me.getDatingMaxDistanzKm() != null ? me.getDatingMaxDistanzKm() : 50;
        candidates = filterByDistance(candidates, me.getDatingLat(), me.getDatingLon(), maxDist);

        // Bereits gelikte und abgelehnte Profile ausschließen
        Set<UUID> alreadyLiked = likeRepository.findByLikerId(me.getUserId()).stream()
                .map(DatingLikeEntity::getLikedId)
                .collect(Collectors.toSet());
        Set<UUID> alreadyPassed = passRepository.findPassedIdsByPasserId(me.getUserId());
        candidates = candidates.stream()
                .filter(u -> !alreadyLiked.contains(u.getUserId()) && !alreadyPassed.contains(u.getUserId()))
                .toList();

        List<UUID> ids = new ArrayList<>(candidates.stream().map(UserEntity::getUserId).toList());
        Collections.shuffle(ids);
        return ids;
    }

    // ── Filter-Utility für andere Services im Package ─────────────────────────

    /**
     * Filtert eine Menge von User-IDs nach dem übergebenen DatingFilter.
     * Verwendet dieselbe Logik wie findSortedIds().
     * Voraussetzung: Die Kandidaten müssen bereits datingAktiv=true + Koordinaten haben.
     */
    Set<UUID> filterUserIds(Collection<UUID> candidateIds, DatingFilter filter,
                            double myLat, double myLon) {
        List<UserEntity> candidates = userRepository.findAllById(candidateIds).stream()
                .filter(u -> u.getDatingLat() != null && u.getDatingLon() != null)
                .toList();

        candidates = filterByDistance(candidates, myLat, myLon, filter.maxDistanceKm());
        candidates = filterByAge(candidates, filter.minAge(), filter.maxAge());

        if (!filter.geschlechter().isEmpty()) {
            candidates = candidates.stream()
                    .filter(u -> u.getGeschlecht() != null && filter.geschlechter().contains(u.getGeschlecht()))
                    .toList();
        }
        if (!filter.neigungen().isEmpty()) {
            candidates = candidates.stream()
                    .filter(u -> u.getNeigung() != null && filter.neigungen().contains(u.getNeigung()))
                    .toList();
        }
        if (!filter.vorliebenIds().isEmpty()) {
            candidates = filterByVorlieben(candidates, filter.vorliebenIds(), filter.vorliebenUnd());
        }

        return candidates.stream().map(UserEntity::getUserId).collect(Collectors.toSet());
    }

    // ── Private helpers ───────────────────────────────────────────────────────

    private List<UserEntity> filterByDistance(List<UserEntity> users, double myLat, double myLon, int maxKm) {
        return users.stream()
                .filter(u -> haversineKm(myLat, myLon, u.getDatingLat(), u.getDatingLon()) <= maxKm)
                .toList();
    }

    private List<UserEntity> filterByAge(List<UserEntity> users, int minAge, int maxAge) {
        return users.stream()
                .filter(u -> {
                    Integer age = u.getAlter();
                    if (age == null) return false;
                    return age >= minAge && age <= maxAge;
                })
                .toList();
    }

    private List<UserEntity> filterByVorlieben(List<UserEntity> candidates,
                                                List<UUID> requiredItemIds,
                                                boolean andLogic) {
        Set<UUID> candidateIds = candidates.stream()
                .map(UserEntity::getUserId)
                .collect(Collectors.toSet());

        Map<UUID, Set<UUID>> positiveByUser = userVorliebeRepository.findByUserIdIn(candidateIds).stream()
                .filter(uv -> POSITIVE_RATINGS.contains(uv.getBewertung()))
                .collect(Collectors.groupingBy(
                        UserVorliebeEntity::getUserId,
                        Collectors.mapping(UserVorliebeEntity::getItemId, Collectors.toSet())
                ));

        Set<UUID> required = new HashSet<>(requiredItemIds);
        return candidates.stream()
                .filter(u -> {
                    Set<UUID> userItems = positiveByUser.getOrDefault(u.getUserId(), Set.of());
                    return andLogic
                            ? userItems.containsAll(required)
                            : required.stream().anyMatch(userItems::contains);
                })
                .toList();
    }

    private DatingProfileDto toDto(UserEntity u, UUID currentUserId, Set<UUID> likedByMe,
                                   double myLat, double myLon, List<UUID> matchingVorliebenIds) {
        double dist = Math.round(haversineKm(myLat, myLon, u.getDatingLat(), u.getDatingLon()) * 10.0) / 10.0;
        return new DatingProfileDto(
                u.getUserId(),
                u.getName(),
                u.getAlter(),
                dist,
                u.getProfilePicture(),
                u.getProfilePictureHq(),
                u.getNeigung() != null ? u.getNeigung().getLabel() : null,
                u.getGeschlecht() != null ? u.getGeschlecht().getLabel() : null,
                u.getDatingStadt(),
                u.getBeschreibung(),
                likedByMe.contains(u.getUserId()),
                matchingVorliebenIds
        );
    }

    /**
     * Haversine-Formel: Luftlinie zwischen zwei Koordinaten in Kilometern.
     */
    static double haversineKm(double lat1, double lon1, double lat2, double lon2) {
        final double R = 6371.0;
        double dLat = Math.toRadians(lat2 - lat1);
        double dLon = Math.toRadians(lon2 - lon1);
        double a = Math.sin(dLat / 2) * Math.sin(dLat / 2)
                + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
                * Math.sin(dLon / 2) * Math.sin(dLon / 2);
        return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
    }
}
