package com.walkguide.service; import com.walkguide.dto.request.LocationUpdateRequest; import com.walkguide.dto.response.LocationResponse; import com.walkguide.entity.LocationHistory; import com.walkguide.entity.User; import com.walkguide.enums.ActivityLogType; import com.walkguide.enums.PairingStatus; import com.walkguide.exception.PairingException; import com.walkguide.exception.ResourceNotFoundException; import com.walkguide.repository.*; import com.walkguide.websocket.LocationBroadcaster; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import java.util.Map; import java.util.Optional; @Service @RequiredArgsConstructor @Slf4j public class LocationService { private final LocationHistoryRepository locationHistoryRepository; private final GeofenceConfigRepository geofenceConfigRepository; private final PairingRelationRepository pairingRelationRepository; private final UserRepository userRepository; private final ActivityLogService activityLogService; private final FcmService fcmService; // ✅ BARU: WebSocket broadcaster untuk real-time location ke Guardian private final LocationBroadcaster locationBroadcaster; public LocationResponse updateLocation(Long userId, LocationUpdateRequest req) { LocationHistory loc = LocationHistory.builder() .userId(userId) .lat(req.getLat()) .lng(req.getLng()) .accuracy(req.getAccuracy()) .speed(req.getSpeed()) .heading(req.getHeading()) .build(); loc = locationHistoryRepository.save(loc); LocationResponse response = toResponse(loc); // ✅ BROADCAST real-time ke Guardian via WebSocket // Guardian subscribe ke /topic/location/{userId} locationBroadcaster.broadcastLocation(userId, response); log.debug("[LOCATION] Broadcast to Guardian | userId={} lat={} lng={}", userId, req.getLat(), req.getLng()); // Cek geofence checkGeofence(userId, req.getLat(), req.getLng()); return response; } public Optional getLastLocation(Long userId) { return locationHistoryRepository .findTopByUserIdOrderByCreatedAtDesc(userId) .map(this::toResponse); } // Untuk Guardian: ambil last location user yang dipair public Optional getLastLocationForGuardian(Long guardianId) { return pairingRelationRepository .findByGuardian_IdAndStatus(guardianId, PairingStatus.ACTIVE) .flatMap(p -> locationHistoryRepository .findTopByUserIdOrderByCreatedAtDesc(p.getUser().getId())) .map(this::toResponse); } public Page getLocationHistory(Long userId, Pageable pageable) { return locationHistoryRepository .findByUserIdOrderByCreatedAtDesc(userId, pageable) .map(this::toResponse); } public Page getLocationHistoryForGuardian(Long guardianId, Pageable pageable) { Long pairedUserId = pairingRelationRepository .findByGuardian_IdAndStatus(guardianId, PairingStatus.ACTIVE) .map(pairing -> pairing.getUser().getId()) .orElseThrow(() -> new PairingException("Guardian belum terhubung dengan User aktif")); return getLocationHistory(pairedUserId, pageable); } // ========== GEOFENCE ========== private void checkGeofence(Long userId, double lat, double lng) { geofenceConfigRepository.findByUserId(userId).ifPresent(cfg -> { if (!cfg.getEnabled() || cfg.getCenterLat() == null) return; double dist = haversineMeters(cfg.getCenterLat(), cfg.getCenterLng(), lat, lng); if (dist > cfg.getRadiusMeters()) { // User keluar geofence — beri tahu Guardian pairingRelationRepository.findByUser_IdAndStatus(userId, PairingStatus.ACTIVE) .ifPresent(pairing -> { User guardian = pairing.getGuardian(); User user = pairing.getUser(); fcmService.sendHighPriority( guardian.getFcmToken(), "⚠️ User Keluar Area Aman", user.getDisplayName() + " meninggalkan area geofence! " + String.format("%.0fm dari pusat area", dist), Map.of("type", "GEOFENCE_EXIT", "userId", String.valueOf(userId), "lat", String.valueOf(lat), "lng", String.valueOf(lng)) ); activityLogService.createLog(user, ActivityLogType.GEOFENCE_EXIT, String.format("User keluar area geofence (%.0fm dari pusat)", dist), null); log.info("[GEOFENCE] User {} keluar area ({:.0f}m)", userId, dist); }); } }); } /** Haversine formula — jarak antara 2 koordinat dalam meter */ public static double haversineMeters(double lat1, double lng1, double lat2, double lng2) { final double R = 6371000; // radius bumi dalam meter double dLat = Math.toRadians(lat2 - lat1); double dLng = Math.toRadians(lng2 - lng1); double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2); return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); } private LocationResponse toResponse(LocationHistory l) { return LocationResponse.builder() .id(l.getId()).lat(l.getLat()).lng(l.getLng()) .accuracy(l.getAccuracy()).speed(l.getSpeed()).heading(l.getHeading()) .createdAt(l.getCreatedAt()).build(); } }