package de.oaa.xxx.location;

import de.oaa.xxx.location.entity.*;
import de.oaa.xxx.location.repository.*;
import de.oaa.xxx.user.UserService;
import jakarta.transaction.Transactional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.security.Principal;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/locations")
public class LocationController {

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

    private final LocationRepository            locationRepo;
    private final LocationImageRepository       imageRepo;
    private final LocationOpeningHoursRepository hoursRepo;
    private final LocationEventRepository       eventRepo;
    private final LocationEventAttendeeRepository attendeeRepo;
    private final LocationFollowRepository      followRepo;
    private final UserService                   userService;

    public LocationController(LocationRepository locationRepo,
                              LocationImageRepository imageRepo,
                              LocationOpeningHoursRepository hoursRepo,
                              LocationEventRepository eventRepo,
                              LocationEventAttendeeRepository attendeeRepo,
                              LocationFollowRepository followRepo,
                              UserService userService) {
        this.locationRepo  = locationRepo;
        this.imageRepo     = imageRepo;
        this.hoursRepo     = hoursRepo;
        this.eventRepo     = eventRepo;
        this.attendeeRepo  = attendeeRepo;
        this.followRepo    = followRepo;
        this.userService   = userService;
    }

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

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

    record LocationPreviewDto(UUID locationId, String name, String profilePictureLq, double distanzKm) {}

    record OpeningHourDto(int dayOfWeek, String openTime, String closeTime, boolean closed) {}

    record GalleryImageDto(UUID imageId, String imageData) {}

    record LocationDetailDto(
            UUID locationId,
            UUID ownerId,
            String name,
            String description,
            String profilePictureHq,
            String profilePictureLq,
            Double lat,
            Double lon,
            String street,
            String city,
            boolean ownershipConfirmed,
            LocalDateTime createdAt,
            List<GalleryImageDto> gallery,
            List<OpeningHourDto> openingHours,
            boolean following
    ) {}

    record CreateRequest(
            String name,
            String description,
            String profilePictureLq,
            String profilePictureHq,
            Double lat,
            Double lon,
            String street,
            String city,
            boolean ownershipConfirmed
    ) {}

    record UpdateRequest(
            String name,
            String description,
            String profilePictureLq,
            String profilePictureHq,
            Double lat,
            Double lon,
            String street,
            String city
    ) {}

    record GalleryUploadRequest(String imageData) {}

    // ── Suche / IDs ──────────────────────────────────────────────────────────

    /**
     * Gibt alle Location-IDs zurück, sortiert nach Entfernung vom angegebenen Punkt.
     */
    @GetMapping("/ids")
    public ResponseEntity<IdsResult> getIds(
            @RequestParam double lat,
            @RequestParam double lon,
            @RequestParam(defaultValue = "50") int maxDistanceKm,
            Principal principal) {

        userService.requireUser(principal);

        List<UUID> sorted = locationRepo.findByLatIsNotNullAndLonIsNotNull().stream()
                .filter(l -> haversineKm(lat, lon, l.getLat(), l.getLon()) <= maxDistanceKm)
                .sorted(Comparator.comparingDouble(l -> haversineKm(lat, lon, l.getLat(), l.getLon())))
                .map(LocationEntity::getLocationId)
                .collect(Collectors.toList());

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

    /**
     * Batch-Laden von Location-Previews (Name + LQ-Bild + Distanz).
     * Benötigt lat/lon für die Distanzberechnung.
     */
    @PostMapping("/batch")
    public ResponseEntity<List<LocationPreviewDto>> getBatch(
            @RequestBody BatchRequest request,
            Principal principal) {

        userService.requireUser(principal);
        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, LocationEntity> byId = locationRepo.findAllById(ids).stream()
                .collect(Collectors.toMap(LocationEntity::getLocationId, l -> l));

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

        List<LocationPreviewDto> result = ids.stream()
                .map(byId::get)
                .filter(Objects::nonNull)
                .map(l -> new LocationPreviewDto(
                        l.getLocationId(),
                        l.getName(),
                        l.getProfilePictureLq(),
                        l.getLat() != null && l.getLon() != null
                                ? Math.round(haversineKm(refLat, refLon, l.getLat(), l.getLon()) * 10.0) / 10.0
                                : -1))
                .toList();

        return ResponseEntity.ok(result);
    }

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

    /**
     * Meine eigenen Locations (IDs, neueste zuerst).
     */
    @GetMapping("/mine")
    public ResponseEntity<IdsResult> getMine(Principal principal) {
        UUID myId = userService.requireUser(principal).getUserId();
        List<UUID> ids = locationRepo.findByOwnerIdOrderByCreatedAtDesc(myId).stream()
                .map(LocationEntity::getLocationId)
                .toList();
        return ResponseEntity.ok(new IdsResult(ids, ids.size()));
    }

    // ── Detail ───────────────────────────────────────────────────────────────

    @GetMapping("/{locationId}")
    public ResponseEntity<LocationDetailDto> getDetail(
            @PathVariable UUID locationId,
            Principal principal) {

        UUID myId = userService.requireUser(principal).getUserId();
        return locationRepo.findById(locationId)
                .map(l -> ResponseEntity.ok(toDetail(l, myId)))
                .orElse(ResponseEntity.notFound().build());
    }

    // ── CRUD ─────────────────────────────────────────────────────────────────

    @PostMapping
    public ResponseEntity<LocationDetailDto> create(
            @RequestBody CreateRequest req,
            Principal principal) {

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

        if (req.name() == null || req.name().isBlank()) return ResponseEntity.badRequest().build();

        LocationEntity loc = new LocationEntity();
        loc.setLocationId(UUID.randomUUID());
        loc.setOwnerId(myId);
        loc.setName(req.name().trim());
        loc.setDescription(req.description() != null ? req.description().trim() : null);
        loc.setProfilePictureLq(req.profilePictureLq());
        loc.setProfilePictureHq(req.profilePictureHq());
        loc.setLat(req.lat());
        loc.setLon(req.lon());
        loc.setStreet(req.street());
        loc.setCity(req.city());
        loc.setOwnershipConfirmed(req.ownershipConfirmed());
        loc.setCreatedAt(LocalDateTime.now());
        locationRepo.save(loc);

        LOGGER.info("User {} hat Location {} angelegt", myId, loc.getLocationId());
        return ResponseEntity.status(201).body(toDetail(loc, myId));
    }

    @PutMapping("/{locationId}")
    public ResponseEntity<LocationDetailDto> update(
            @PathVariable UUID locationId,
            @RequestBody UpdateRequest req,
            Principal principal) {

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

        if (req.name() != null && !req.name().isBlank()) loc.setName(req.name().trim());
        if (req.description() != null) loc.setDescription(req.description().trim());
        if (req.profilePictureLq() != null) loc.setProfilePictureLq(req.profilePictureLq());
        if (req.profilePictureHq() != null) loc.setProfilePictureHq(req.profilePictureHq());
        if (req.lat() != null) loc.setLat(req.lat());
        if (req.lon() != null) loc.setLon(req.lon());
        if (req.street() != null) loc.setStreet(req.street());
        if (req.city() != null) loc.setCity(req.city());
        locationRepo.save(loc);

        return ResponseEntity.ok(toDetail(loc, myId));
    }

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

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

        // Kaskade: Events + Attendees + Bilder + Öffnungszeiten + Follows löschen
        eventRepo.findByLocationIdOrderByStartAtAsc(locationId).forEach(e -> {
            attendeeRepo.deleteByEventId(e.getEventId());
        });
        eventRepo.deleteByLocationId(locationId);
        imageRepo.deleteByLocationId(locationId);
        hoursRepo.deleteByLocationId(locationId);
        followRepo.deleteByLocationId(locationId);
        locationRepo.deleteById(locationId);

        LOGGER.info("User {} hat Location {} gelöscht", myId, locationId);
        return ResponseEntity.noContent().build();
    }

    // ── Galerie ───────────────────────────────────────────────────────────────

    @PostMapping("/{locationId}/gallery")
    public ResponseEntity<GalleryImageDto> uploadGalleryImage(
            @PathVariable UUID locationId,
            @RequestBody GalleryUploadRequest req,
            Principal principal) {

        UUID myId = userService.requireUser(principal).getUserId();
        var opt = locationRepo.findById(locationId);
        if (opt.isEmpty()) return ResponseEntity.notFound().build();
        if (!opt.get().getOwnerId().equals(myId)) return ResponseEntity.status(403).build();
        if (req.imageData() == null || req.imageData().isBlank()) return ResponseEntity.badRequest().build();
        if (imageRepo.countByLocationId(locationId) >= MAX_GALLERY_IMAGES) return ResponseEntity.status(422).build();

        LocationImageEntity img = new LocationImageEntity();
        img.setImageId(UUID.randomUUID());
        img.setLocationId(locationId);
        img.setImageData(req.imageData());
        img.setUploadedAt(LocalDateTime.now());
        imageRepo.save(img);

        return ResponseEntity.status(201).body(new GalleryImageDto(img.getImageId(), img.getImageData()));
    }

    @Transactional
    @DeleteMapping("/{locationId}/gallery/{imageId}")
    public ResponseEntity<Void> deleteGalleryImage(
            @PathVariable UUID locationId,
            @PathVariable UUID imageId,
            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 imgOpt = imageRepo.findById(imageId);
        if (imgOpt.isEmpty()) return ResponseEntity.notFound().build();
        if (!imgOpt.get().getLocationId().equals(locationId)) return ResponseEntity.status(400).build();

        imageRepo.delete(imgOpt.get());
        return ResponseEntity.noContent().build();
    }

    // ── Öffnungszeiten ────────────────────────────────────────────────────────

    @GetMapping("/{locationId}/opening-hours")
    public ResponseEntity<List<OpeningHourDto>> getOpeningHours(
            @PathVariable UUID locationId,
            Principal principal) {

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

        List<OpeningHourDto> dtos = hoursRepo.findByLocationIdOrderByDayOfWeek(locationId).stream()
                .map(h -> new OpeningHourDto(h.getDayOfWeek(), h.getOpenTime(), h.getCloseTime(), h.isClosed()))
                .toList();
        return ResponseEntity.ok(dtos);
    }

    /**
     * Setzt die Öffnungszeiten einer Location vollständig neu (kompletter Ersatz).
     */
    @Transactional
    @PutMapping("/{locationId}/opening-hours")
    public ResponseEntity<List<OpeningHourDto>> setOpeningHours(
            @PathVariable UUID locationId,
            @RequestBody List<OpeningHourDto> hours,
            Principal principal) {

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

        hoursRepo.deleteByLocationId(locationId);

        List<LocationOpeningHoursEntity> saved = new ArrayList<>();
        for (OpeningHourDto dto : hours) {
            if (dto.dayOfWeek() < 1 || dto.dayOfWeek() > 7) continue;
            LocationOpeningHoursEntity e = new LocationOpeningHoursEntity();
            e.setHoursId(UUID.randomUUID());
            e.setLocationId(locationId);
            e.setDayOfWeek(dto.dayOfWeek());
            e.setOpenTime(dto.openTime());
            e.setCloseTime(dto.closeTime());
            e.setClosed(dto.closed());
            saved.add(hoursRepo.save(e));
        }

        return ResponseEntity.ok(saved.stream()
                .map(h -> new OpeningHourDto(h.getDayOfWeek(), h.getOpenTime(), h.getCloseTime(), h.isClosed()))
                .toList());
    }

    // ── Follow ────────────────────────────────────────────────────────────────

    @PostMapping("/{locationId}/follow")
    public ResponseEntity<Map<String, Object>> toggleFollow(
            @PathVariable UUID locationId,
            Principal principal) {

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

        var existing = followRepo.findByUserIdAndLocationId(myId, locationId);
        boolean following;
        if (existing.isPresent()) {
            followRepo.delete(existing.get());
            following = false;
        } else {
            var f = new de.oaa.xxx.location.entity.LocationFollowEntity();
            f.setFollowId(UUID.randomUUID());
            f.setLocationId(locationId);
            f.setUserId(myId);
            f.setFollowedAt(LocalDateTime.now());
            followRepo.save(f);
            following = true;
        }
        return ResponseEntity.ok(Map.of("following", following));
    }

    @GetMapping("/followed")
    public ResponseEntity<IdsResult> getFollowed(Principal principal) {
        UUID myId = userService.requireUser(principal).getUserId();
        List<UUID> ids = followRepo.findByUserId(myId).stream()
                .map(de.oaa.xxx.location.entity.LocationFollowEntity::getLocationId)
                .toList();
        return ResponseEntity.ok(new IdsResult(ids, ids.size()));
    }

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

    private LocationDetailDto toDetail(LocationEntity l, UUID myId) {
        List<GalleryImageDto> gallery = imageRepo.findByLocationIdOrderByUploadedAtAsc(l.getLocationId()).stream()
                .map(i -> new GalleryImageDto(i.getImageId(), i.getImageData()))
                .toList();
        List<OpeningHourDto> hours = hoursRepo.findByLocationIdOrderByDayOfWeek(l.getLocationId()).stream()
                .map(h -> new OpeningHourDto(h.getDayOfWeek(), h.getOpenTime(), h.getCloseTime(), h.isClosed()))
                .toList();
        boolean following = followRepo.findByUserIdAndLocationId(myId, l.getLocationId()).isPresent();
        return new LocationDetailDto(
                l.getLocationId(), l.getOwnerId(), l.getName(), l.getDescription(),
                l.getProfilePictureHq(), l.getProfilePictureLq(),
                l.getLat(), l.getLon(), l.getStreet(), l.getCity(),
                l.isOwnershipConfirmed(), l.getCreatedAt(),
                gallery, hours, following);
    }

    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));
    }
}
