package com.dedicatedcode.reitti.service.processing;

import com.dedicatedcode.reitti.event.ProcessedVisitCreatedEvent;
import com.dedicatedcode.reitti.event.RecalculateTripEvent;
import com.dedicatedcode.reitti.model.geo.*;
import com.dedicatedcode.reitti.model.security.User;
import com.dedicatedcode.reitti.repository.*;
import com.dedicatedcode.reitti.service.UserNotificationService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;

@Service
public class TripDetectionService {

    private static final Logger logger = LoggerFactory.getLogger(TripDetectionService.class);

    private final ProcessedVisitJdbcService processedVisitJdbcService;
    private final PreviewProcessedVisitJdbcService previewProcessedVisitJdbcService;
    private final RawLocationPointJdbcService rawLocationPointJdbcService;
    private final PreviewRawLocationPointJdbcService previewRawLocationPointJdbcService;
    private final TripJdbcService tripJdbcService;
    private final PreviewTripJdbcService previewTripJdbcService;
    private final TransportModeService transportModeService;
    private final UserJdbcService userJdbcService;
    private final UserNotificationService userNotificationService;
    private final ConcurrentHashMap<String, ReentrantLock> userLocks = new ConcurrentHashMap<>();

    public TripDetectionService(ProcessedVisitJdbcService processedVisitJdbcService,
                                PreviewProcessedVisitJdbcService previewProcessedVisitJdbcService,
                                RawLocationPointJdbcService rawLocationPointJdbcService,
                                PreviewRawLocationPointJdbcService previewRawLocationPointJdbcService,
                                TripJdbcService tripJdbcService,
                                PreviewTripJdbcService previewTripJdbcService, TransportModeService transportModeService,
                                UserJdbcService userJdbcService,
                                UserNotificationService userNotificationService) {
        this.processedVisitJdbcService = processedVisitJdbcService;
        this.previewProcessedVisitJdbcService = previewProcessedVisitJdbcService;
        this.rawLocationPointJdbcService = rawLocationPointJdbcService;
        this.previewRawLocationPointJdbcService = previewRawLocationPointJdbcService;
        this.tripJdbcService = tripJdbcService;
        this.previewTripJdbcService = previewTripJdbcService;
        this.transportModeService = transportModeService;
        this.userJdbcService = userJdbcService;
        this.userNotificationService = userNotificationService;
    }

    public void visitCreated(ProcessedVisitCreatedEvent event) {
        String username = event.getUsername();
        ReentrantLock userLock = userLocks.computeIfAbsent(username, k -> new ReentrantLock());
        
        userLock.lock();
        try {
            User user = this.userJdbcService.findByUsername(username).orElseThrow();

            Optional<ProcessedVisit> createdVisit;
            if (event.getPreviewId() == null) {
                createdVisit = this.processedVisitJdbcService.findByUserAndId(user, event.getVisitId());
            } else {
                createdVisit = this.previewProcessedVisitJdbcService.findByUserAndId(user, event.getVisitId());
            }
            createdVisit.ifPresent(visit -> {
                //find visits in timerange
                Instant searchStart = visit.getStartTime().minus(1, ChronoUnit.DAYS);
                Instant searchEnd = visit.getEndTime().plus(1, ChronoUnit.DAYS);

                List<ProcessedVisit> visits;
                if (event.getPreviewId() == null) {
                    visits = this.processedVisitJdbcService.findByUserAndTimeOverlap(user, searchStart, searchEnd);
                } else {
                    visits = this.previewProcessedVisitJdbcService.findByUserAndTimeOverlap(user, event.getPreviewId(), searchStart, searchEnd);
                }

                if (visits.size() < 2) {
                    logger.info("Not enough visits to detect trips for user: {}", user.getUsername());
                    return;
                }

                List<Trip> trips = new ArrayList<>();
                // Iterate through consecutive visits to detect trips
                for (int i = 0; i < visits.size() - 1; i++) {
                    ProcessedVisit startVisit = visits.get(i);
                    ProcessedVisit endVisit = visits.get(i + 1);

                    // Create a trip between these two visits
                    Trip trip = createTripBetweenVisits(user, event.getPreviewId(), startVisit, endVisit);
                    if (trip != null) {
                        trips.add(trip);
                    }
                }

                if (event.getPreviewId() == null) {
                    tripJdbcService.bulkInsert(user, trips);
                } else {
                    previewTripJdbcService.bulkInsert(user, event.getPreviewId(), trips);

                }
                if (event.getPreviewId() == null) {
                    userNotificationService.newTrips(user, trips);
                } else {
                    userNotificationService.newTrips(user, trips, event.getPreviewId());
                }
            });
        } finally {
            userLock.unlock();
        }
    }

    private Trip createTripBetweenVisits(User user, String previewId, ProcessedVisit startVisit, ProcessedVisit endVisit) {
        // Trip starts when the first visit ends
        Instant tripStartTime = startVisit.getEndTime();

        // Trip ends when the second visit starts
        Instant tripEndTime = endVisit.getStartTime();

        if (previewId != null) {
            if (this.previewProcessedVisitJdbcService.findById(startVisit.getId()).isEmpty() || this.previewProcessedVisitJdbcService.findById(endVisit.getId()).isEmpty()) {
                logger.debug("One of the following preview visits [{},{}] where already deleted. Will skip trip creation.", startVisit.getId(), endVisit.getId());
                return null;
            }
        } else {
            if (this.processedVisitJdbcService.findById(startVisit.getId()).isEmpty() || this.processedVisitJdbcService.findById(endVisit.getId()).isEmpty()) {
                logger.debug("One of the following visits [{},{}] where already deleted. Will skip trip creation.", startVisit.getId(), endVisit.getId());
                return null;
            }
        }
        // If end time is before or equal to start time, this is not a valid trip
        if (tripEndTime.isBefore(tripStartTime) || tripEndTime.equals(tripStartTime)) {
            logger.warn("Invalid trip time range detected for user {}: {} to {}",
                    user.getUsername(), tripStartTime, tripEndTime);
            return null;
        }


        if (previewId == null) {
            // Check if a trip already exists with the same start and end times
            if (tripJdbcService.existsByUserAndStartTimeAndEndTime(user, tripStartTime, tripEndTime)) {
                logger.debug("Trip already exists for user {} from {} to {}",
                        user.getUsername(), tripStartTime, tripEndTime);
                return null;
            }
        }

        // Get location points between the two visits
        List<RawLocationPoint> tripPoints;
        if (previewId == null) {
            tripPoints = rawLocationPointJdbcService.findByUserAndTimestampBetweenOrderByTimestampAsc(user, tripStartTime, tripEndTime);
        } else {
            tripPoints = previewRawLocationPointJdbcService.findByUserAndTimestampBetweenOrderByTimestampAsc(user, previewId, tripStartTime, tripEndTime);
        }
        double estimatedDistanceInMeters = calculateDistanceBetweenPlaces(startVisit.getPlace(), endVisit.getPlace());
        double travelledDistanceMeters = GeoUtils.calculateTripDistance(tripPoints);
        // Create a new trip
        TransportMode transportMode = this.transportModeService.inferTransportMode(user, tripPoints, tripStartTime, tripEndTime);
        Trip trip = new Trip(
                tripStartTime,
                tripEndTime,
                tripEndTime.getEpochSecond() - tripStartTime.getEpochSecond(),
                estimatedDistanceInMeters,
                travelledDistanceMeters,
                transportMode,
                startVisit,
                endVisit
        );
        logger.debug("Created trip from {} to {}: travelled distance={}m, mode={}",
                startVisit.getPlace().getName(), endVisit.getPlace().getName(), Math.round(travelledDistanceMeters), transportMode);

        // Save and return the trip
        return trip;
    }

    private double calculateDistanceBetweenPlaces(SignificantPlace place1, SignificantPlace place2) {
        return GeoUtils.distanceInMeters(
                place1.getLatitudeCentroid(), place1.getLongitudeCentroid(),
                place2.getLatitudeCentroid(), place2.getLongitudeCentroid());
    }

    public void recalculateTrip(RecalculateTripEvent event) {
        User user = this.userJdbcService.findByUsername(event.getUsername()).orElseThrow();

        this.tripJdbcService.findByUserAndId(user, event.getTripId()).ifPresent(trip -> {
            logger.info("Recalculating trip [{}] for user [{}]", trip.getId(), user.getUsername());
            Instant startTime = trip.getStartTime();
            Instant endTime = trip.getEndTime();
            List<RawLocationPoint> tripPoints = rawLocationPointJdbcService.findByUserAndTimestampBetweenOrderByTimestampAsc(user, startTime, endTime);
            TransportMode transportMode = this.transportModeService.inferTransportMode(user, tripPoints, startTime, endTime);
            if (trip.getTransportModeInferred() != transportMode) {
                logger.info("TransportMode changed from [{}] to [{}] for trip [{}]", trip.getTransportModeInferred(), transportMode, trip.getId());
                this.tripJdbcService.update(trip.withTransportMode(transportMode));
            }
        });
    }
}
