package de.oaa.xxx.location;

import java.security.Principal;
import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import de.oaa.xxx.location.entity.LocationEventAttendeeEntity;
import de.oaa.xxx.location.entity.LocationEventEntity;
import de.oaa.xxx.location.entity.LocationFollowEntity;
import de.oaa.xxx.location.repository.LocationEventAttendeeRepository;
import de.oaa.xxx.location.repository.LocationEventRepository;
import de.oaa.xxx.location.repository.LocationFollowRepository;
import de.oaa.xxx.location.repository.LocationRepository;
import de.oaa.xxx.user.UserEntity;
import de.oaa.xxx.user.UserRepository;
import de.oaa.xxx.user.UserService;
import jakarta.transaction.Transactional;

@RestController
public class LocationEventController {

    private static final Logger LOGGER = LoggerFactory.getLogger(LocationEventController.class);
    private static final int MAX_BATCH_SIZE = 50;

    private final LocationRepository            locationRepo;
    private final LocationEventRepository       eventRepo;
    private final LocationEventAttendeeRepository attendeeRepo;
    private final LocationFollowRepository      followRepo;
    private final UserRepository                userRepo;
    private final UserService                   userService;

    public LocationEventController(LocationRepository locationRepo,
                                   LocationEventRepository eventRepo,
                                   LocationEventAttendeeRepository attendeeRepo,
                                   LocationFollowRepository followRepo,
                                   UserRepository userRepo,
                                   UserService userService) {
        this.locationRepo  = locationRepo;
        this.eventRepo     = eventRepo;
        this.attendeeRepo  = attendeeRepo;
        this.followRepo    = followRepo;
        this.userRepo      = userRepo;
        this.userService   = userService;
    }

    // ── DTOs ─────────────────────────────────────────────────────────────────

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

    record EventPreviewDto(
            UUID eventId,
            UUID locationId,
            String locationName,
            String title,
            String imageData,
            LocalDateTime startAt,
            double distanzKm,
            long attendeeCount,
            boolean attendingMe
    ) {}

    record AttendeeDto(UUID userId, String name, String profilePictureLq, String geschlecht) {}

    record EventDetailDto(
            UUID eventId,
            UUID locationId,
            String locationName,
            String title,
            String description,
            String imageData,
            LocalDateTime startAt,
            LocalDateTime createdAt,
            boolean attendingMe,
            List<AttendeeDto> attendees
    ) {}

    record CreateEventRequest(String title, String description, String imageData, LocalDateTime startAt) {}

    record UpdateEventRequest(String title, String description, String imageData, LocalDateTime startAt) {}

    record BatchRequest(List<UUID> ids, Double lat, Double lon) {}

    // ── Events einer Location ─────────────────────────────────────────────────

    @GetMapping("/locations/{locationId}/events")
    public ResponseEntity<List<EventPreviewDto>> getEventsForLocation(
            @PathVariable UUID locationId,
            Principal principal) {

        UUID myId = userService.requireUser(principal).getUserId();
        if (!locationRepo.existsById(locationId)) return ResponseEntity.notFound().build();

        var location = locationRepo.findById(locationId).orElseThrow();
        double refLat = location.getLat() != null ? location.getLat() : 0;
        double refLon = location.getLon() != null ? location.getLon() : 0;

        List<EventPreviewDto> dtos = eventRepo.findByLocationIdOrderByStartAtAsc(locationId).stream()
                .map(e -> toPreview(e, location.getName(), refLat, refLon, myId))
                .toList();
        return ResponseEntity.ok(dtos);
    }

    @PostMapping("/locations/{locationId}/events")
    public ResponseEntity<EventDetailDto> createEvent(
            @PathVariable UUID locationId,
            @RequestBody CreateEventRequest req,
            Principal principal) {

        UUID myId = userService.requireUser(principal).getUserId();
        var locOpt = locationRepo.findById(locationId);
        if (locOpt.isEmpty()) return ResponseEntity.notFound().build();
        if (!locOpt.get().getOwnerId().equals(myId)) return ResponseEntity.status(403).build();
        if (req.title() == null || req.title().isBlank()) return ResponseEntity.badRequest().build();
        if (req.startAt() == null) return ResponseEntity.badRequest().build();

        LocationEventEntity event = new LocationEventEntity();
        event.setEventId(UUID.randomUUID());
        event.setLocationId(locationId);
        event.setTitle(req.title().trim());
        event.setDescription(req.description() != null ? req.description().trim() : null);
        event.setImageData(req.imageData());
        event.setStartAt(req.startAt());
        event.setCreatedAt(LocalDateTime.now());
        eventRepo.save(event);

        LOGGER.info("Location {} hat Event {} angelegt", locationId, event.getEventId());
        return ResponseEntity.status(201).body(toDetail(event, locOpt.get().getName(), myId));
    }

    @PutMapping("/locations/{locationId}/events/{eventId}")
    public ResponseEntity<EventDetailDto> updateEvent(
            @PathVariable UUID locationId,
            @PathVariable UUID eventId,
            @RequestBody UpdateEventRequest req,
            Principal principal) {

        UUID myId = userService.requireUser(principal).getUserId();
        var locOpt = locationRepo.findById(locationId);
        if (locOpt.isEmpty()) return ResponseEntity.notFound().build();
        if (!locOpt.get().getOwnerId().equals(myId)) return ResponseEntity.status(403).build();

        var evtOpt = eventRepo.findById(eventId);
        if (evtOpt.isEmpty()) return ResponseEntity.notFound().build();
        if (!evtOpt.get().getLocationId().equals(locationId)) return ResponseEntity.status(400).build();

        LocationEventEntity event = evtOpt.get();
        if (req.title() != null && !req.title().isBlank()) event.setTitle(req.title().trim());
        if (req.description() != null) event.setDescription(req.description().trim());
        if (req.imageData() != null) event.setImageData(req.imageData());
        if (req.startAt() != null) event.setStartAt(req.startAt());
        eventRepo.save(event);

        return ResponseEntity.ok(toDetail(event, locOpt.get().getName(), myId));
    }

    @Transactional
    @DeleteMapping("/locations/{locationId}/events/{eventId}")
    public ResponseEntity<Void> deleteEvent(
            @PathVariable UUID locationId,
            @PathVariable UUID eventId,
            Principal principal) {

        UUID myId = userService.requireUser(principal).getUserId();
        var locOpt = locationRepo.findById(locationId);
        if (locOpt.isEmpty()) return ResponseEntity.notFound().build();
        if (!locOpt.get().getOwnerId().equals(myId)) return ResponseEntity.status(403).build();

        var evtOpt = eventRepo.findById(eventId);
        if (evtOpt.isEmpty()) return ResponseEntity.notFound().build();
        if (!evtOpt.get().getLocationId().equals(locationId)) return ResponseEntity.status(400).build();

        attendeeRepo.deleteByEventId(eventId);
        eventRepo.delete(evtOpt.get());
        return ResponseEntity.noContent().build();
    }

    // ── Event-Detail und Teilnahme ────────────────────────────────────────────

    @GetMapping("/location-events/{eventId}")
    public ResponseEntity<EventDetailDto> getEvent(
            @PathVariable UUID eventId,
            Principal principal) {

        UUID myId = userService.requireUser(principal).getUserId();
        var evtOpt = eventRepo.findById(eventId);
        if (evtOpt.isEmpty()) return ResponseEntity.notFound().build();

        String locationName = locationRepo.findById(evtOpt.get().getLocationId())
                .map(l -> l.getName()).orElse("");
        return ResponseEntity.ok(toDetail(evtOpt.get(), locationName, myId));
    }

    @PostMapping("/location-events/{eventId}/attend")
    public ResponseEntity<Map<String, Object>> toggleAttend(
            @PathVariable UUID eventId,
            Principal principal) {

        UUID myId = userService.requireUser(principal).getUserId();
        if (!eventRepo.existsById(eventId)) return ResponseEntity.notFound().build();

        var existing = attendeeRepo.findByEventIdAndUserId(eventId, myId);
        boolean attending;
        if (existing.isPresent()) {
            attendeeRepo.delete(existing.get());
            attending = false;
        } else {
            LocationEventAttendeeEntity a = new LocationEventAttendeeEntity();
            a.setAttendeeId(UUID.randomUUID());
            a.setEventId(eventId);
            a.setUserId(myId);
            a.setRegisteredAt(LocalDateTime.now());
            attendeeRepo.save(a);
            attending = true;
        }
        long count = attendeeRepo.countByEventId(eventId);
        return ResponseEntity.ok(Map.of("attending", attending, "attendeeCount", count));
    }

    // ── Event-Suche (IDs + Batch) ─────────────────────────────────────────────

    /**
     * Liefert Event-IDs sortiert nach Datum (nächste zuerst).
     * Immer enthalten: Events von abonnierten Locations.
     * Optional: Events im Umkreis von lat/lon (wenn angegeben).
     */
    @GetMapping("/location-events/ids")
    public ResponseEntity<IdsResult> searchIds(
            @RequestParam(required = false) Double lat,
            @RequestParam(required = false) Double lon,
            @RequestParam(defaultValue = "50") int maxDistanceKm,
            @RequestParam(required = false) String from,
            @RequestParam(required = false) String to,
            Principal principal) {

        UUID myId = userService.requireUser(principal).getUserId();

        LocalDateTime fromDt = from != null ? LocalDateTime.parse(from) : LocalDateTime.now();
        LocalDateTime toDt   = to   != null ? LocalDateTime.parse(to)   : fromDt.plusMonths(3);

        // Abonnierte Locations – deren Events werden immer eingeschlossen
        Set<UUID> followedLocationIds = followRepo.findByUserId(myId).stream()
                .map(LocationFollowEntity::getLocationId)
                .collect(Collectors.toSet());

        Map<UUID, double[]> locationCoords = new HashMap<>();
        List<UUID> sorted = eventRepo.findInTimeRange(fromDt, toDt).stream()
                .filter(e -> {
                    // Immer einschließen wenn Location abonniert
                    if (followedLocationIds.contains(e.getLocationId())) return true;
                    // Ohne Koordinaten kein Umkreis-Filter
                    if (lat == null || lon == null) return false;
                    double[] coords = locationCoords.computeIfAbsent(e.getLocationId(), locId ->
                            locationRepo.findById(locId)
                                    .filter(l -> l.getLat() != null && l.getLon() != null)
                                    .map(l -> new double[]{l.getLat(), l.getLon()})
                                    .orElse(null));
                    if (coords == null) return false;
                    return LocationController.haversineKm(lat, lon, coords[0], coords[1]) <= maxDistanceKm;
                })
                .sorted(Comparator.comparing(LocationEventEntity::getStartAt))
                .map(LocationEventEntity::getEventId)
                .collect(Collectors.toList());

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

    /**
     * Batch-Laden von Event-Previews.
     */
    @PostMapping("/location-events/batch")
    public ResponseEntity<List<EventPreviewDto>> getBatch(
            @RequestBody BatchRequest request,
            Principal principal) {

        UUID myId = userService.requireUser(principal).getUserId();
        if (request.ids() == null || request.ids().isEmpty()) return ResponseEntity.ok(List.of());

        List<UUID> ids = request.ids().stream().filter(Objects::nonNull).limit(MAX_BATCH_SIZE).toList();
        Map<UUID, LocationEventEntity> byId = eventRepo.findAllById(ids).stream()
                .collect(Collectors.toMap(LocationEventEntity::getEventId, Function.identity()));

        double refLat = request.lat() != null ? request.lat() : 0;
        double refLon = request.lon() != null ? request.lon() : 0;

        // Locations für Namens-Lookup und Distanzberechnung
        Set<UUID> locationIds = byId.values().stream()
                .map(LocationEventEntity::getLocationId).collect(Collectors.toSet());
        Map<UUID, de.oaa.xxx.location.entity.LocationEntity> locationById =
                locationRepo.findAllById(locationIds).stream()
                        .collect(Collectors.toMap(
                                de.oaa.xxx.location.entity.LocationEntity::getLocationId, Function.identity()));

        List<EventPreviewDto> result = ids.stream()
                .map(byId::get)
                .filter(Objects::nonNull)
                .map(e -> {
                    var loc = locationById.get(e.getLocationId());
                    String locName = loc != null ? loc.getName() : "";
                    double dist = (loc != null && loc.getLat() != null && loc.getLon() != null)
                            ? Math.round(LocationController.haversineKm(refLat, refLon, loc.getLat(), loc.getLon()) * 10.0) / 10.0
                            : -1;
                    return toPreview(e, locName, refLat, refLon, myId);
                })
                .toList();

        return ResponseEntity.ok(result);
    }

    // ── Helpers ───────────────────────────────────────────────────────────────

    private EventPreviewDto toPreview(LocationEventEntity e, String locationName,
                                      double refLat, double refLon, UUID myId) {
        var loc = locationRepo.findById(e.getLocationId()).orElse(null);
        double dist = (loc != null && loc.getLat() != null && loc.getLon() != null)
                ? Math.round(LocationController.haversineKm(refLat, refLon, loc.getLat(), loc.getLon()) * 10.0) / 10.0
                : -1;
        long count   = attendeeRepo.countByEventId(e.getEventId());
        boolean mine = attendeeRepo.findByEventIdAndUserId(e.getEventId(), myId).isPresent();
        return new EventPreviewDto(e.getEventId(), e.getLocationId(), locationName,
                e.getTitle(), e.getImageData(), e.getStartAt(), dist, count, mine);
    }

    private EventDetailDto toDetail(LocationEventEntity e, String locationName, UUID myId) {
        List<LocationEventAttendeeEntity> attendeeEntities =
                attendeeRepo.findByEventIdOrderByRegisteredAtAsc(e.getEventId());

        Set<UUID> attendeeUserIds = attendeeEntities.stream()
                .map(LocationEventAttendeeEntity::getUserId).collect(Collectors.toSet());
        Map<UUID, UserEntity> userById = attendeeUserIds.isEmpty()
                ? Map.of()
                : userRepo.findAllById(attendeeUserIds).stream()
                        .collect(Collectors.toMap(UserEntity::getUserId, Function.identity()));

        // Sortierung: Alphabetisch nach Geschlecht-Name, dann nach Registrierungszeit
        List<AttendeeDto> attendees = attendeeEntities.stream()
                .map(a -> {
                    UserEntity u = userById.get(a.getUserId());
                    if (u == null) return null;
                    String geschlecht = u.getGeschlecht() != null ? u.getGeschlecht().name() : "UNBEKANNT";
                    return new AttendeeDto(u.getUserId(), u.getName(), u.getProfilePicture(), geschlecht);
                })
                .filter(Objects::nonNull)
                .sorted(Comparator
                        .comparing((AttendeeDto a) -> {
                            // Sortierreihenfolge: WEIBLICH → MAENNLICH → DIVERS → rest
                            return switch (a.geschlecht()) {
                                case "WEIBLICH"  -> 0;
                                case "MAENNLICH" -> 1;
                                case "DIVERS"    -> 2;
                                default          -> 3;
                            };
                        })
                        .thenComparing(AttendeeDto::name))
                .toList();

        boolean attendingMe = attendeeRepo.findByEventIdAndUserId(e.getEventId(), myId).isPresent();

        return new EventDetailDto(
                e.getEventId(), e.getLocationId(), locationName,
                e.getTitle(), e.getDescription(), e.getImageData(),
                e.getStartAt(), e.getCreatedAt(),
                attendingMe, attendees);
    }
}
