package de.oaa.xxx.dating; import de.oaa.xxx.social.SystemMessageService; import de.oaa.xxx.social.entity.MessageCause; import de.oaa.xxx.social.repository.BlockRepository; import de.oaa.xxx.subscription.SubscriptionLimitService; 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.user.UserService; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; import org.springframework.web.server.ResponseStatusException; import java.security.Principal; import java.time.LocalDateTime; import java.util.*; import java.util.stream.Collectors; @RestController @RequestMapping("/dating/dates") public class DatingDateController { private static final int MAX_DATES_STANDARD = 1; private static final int MAX_DATES_PRO = 5; private final DatingDateRepository dateRepository; private final DatingDateInterestRepository interestRepository; private final DatingService datingService; private final UserService userService; private final UserRepository userRepository; private final BlockRepository blockRepository; private final SubscriptionLimitService subscriptionLimitService; private final SystemMessageService systemMessageService; public DatingDateController(DatingDateRepository dateRepository, DatingDateInterestRepository interestRepository, DatingService datingService, UserService userService, UserRepository userRepository, BlockRepository blockRepository, SubscriptionLimitService subscriptionLimitService, SystemMessageService systemMessageService) { this.dateRepository = dateRepository; this.interestRepository = interestRepository; this.datingService = datingService; this.userService = userService; this.userRepository = userRepository; this.blockRepository = blockRepository; this.subscriptionLimitService = subscriptionLimitService; this.systemMessageService = systemMessageService; } // ── DTOs ────────────────────────────────────────────────────────────────── record DatingDateDto( UUID dateId, UUID creatorId, String creatorName, String creatorProfilePicture, String title, String description, String imageData, LocalDateTime scheduledAt, LocalDateTime createdAt, int interestCount, boolean myInterest ) {} record DatesResult(List mine, List available) {} record CreateDateRequest(String title, String description, String imageData, LocalDateTime scheduledAt) {} record InterestResult(boolean myInterest, int interestCount) {} record DateInterestDto(UUID userId, String name, String profilePicture, LocalDateTime interestedAt, boolean blocked) {} // ── Dates abrufen ───────────────────────────────────────────────────────── /** * Gibt eigene Dates (ungefiltert) + verfügbare Dates (gefiltert nach Suchkriterien) zurück. * Erfordert Dating aktiv + Standort (gleiche Voraussetzung wie Entdecken-Tab). */ @GetMapping public ResponseEntity getDates( @RequestParam(name = "maxDistanceKm", defaultValue = "50") int maxDistanceKm, @RequestParam(name = "minAge", defaultValue = "18") int minAge, @RequestParam(name = "maxAge", defaultValue = "99") int maxAge, @RequestParam(name = "geschlechter", required = false) List geschlechter, @RequestParam(name = "neigungen", required = false) List neigungen, @RequestParam(name = "vorliebenIds", required = false) List vorliebenIds, @RequestParam(name = "vorliebenUnd", defaultValue = "false") boolean vorliebenUnd, Principal principal) { UserEntity me = requireDatingUser(principal); UUID myId = me.getUserId(); LocalDateTime now = LocalDateTime.now(); // Meine eigenen Dates (aktive) List myDates = dateRepository.findByCreatorIdOrderByCreatedAtDesc(myId).stream() .filter(d -> d.getExpiresAt().isAfter(now)) .toList(); // Alle fremden, noch nicht abgelaufenen Dates List candidates = dateRepository.findAvailableExcluding(myId, now); // Ersteller-IDs filtern (gleiche Kriterien wie Entdecken-Tab) Set creatorIds = candidates.stream().map(DatingDateEntity::getCreatorId).collect(Collectors.toSet()); DatingService.DatingFilter filter = new DatingService.DatingFilter( Math.max(maxDistanceKm, 1), Math.max(minAge, 0), Math.min(maxAge, 120), parseEnumList(geschlechter, Geschlecht.class), parseEnumList(neigungen, Neigung.class), vorliebenIds != null ? vorliebenIds : List.of(), vorliebenUnd ); Set matchingCreators = datingService.filterUserIds(creatorIds, filter, me.getDatingLat(), me.getDatingLon()); List available = candidates.stream() .filter(d -> matchingCreators.contains(d.getCreatorId())) .toList(); // Creator-User-Daten laden Set allCreatorIds = new HashSet<>(creatorIds); myDates.forEach(d -> allCreatorIds.add(d.getCreatorId())); Map creators = userRepository.findAllById(allCreatorIds).stream() .collect(Collectors.toMap(UserEntity::getUserId, u -> u)); // Alle relevanten Date-IDs Set allDateIds = new HashSet<>(); myDates.forEach(d -> allDateIds.add(d.getDateId())); available.forEach(d -> allDateIds.add(d.getDateId())); // Meine Interessen und Interest-Counts Set myInterests = new HashSet<>(); Map interestCounts = new HashMap<>(); for (UUID dateId : allDateIds) { if (interestRepository.existsByDateIdAndUserId(dateId, myId)) myInterests.add(dateId); interestCounts.put(dateId, interestRepository.countByDateId(dateId)); } List mineDtos = myDates.stream() .map(d -> toDto(d, creators, myInterests, interestCounts)) .toList(); List availableDtos = available.stream() .map(d -> toDto(d, creators, myInterests, interestCounts)) .toList(); return ResponseEntity.ok(new DatesResult(mineDtos, availableDtos)); } // ── Dates eines bestimmten Users abrufen (Profilansicht) ───────────────── /** * Gibt die aktiven Dates eines bestimmten Users zurück. * Nur abrufbar, wenn der anfragende User selbst Dating aktiviert hat. */ @GetMapping("/by-user/{userId}") public ResponseEntity> getDatesByUser( @PathVariable("userId") UUID userId, Principal principal) { UserEntity me = requireDatingUser(principal); LocalDateTime now = LocalDateTime.now(); List dates = dateRepository.findByCreatorIdOrderByCreatedAtDesc(userId).stream() .filter(d -> d.getExpiresAt().isAfter(now)) .toList(); if (dates.isEmpty()) return ResponseEntity.ok(List.of()); UserEntity creator = userRepository.findById(userId).orElse(null); Map creators = creator != null ? Map.of(userId, creator) : Map.of(); Set myInterests = new HashSet<>(); Map interestCounts = new HashMap<>(); for (DatingDateEntity d : dates) { if (interestRepository.existsByDateIdAndUserId(d.getDateId(), me.getUserId())) myInterests.add(d.getDateId()); interestCounts.put(d.getDateId(), interestRepository.countByDateId(d.getDateId())); } return ResponseEntity.ok(dates.stream() .map(d -> toDto(d, creators, myInterests, interestCounts)) .toList()); } // ── Date erstellen ──────────────────────────────────────────────────────── @PostMapping public ResponseEntity createDate(@RequestBody CreateDateRequest req, Principal principal) { UserEntity me = requireDatingUser(principal); if (req.title() == null || req.title().isBlank()) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Titel fehlt"); if (req.description() == null || req.description().isBlank()) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Beschreibung fehlt"); if (req.title().length() > 200) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Titel zu lang"); boolean pro = subscriptionLimitService.hasActivePaidSubscription(me.getUserId()); int maxDates = pro ? MAX_DATES_PRO : MAX_DATES_STANDARD; long existing = dateRepository.countByCreatorId(me.getUserId()); if (existing >= maxDates) { throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, pro ? "Maximal 5 Dates erlaubt (Pro)" : "Als Standardmitglied ist nur 1 Date erlaubt"); } LocalDateTime now = LocalDateTime.now(); LocalDateTime expiresAt = req.scheduledAt() != null ? req.scheduledAt() : now.plusDays(30); DatingDateEntity entity = new DatingDateEntity(); entity.setDateId(UUID.randomUUID()); entity.setCreatorId(me.getUserId()); entity.setTitle(req.title().strip()); entity.setDescription(req.description().strip()); entity.setImageData(req.imageData()); entity.setScheduledAt(req.scheduledAt()); entity.setCreatedAt(now); entity.setExpiresAt(expiresAt); dateRepository.save(entity); Map creators = Map.of(me.getUserId(), me); return ResponseEntity.status(HttpStatus.CREATED) .body(toDto(entity, creators, Set.of(), Map.of(entity.getDateId(), 0L))); } // ── Date bearbeiten ─────────────────────────────────────────────────────── @PutMapping("/{dateId}") public ResponseEntity updateDate(@PathVariable("dateId") UUID dateId, @RequestBody CreateDateRequest req, Principal principal) { UserEntity me = requireDatingUser(principal); DatingDateEntity entity = requireMyDate(dateId, me.getUserId()); if (req.title() == null || req.title().isBlank()) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Titel fehlt"); if (req.description() == null || req.description().isBlank()) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Beschreibung fehlt"); if (req.title().length() > 200) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Titel zu lang"); entity.setTitle(req.title().strip()); entity.setDescription(req.description().strip()); if (req.imageData() != null) entity.setImageData(req.imageData().isBlank() ? null : req.imageData()); entity.setScheduledAt(req.scheduledAt()); entity.setExpiresAt(req.scheduledAt() != null ? req.scheduledAt() : entity.getCreatedAt().plusDays(30)); dateRepository.save(entity); long count = interestRepository.countByDateId(dateId); boolean myInterest = interestRepository.existsByDateIdAndUserId(dateId, me.getUserId()); Map creators = Map.of(me.getUserId(), me); return ResponseEntity.ok(toDto(entity, creators, myInterest ? Set.of(dateId) : Set.of(), Map.of(dateId, count))); } // ── Date löschen ────────────────────────────────────────────────────────── @DeleteMapping("/{dateId}") @Transactional public ResponseEntity deleteDate(@PathVariable("dateId") UUID dateId, Principal principal) { UserEntity me = requireDatingUser(principal); requireMyDate(dateId, me.getUserId()); interestRepository.deleteByDateId(dateId); dateRepository.deleteById(dateId); return ResponseEntity.noContent().build(); } // ── Interesse bekunden / zurückziehen ────────────────────────────────────── @PostMapping("/{dateId}/interest") @Transactional public ResponseEntity toggleInterest(@PathVariable("dateId") UUID dateId, Principal principal) { UserEntity me = userService.requireUser(principal); DatingDateEntity date = dateRepository.findById(dateId) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Date nicht gefunden")); if (date.getCreatorId().equals(me.getUserId())) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Eigenes Date"); if (date.getExpiresAt().isBefore(LocalDateTime.now())) throw new ResponseStatusException(HttpStatus.GONE, "Date abgelaufen"); Optional existing = interestRepository.findByDateIdAndUserId(dateId, me.getUserId()); boolean myInterest; if (existing.isPresent()) { interestRepository.delete(existing.get()); myInterest = false; } else { DatingDateInterestEntity interest = new DatingDateInterestEntity(); interest.setInterestId(UUID.randomUUID()); interest.setDateId(dateId); interest.setUserId(me.getUserId()); interest.setInterestedAt(LocalDateTime.now()); interestRepository.save(interest); myInterest = true; // Benachrichtigung an Ersteller String text = me.getName() + " hat Interesse an deinem Date \"" + date.getTitle() + "\" bekundet."; systemMessageService.send(me.getUserId(), date.getCreatorId(), text, "/dating.html?tab=dates", MessageCause.DATE_INTEREST); } long count = interestRepository.countByDateId(dateId); return ResponseEntity.ok(new InterestResult(myInterest, (int) count)); } // ── Interessenten abrufen (nur Ersteller) ───────────────────────────────── @GetMapping("/{dateId}/interests") public ResponseEntity> getInterests(@PathVariable("dateId") UUID dateId, Principal principal) { UserEntity me = userService.requireUser(principal); DatingDateEntity date = dateRepository.findById(dateId) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Date nicht gefunden")); if (!date.getCreatorId().equals(me.getUserId())) throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Kein Zugriff"); List interests = interestRepository.findByDateIdOrderByInterestedAtDesc(dateId); Set userIds = interests.stream().map(DatingDateInterestEntity::getUserId).collect(Collectors.toSet()); Map users = userRepository.findAllById(userIds).stream() .collect(Collectors.toMap(UserEntity::getUserId, u -> u)); List dtos = interests.stream() .map(i -> { UserEntity u = users.get(i.getUserId()); if (u == null) return null; boolean blocked = blockRepository.existsBlock(me.getUserId(), u.getUserId()); return new DateInterestDto(u.getUserId(), u.getName(), u.getProfilePicture(), i.getInterestedAt(), blocked); }) .filter(Objects::nonNull) .toList(); return ResponseEntity.ok(dtos); } // ── Helpers ─────────────────────────────────────────────────────────────── private UserEntity requireDatingUser(Principal principal) { UserEntity me = userService.requireUser(principal); if (!me.isDatingAktiv()) throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Dating nicht aktiviert"); if (me.getDatingLat() == null || me.getDatingLon() == null) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Standort nicht gesetzt"); return me; } private DatingDateEntity requireMyDate(UUID dateId, UUID userId) { DatingDateEntity entity = dateRepository.findById(dateId) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Date nicht gefunden")); if (!entity.getCreatorId().equals(userId)) throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Kein Zugriff"); return entity; } private DatingDateDto toDto(DatingDateEntity d, Map creators, Set myInterests, Map interestCounts) { UserEntity creator = creators.get(d.getCreatorId()); return new DatingDateDto( d.getDateId(), d.getCreatorId(), creator != null ? creator.getName() : null, creator != null ? creator.getProfilePicture() : null, d.getTitle(), d.getDescription(), d.getImageData(), d.getScheduledAt(), d.getCreatedAt(), interestCounts.getOrDefault(d.getDateId(), 0L).intValue(), myInterests.contains(d.getDateId()) ); } private > List parseEnumList(List values, Class enumClass) { if (values == null || values.isEmpty()) return List.of(); return values.stream() .map(s -> { try { return Enum.valueOf(enumClass, s); } catch (IllegalArgumentException e) { return null; } }) .filter(Objects::nonNull) .toList(); } }