package org.github.tess1o.geopulse.streaming.service;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import lombok.extern.slf4j.Slf4j;
import org.github.tess1o.geopulse.streaming.config.TimelineConfig;
import org.github.tess1o.geopulse.streaming.config.TimelineConfigurationProvider;
import org.github.tess1o.geopulse.streaming.model.dto.TripClassificationDetailsDTO;
import org.github.tess1o.geopulse.streaming.model.dto.TripClassificationDetailsDTO.*;
import org.github.tess1o.geopulse.streaming.model.entity.TimelineTripEntity;
import org.github.tess1o.geopulse.streaming.model.shared.MovementTypeSource;
import org.github.tess1o.geopulse.streaming.repository.TimelineTripRepository;
import org.github.tess1o.geopulse.streaming.service.trips.TravelClassification;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

/**
 * Service for generating detailed trip classification explanations.
 * Helps users understand why their trip was classified as a specific transport type.
 */
@ApplicationScoped
@Slf4j
public class TripClassificationDetailsService {

    @Inject
    TimelineTripRepository tripRepository;

    @Inject
    TimelineConfigurationProvider configProvider;

    @Inject
    TravelClassification travelClassification;

    // ============================================
    // GPS VALIDATION CONSTANTS
    // These MUST match TravelClassification constants to ensure consistency
    // ============================================

    /**
     * Minimum GPS speed to indicate actual movement (km/h).
     * Used to distinguish between stopped/very slow movement vs GPS noise.
     */
    private static final double MIN_GPS_MOVEMENT_SPEED_KMH = 3.0;

    /**
     * Speed threshold for unrealistically low calculated speeds (km/h).
     * If calculated speed is below this but GPS shows movement, calculated distance is likely wrong.
     */
    private static final double MIN_REALISTIC_CALCULATED_SPEED_KMH = 1.0;

    /**
     * Maximum allowed ratio of GPS max speed to calculated avg speed.
     * If GPS max > calculated avg * this ratio, GPS max is considered unreliable noise.
     */
    private static final double GPS_MAX_SPEED_NOISE_RATIO = 5.0;

    /**
     * Multiplier for estimating reasonable max speed from avg speed.
     * Used when GPS max speed is deemed unreliable.
     */
    private static final double ESTIMATED_MAX_SPEED_MULTIPLIER = 1.5;

    /**
     * Coefficient for walk speed verification (multiplier).
     * If calculated speed exceeds walkingMaxMaxSpeed * this coefficient,
     * reclassify as the preferred motor-vehicle label or UNKNOWN.
     */
    private static final double WALK_VERIFICATION_COEFFICIENT = 1.2;

    // ============================================
    // DISTANCE-BASED CLASSIFICATION THRESHOLDS
    // These MUST match TravelClassification constants to ensure consistency
    // ============================================

    /**
     * Extreme distance threshold (km).
     * Distances exceeding this are almost certainly flights (no realistic ground transport alternative).
     */
    private static final double EXTREME_FLIGHT_DISTANCE_KM = 1000.0;

    /**
     * Minimum average speed for extreme distance classification (km/h).
     * Prevents GPS drift/errors and high-speed trains from triggering flight classification.
     */
    private static final double EXTREME_FLIGHT_MIN_SPEED_KMH = 350.0;

    /**
     * Long distance threshold for flight detection (km).
     * Combined with high speed, indicates likely flight.
     */
    private static final double LONG_FLIGHT_DISTANCE_KM = 300.0;

    /**
     * High speed threshold for long distance flights (km/h).
     */
    private static final double LONG_FLIGHT_MIN_SPEED_KMH = 280.0;

    /**
     * Very long distance threshold for flight detection (km).
     */
    private static final double VERY_LONG_FLIGHT_DISTANCE_KM = 600.0;

    /**
     * Modest speed threshold for very long distance flights (km/h).
     */
    private static final double VERY_LONG_FLIGHT_MIN_SPEED_KMH = 150.0;

    /**
     * Short-haul flight minimum distance (km).
     */
    private static final double SHORT_FLIGHT_MIN_DISTANCE_KM = 350.0;

    /**
     * Short-haul flight maximum distance (km).
     */
    private static final double SHORT_FLIGHT_MAX_DISTANCE_KM = 600.0;

    /**
     * Short-haul flight minimum average speed (km/h).
     */
    private static final double SHORT_FLIGHT_MIN_SPEED_KMH = 110.0;

    /**
     * Train minimum distance threshold (km).
     */
    private static final double TRAIN_MIN_DISTANCE_KM = 100.0;

    /**
     * Train maximum distance threshold (km).
     */
    private static final double TRAIN_MAX_DISTANCE_KM = 1500.0;

    /**
     * Train minimum average speed (km/h).
     */
    private static final double TRAIN_MIN_SPEED_KMH = 50.0;

    /**
     * Train maximum average speed (km/h).
     */
    private static final double TRAIN_MAX_SPEED_KMH = 200.0;

    /**
     * Train minimum duration threshold (hours).
     */
    private static final double TRAIN_MIN_DURATION_HOURS = 1.0;

    /**
     * Get comprehensive classification details for a trip.
     *
     * @param tripId ID of the trip
     * @param userId ID of the requesting user (for ownership validation)
     * @return classification details if trip exists and user owns it
     */
    public Optional<TripClassificationDetailsDTO> getTripClassificationDetails(Long tripId, UUID userId) {
        // Fetch trip by ID
        Optional<TimelineTripEntity> tripOpt = tripRepository.findByIdOptional(tripId);
        if (tripOpt.isEmpty()) {
            log.warn("Trip with ID {} not found", tripId);
            return Optional.empty();
        }

        TimelineTripEntity trip = tripOpt.get();

        // Validate ownership
        if (!trip.getUser().getId().equals(userId)) {
            log.warn("User {} attempted to access trip {} owned by {}",
                    userId, tripId, trip.getUser().getId());
            return Optional.empty();
        }

        // Get user's current configuration
        TimelineConfig config = configProvider.getConfigurationForUser(userId);

        // Build statistics
        TripStatistics statistics = buildTripStatistics(trip);

        // Build thresholds from config
        ClassificationThresholds thresholds = buildThresholds(config);

        String algorithmClassification = travelClassification.classifyTravelType(trip, config).name();
        String movementTypeSource = trip.getMovementTypeSource() != null
                ? trip.getMovementTypeSource().name()
                : MovementTypeSource.AUTO.name();

        // Generate classification steps explanation
        List<ClassificationStep> steps = explainClassification(trip, config, statistics);

        // Generate final reason
        String finalReason = generateFinalReason(
                trip.getMovementType(),
                algorithmClassification,
                movementTypeSource,
                statistics
        );

        TripClassificationDetailsDTO dto = new TripClassificationDetailsDTO(
                trip.getId(),
                trip.getTimestamp(),
                trip.getTripDuration(),
                trip.getDistanceMeters(),
                trip.getMovementType(),
                algorithmClassification,
                movementTypeSource,
                statistics,
                thresholds,
                steps,
                finalReason
        );

        return Optional.of(dto);
    }

    /**
     * Build trip statistics from entity, converting speeds to km/h.
     */
    private TripStatistics buildTripStatistics(TimelineTripEntity trip) {
        Double avgGpsSpeedKmh = trip.getAvgGpsSpeed() != null ? trip.getAvgGpsSpeed() * 3.6 : null;
        Double maxGpsSpeedKmh = trip.getMaxGpsSpeed() != null ? trip.getMaxGpsSpeed() * 3.6 : null;
        Double speedVarianceKmh = trip.getSpeedVariance();

        // Calculate average speed from distance and duration
        double distanceKm = trip.getDistanceMeters() / 1000.0;
        double hours = trip.getTripDuration() / 3600.0;
        Double calculatedAvgSpeedKmh = hours > 0 ? distanceKm / hours : 0.0;

        // Determine GPS reliability (simplified logic from TravelClassification)
        boolean gpsReliable = isGpsReliable(avgGpsSpeedKmh, calculatedAvgSpeedKmh);

        return new TripStatistics(
                avgGpsSpeedKmh,
                maxGpsSpeedKmh,
                calculatedAvgSpeedKmh,
                speedVarianceKmh,
                trip.getLowAccuracyPointsCount(),
                gpsReliable,
                trip.getWaterDistanceMeters(),
                trip.getWaterDistanceRatio(),
                trip.getLongestWaterSegmentMeters(),
                trip.getWaterSampleCount(),
                trip.getWaterEvidenceAvailable()
        );
    }

    /**
     * GPS reliability check - matches TravelClassification.isGpsUnreliable() logic.
     * Returns TRUE if GPS is reliable, FALSE if unreliable.
     * <p>
     * IMPORTANT: This must stay in sync with TravelClassification.isGpsUnreliable()
     * to ensure the classification dialog explanation matches actual classification.
     */
    private boolean isGpsReliable(Double avgGpsSpeedKmh, Double calculatedAvgSpeedKmh) {
        if (avgGpsSpeedKmh == null || calculatedAvgSpeedKmh == null || calculatedAvgSpeedKmh <= 0) {
            return avgGpsSpeedKmh != null;
        }

        // Low speed trips: BIDIRECTIONAL validation
        if (calculatedAvgSpeedKmh < 20.0) {
            // Check 1: GPS too HIGH (exceeds calculated by more than 2x)
            if (avgGpsSpeedKmh > calculatedAvgSpeedKmh * 2.0) {
                return false; // GPS unreliable - too high
            }

            // Check 2: GPS too LOW (less than calculated / 1.3)
            // This matches TravelClassification.LOW_SPEED_GPS_MIN_RELIABILITY_RATIO
            if (avgGpsSpeedKmh < calculatedAvgSpeedKmh / 1.3) {
                return false; // GPS unreliable - too low
            }

            return true; // GPS reliable (within acceptable range)
        }

        // High speed trips: trust GPS
        if (avgGpsSpeedKmh > 200.0) {
            return true;
        }

        // Medium speed: GPS should not differ by more than 50%
        double difference = Math.abs(avgGpsSpeedKmh - calculatedAvgSpeedKmh);
        double percentDiff = difference / calculatedAvgSpeedKmh;
        return percentDiff <= 0.5;
    }

    /**
     * Build configuration thresholds from user config.
     */
    private ClassificationThresholds buildThresholds(TimelineConfig config) {
        return new ClassificationThresholds(
                // WALK
                config.getWalkingMaxAvgSpeed(),
                config.getWalkingMaxMaxSpeed(),

                // Motor vehicle
                isCarEnabled(config),
                isMotorcycleEnabled(config),
                normalizePreferredMotorizedType(config),
                isMotorVehicleEnabled(config) ? config.getCarMinAvgSpeed() : null,
                isMotorVehicleEnabled(config) ? config.getCarMinMaxSpeed() : null,

                // BICYCLE
                config.getBicycleEnabled(),
                config.getBicycleEnabled() ? config.getBicycleMinAvgSpeed() : null,
                config.getBicycleEnabled() ? config.getBicycleMaxAvgSpeed() : null,
                config.getBicycleEnabled() ? config.getBicycleMaxMaxSpeed() : null,

                // RUNNING
                config.getRunningEnabled(),
                config.getRunningEnabled() ? config.getRunningMinAvgSpeed() : null,
                config.getRunningEnabled() ? config.getRunningMaxAvgSpeed() : null,
                config.getRunningEnabled() ? config.getRunningMaxMaxSpeed() : null,

                // TRAIN
                config.getTrainEnabled(),
                config.getTrainEnabled() ? config.getTrainMinAvgSpeed() : null,
                config.getTrainEnabled() ? config.getTrainMaxAvgSpeed() : null,
                config.getTrainEnabled() ? config.getTrainMinMaxSpeed() : null,
                config.getTrainEnabled() ? config.getTrainMaxMaxSpeed() : null,
                config.getTrainEnabled() ? config.getTrainMaxSpeedVariance() : null,

                // FLIGHT
                config.getFlightEnabled(),
                config.getFlightEnabled() ? config.getFlightMinAvgSpeed() : null,
                config.getFlightEnabled() ? config.getFlightMinMaxSpeed() : null,

                // BOAT
                config.getBoatEnabled(),
                config.getBoatEnabled() ? config.getBoatMinWaterRatio() : null,
                config.getBoatEnabled() ? config.getBoatMinWaterDistanceMeters() : null,
                config.getBoatEnabled() ? config.getBoatMinContinuousWaterDistanceMeters() : null,
                config.getBoatEnabled() ? config.getBoatMaxPlausibleSpeed() : null
        );
    }

    /**
     * Generate step-by-step classification explanation.
     * Follows the same priority order as TravelClassification.classifyBySpeed().
     * IMPORTANT: Implements "early stopping" - once a type matches, remaining types are marked as "not checked"
     * because the algorithm stops at the first match.
     *
     * CRITICAL: This method MUST replicate the exact logic from TravelClassification.validateGpsReliability()
     * to ensure the explanation matches the actual classification.
     */
    private List<ClassificationStep> explainClassification(
            TimelineTripEntity trip,
            TimelineConfig config,
            TripStatistics statistics) {

        List<ClassificationStep> steps = new ArrayList<>();

        // Check if this is an inferred trip (no GPS statistics available)
        boolean isInferredTrip = trip.getAvgGpsSpeed() == null && trip.getMaxGpsSpeed() == null;

        // Determine which speeds to use - MUST match TravelClassification.validateGpsReliability() exactly
        double avgSpeedKmh;
        double maxSpeedKmh;
        String speedSource;

        boolean gpsAvgReliable = statistics.gpsReliable() && statistics.avgGpsSpeedKmh() != null;

        if (gpsAvgReliable) {
            // GPS avg is reliable
            avgSpeedKmh = statistics.avgGpsSpeedKmh();

            // Check for max speed noise spikes (matches TravelClassification:463-471)
            if (statistics.maxGpsSpeedKmh() != null &&
                statistics.maxGpsSpeedKmh() > avgSpeedKmh * GPS_MAX_SPEED_NOISE_RATIO) {
                // Max speed spike is unrealistic compared to avg
                maxSpeedKmh = avgSpeedKmh * ESTIMATED_MAX_SPEED_MULTIPLIER;
                speedSource = String.format("Using GPS average speed (%.1f km/h). GPS max (%.1f km/h) was a noise spike, using estimated max (%.1f km/h)",
                        avgSpeedKmh, statistics.maxGpsSpeedKmh(), maxSpeedKmh);
            } else {
                maxSpeedKmh = statistics.maxGpsSpeedKmh() != null ? statistics.maxGpsSpeedKmh() : avgSpeedKmh * ESTIMATED_MAX_SPEED_MULTIPLIER;
                speedSource = String.format("Using GPS average speed (%.1f km/h) for classification", avgSpeedKmh);
            }
        } else {
            // GPS avg is unreliable - check for special case (matches TravelClassification:477-487)
            if (statistics.calculatedAvgSpeedKmh() < MIN_REALISTIC_CALCULATED_SPEED_KMH &&
                statistics.avgGpsSpeedKmh() != null &&
                statistics.avgGpsSpeedKmh() > MIN_GPS_MOVEMENT_SPEED_KMH) {
                // Calculated speed is unrealistically low but GPS shows movement - trust GPS
                avgSpeedKmh = statistics.avgGpsSpeedKmh();
                maxSpeedKmh = statistics.maxGpsSpeedKmh() != null ? statistics.maxGpsSpeedKmh() : avgSpeedKmh * ESTIMATED_MAX_SPEED_MULTIPLIER;
                speedSource = String.format("Calculated speed (%.1f km/h) is unrealistically low but GPS shows movement (%.1f km/h). Trusting GPS.",
                        statistics.calculatedAvgSpeedKmh(), avgSpeedKmh);
            } else {
                // Use calculated avg, but decide about max speed (matches TravelClassification:490-491)
                avgSpeedKmh = statistics.calculatedAvgSpeedKmh();
                maxSpeedKmh = determineMaxSpeed(statistics);
                speedSource = String.format("Using calculated average speed (%.1f km/h) because GPS (%.1f km/h) was unreliable",
                        avgSpeedKmh, statistics.avgGpsSpeedKmh());
            }
        }

        Double speedVariance = statistics.speedVarianceKmh();

        // Track if we found a match (for early stopping)
        boolean foundMatch = false;
        String matchedType = null;

        // Priority 1: FLIGHT
        ClassificationStep flightStep = checkFlight(avgSpeedKmh, maxSpeedKmh,
                trip.getDistanceMeters(), trip.getTripDuration(), isInferredTrip, config, speedSource);
        steps.add(flightStep);
        if (flightStep.checked() && flightStep.passed()) {
            foundMatch = true;
            matchedType = "FLIGHT";
        }

        // Priority 2: BOAT
        if (!foundMatch) {
            ClassificationStep boatStep = checkBoat(avgSpeedKmh, maxSpeedKmh, config, statistics);
            steps.add(boatStep);
            if (boatStep.checked() && boatStep.passed()) {
                foundMatch = true;
                matchedType = "BOAT";
            }
        } else {
            steps.add(createSkippedStep("BOAT", matchedType));
        }

        // Priority 3: TRAIN
        if (!foundMatch) {
            ClassificationStep trainStep = checkTrain(avgSpeedKmh, maxSpeedKmh, speedVariance,
                    trip.getDistanceMeters(), trip.getTripDuration(), isInferredTrip, config, speedSource);
            steps.add(trainStep);
            if (trainStep.checked() && trainStep.passed()) {
                foundMatch = true;
                matchedType = "TRAIN";
            }
        } else {
            steps.add(createSkippedStep("TRAIN", matchedType));
        }

        // Priority 3: BICYCLE
        if (!foundMatch) {
            ClassificationStep bicycleStep = checkBicycle(avgSpeedKmh, maxSpeedKmh, config, speedSource);
            steps.add(bicycleStep);
            if (bicycleStep.checked() && bicycleStep.passed()) {
                foundMatch = true;
                matchedType = "BICYCLE";
            }
        } else {
            steps.add(createSkippedStep("BICYCLE", matchedType));
        }

        // Priority 4: RUNNING
        if (!foundMatch) {
            ClassificationStep runningStep = checkRunning(avgSpeedKmh, maxSpeedKmh, config, speedSource);
            steps.add(runningStep);
            if (runningStep.checked() && runningStep.passed()) {
                foundMatch = true;
                matchedType = "RUNNING";
            }
        } else {
            steps.add(createSkippedStep("RUNNING", matchedType));
        }

        // Priority 5: MOTOR VEHICLE
        if (!foundMatch) {
            ClassificationStep motorVehicleStep = checkMotorVehicle(avgSpeedKmh, maxSpeedKmh, config, speedSource);
            steps.add(motorVehicleStep);
            if (motorVehicleStep.checked() && motorVehicleStep.passed()) {
                foundMatch = true;
                matchedType = motorVehicleStep.tripType();
            }
        } else {
            steps.add(createSkippedStep(resolveMotorizedTripType(config), matchedType));
        }

        // Priority 6: WALK
        if (!foundMatch) {
            ClassificationStep walkStep = checkWalk(avgSpeedKmh, maxSpeedKmh, config, speedSource);
            steps.add(walkStep);
            if (walkStep.checked() && walkStep.passed()) {
                foundMatch = true;
                matchedType = "WALK";
            }
        } else {
            steps.add(createSkippedStep("WALK", matchedType));
        }

        // Priority 7: UNKNOWN (fallback) - only if nothing else matched
        if (!foundMatch) {
            steps.add(checkUnknown());
            matchedType = "UNKNOWN";
        } else {
            steps.add(createSkippedStep("UNKNOWN", matchedType));
        }

        // CRITICAL: Apply post-classification verification (matches TravelClassification.verifyAndCorrectClassification)
        // This can override the initial classification result
        String finalMatchedType = verifyAndCorrectClassification(
                matchedType,
                trip.getDistanceMeters(),
                trip.getTripDuration(),
                config
        );

        // If verification changed the classification, add an explanation step
        if (!finalMatchedType.equals(matchedType)) {
            ClassificationStep verificationStep = new ClassificationStep(
                    finalMatchedType,
                    true,
                    true,
                    String.format("Post-classification verification: Initial classification '%s' was corrected to '%s' " +
                                    "because calculated speed (%.1f km/h) exceeds realistic walking speed (%.1f km/h threshold)",
                            matchedType,
                            finalMatchedType,
                            statistics.calculatedAvgSpeedKmh(),
                            config.getWalkingMaxMaxSpeed() * WALK_VERIFICATION_COEFFICIENT),
                    List.of()
            );
            steps.add(verificationStep);
        }

        return steps;
    }

    /**
     * Creates a "skipped" step for transport types that weren't checked
     * because a higher priority type already matched.
     */
    private ClassificationStep createSkippedStep(String tripType, String matchedType) {
        String reason = String.format("Not checked - higher priority type '%s' already matched", matchedType);
        return new ClassificationStep(tripType, false, false, reason, List.of());
    }

    /**
     * Determines max speed to use when GPS avg is unreliable.
     * Replicates TravelClassification.adjustMaxSpeed() logic.
     *
     * @param statistics trip statistics
     * @return adjusted max speed
     */
    private double determineMaxSpeed(TripStatistics statistics) {
        double maxGpsSpeedKmh = statistics.maxGpsSpeedKmh() != null ? statistics.maxGpsSpeedKmh() : 0.0;
        double calculatedAvgSpeedKmh = statistics.calculatedAvgSpeedKmh();

        // Matches TravelClassification.adjustMaxSpeed():567
        if (maxGpsSpeedKmh > calculatedAvgSpeedKmh * GPS_MAX_SPEED_NOISE_RATIO) {
            // GPS max is too high - likely noise spike
            // Use reasonable estimate instead
            double estimatedMax = calculatedAvgSpeedKmh * ESTIMATED_MAX_SPEED_MULTIPLIER;
            log.debug("GPS max speed ({} km/h) is unreliable ({}x calculated avg {} km/h). Using estimated max: {} km/h",
                    String.format("%.2f", maxGpsSpeedKmh),
                    String.format("%.2f", maxGpsSpeedKmh / calculatedAvgSpeedKmh),
                    String.format("%.2f", calculatedAvgSpeedKmh),
                    String.format("%.2f", estimatedMax));
            return estimatedMax;
        } else {
            // GPS max seems reasonable - keep it
            // This handles: car trip with traffic stops (10 km/h avg, 49 km/h max)
            log.debug("GPS max speed ({} km/h) seems reasonable compared to calculated avg ({} km/h). Keeping GPS max.",
                    String.format("%.2f", maxGpsSpeedKmh),
                    String.format("%.2f", calculatedAvgSpeedKmh));
            return maxGpsSpeedKmh;
        }
    }

    /**
     * Verifies and corrects the trip classification based on realistic speed constraints.
     * Replicates TravelClassification.verifyAndCorrectClassification() logic.
     *
     * If a trip is classified as WALK but the speed is unrealistically high,
     * it's corrected to the preferred motor-vehicle label or UNKNOWN.
     *
     * @param tripType       the initial trip classification
     * @param distanceMeters the total distance of the trip in meters
     * @param tripDuration   the duration of the trip in seconds
     * @param config         the timeline configuration
     * @return the corrected trip type
     */
    private String verifyAndCorrectClassification(String tripType,
                                                  long distanceMeters,
                                                  long tripDuration,
                                                  TimelineConfig config) {
        if ("WALK".equals(tripType)) {
            final double distanceKm = distanceMeters / 1000.0;
            final double hours = tripDuration / 3600.0;
            final double avgSpeedKmh = hours > 0 ? distanceKm / hours : 0.0;

            // If calculated speed exceeds walking threshold with tolerance, re-classify as motor vehicle
            // or UNKNOWN if motor-vehicle labels are disabled.
            // Uses WALK_VERIFICATION_COEFFICIENT (1.2x) to allow for GPS inaccuracies in short trips
            if (avgSpeedKmh > config.getWalkingMaxMaxSpeed() * WALK_VERIFICATION_COEFFICIENT) {
                String fallbackType = isMotorVehicleEnabled(config) ? resolveMotorizedTripType(config) : "UNKNOWN";
                log.debug("Correcting WALK to {} due to unrealistic speed: {} km/h (exceeds {} km/h threshold)",
                        fallbackType,
                        avgSpeedKmh,
                        config.getWalkingMaxMaxSpeed() * WALK_VERIFICATION_COEFFICIENT);
                return fallbackType;
            }
        }
        return tripType;
    }

    private ClassificationStep checkFlight(double avgSpeedKmh, double maxSpeedKmh,
                                           long distanceMeters, long tripDurationSeconds,
                                           boolean isInferredTrip, TimelineConfig config, String speedSource) {
        if (!Boolean.TRUE.equals(config.getFlightEnabled())) {
            return new ClassificationStep("FLIGHT", false, false,
                    "Flight detection is not enabled in configuration", List.of());
        }

        List<ThresholdCheck> checks = new ArrayList<>();
        double distanceKm = distanceMeters / 1000.0;

        // If inferred trip, use distance-based heuristics (matches TravelClassification.classifyWithoutGpsStatistics)
        if (isInferredTrip) {
            // Rule 1: Extreme distance + minimum speed
            boolean extremeDistanceCheck = distanceKm > EXTREME_FLIGHT_DISTANCE_KM && avgSpeedKmh > EXTREME_FLIGHT_MIN_SPEED_KMH;
            checks.add(new ThresholdCheck("Extreme Distance", ">",
                    EXTREME_FLIGHT_DISTANCE_KM, distanceKm, distanceKm > EXTREME_FLIGHT_DISTANCE_KM));
            checks.add(new ThresholdCheck("Minimum Speed (extreme distance)", ">",
                    EXTREME_FLIGHT_MIN_SPEED_KMH, avgSpeedKmh, avgSpeedKmh > EXTREME_FLIGHT_MIN_SPEED_KMH));

            // Rule 2: Long distance + high speed
            boolean longDistanceCheck = distanceKm > LONG_FLIGHT_DISTANCE_KM && avgSpeedKmh > LONG_FLIGHT_MIN_SPEED_KMH;
            checks.add(new ThresholdCheck("Long Distance", ">",
                    LONG_FLIGHT_DISTANCE_KM, distanceKm, distanceKm > LONG_FLIGHT_DISTANCE_KM));
            checks.add(new ThresholdCheck("High Speed (long distance)", ">",
                    LONG_FLIGHT_MIN_SPEED_KMH, avgSpeedKmh, avgSpeedKmh > LONG_FLIGHT_MIN_SPEED_KMH));

            // Rule 2.5: Short-haul flight
            boolean shortFlightCheck = distanceKm > SHORT_FLIGHT_MIN_DISTANCE_KM &&
                    distanceKm <= SHORT_FLIGHT_MAX_DISTANCE_KM &&
                    avgSpeedKmh > SHORT_FLIGHT_MIN_SPEED_KMH;
            checks.add(new ThresholdCheck("Short-haul Distance (min)", ">",
                    SHORT_FLIGHT_MIN_DISTANCE_KM, distanceKm, distanceKm > SHORT_FLIGHT_MIN_DISTANCE_KM));
            checks.add(new ThresholdCheck("Short-haul Distance (max)", "<=",
                    SHORT_FLIGHT_MAX_DISTANCE_KM, distanceKm, distanceKm <= SHORT_FLIGHT_MAX_DISTANCE_KM));
            checks.add(new ThresholdCheck("Short-haul Speed", ">",
                    SHORT_FLIGHT_MIN_SPEED_KMH, avgSpeedKmh, avgSpeedKmh > SHORT_FLIGHT_MIN_SPEED_KMH));

            // Rule 3: Very long distance + modest speed
            boolean veryLongDistanceCheck = distanceKm > VERY_LONG_FLIGHT_DISTANCE_KM && avgSpeedKmh > VERY_LONG_FLIGHT_MIN_SPEED_KMH;
            checks.add(new ThresholdCheck("Very Long Distance", ">",
                    VERY_LONG_FLIGHT_DISTANCE_KM, distanceKm, distanceKm > VERY_LONG_FLIGHT_DISTANCE_KM));
            checks.add(new ThresholdCheck("Modest Speed (very long distance)", ">",
                    VERY_LONG_FLIGHT_MIN_SPEED_KMH, avgSpeedKmh, avgSpeedKmh > VERY_LONG_FLIGHT_MIN_SPEED_KMH));

            boolean passed = extremeDistanceCheck || longDistanceCheck || shortFlightCheck || veryLongDistanceCheck;
            String reason = passed
                    ? String.format("Flight detected (inferred trip): distance %.0f km and speed %.1f km/h match flight patterns. %s",
                    distanceKm, avgSpeedKmh, speedSource)
                    : String.format("Not a flight (inferred trip): distance %.0f km and speed %.1f km/h don't match flight patterns. %s",
                    distanceKm, avgSpeedKmh, speedSource);

            return new ClassificationStep("FLIGHT", true, passed, reason, checks);
        } else {
            // Standard GPS-based classification
            boolean avgCheck = avgSpeedKmh >= config.getFlightMinAvgSpeed();
            boolean maxCheck = maxSpeedKmh >= config.getFlightMinMaxSpeed();

            checks.add(new ThresholdCheck("Average Speed", ">=",
                    config.getFlightMinAvgSpeed(), avgSpeedKmh, avgCheck));
            checks.add(new ThresholdCheck("Max Speed", ">=",
                    config.getFlightMinMaxSpeed(), maxSpeedKmh, maxCheck));

            boolean passed = avgCheck || maxCheck; // OR logic
            String reason = passed
                    ? String.format("Flight detected: average speed (%.1f km/h) OR max speed (%.1f km/h) exceeds flight thresholds. %s",
                    avgSpeedKmh, maxSpeedKmh, speedSource)
                    : String.format("Not a flight: average speed (%.1f km/h) < %.1f km/h AND max speed (%.1f km/h) < %.1f km/h. %s",
                    avgSpeedKmh, config.getFlightMinAvgSpeed(), maxSpeedKmh, config.getFlightMinMaxSpeed(), speedSource);

            return new ClassificationStep("FLIGHT", true, passed, reason, checks);
        }
    }

    private ClassificationStep checkTrain(double avgSpeedKmh, double maxSpeedKmh, Double speedVariance,
                                          long distanceMeters, long tripDurationSeconds,
                                          boolean isInferredTrip, TimelineConfig config, String speedSource) {
        if (!Boolean.TRUE.equals(config.getTrainEnabled())) {
            return new ClassificationStep("TRAIN", false, false,
                    "Train detection is not enabled in configuration", List.of());
        }

        List<ThresholdCheck> checks = new ArrayList<>();
        double distanceKm = distanceMeters / 1000.0;
        double hours = tripDurationSeconds / 3600.0;

        // If inferred trip, use distance-based heuristics (matches TravelClassification.classifyWithoutGpsStatistics)
        if (isInferredTrip) {
            // Distance-based train detection
            boolean distanceMinCheck = distanceKm > TRAIN_MIN_DISTANCE_KM;
            boolean distanceMaxCheck = distanceKm < TRAIN_MAX_DISTANCE_KM;
            boolean speedMinCheck = avgSpeedKmh >= TRAIN_MIN_SPEED_KMH;
            boolean speedMaxCheck = avgSpeedKmh <= TRAIN_MAX_SPEED_KMH;
            boolean durationCheck = hours >= TRAIN_MIN_DURATION_HOURS;

            checks.add(new ThresholdCheck("Distance (min)", ">",
                    TRAIN_MIN_DISTANCE_KM, distanceKm, distanceMinCheck));
            checks.add(new ThresholdCheck("Distance (max)", "<",
                    TRAIN_MAX_DISTANCE_KM, distanceKm, distanceMaxCheck));
            checks.add(new ThresholdCheck("Speed (min)", ">=",
                    TRAIN_MIN_SPEED_KMH, avgSpeedKmh, speedMinCheck));
            checks.add(new ThresholdCheck("Speed (max)", "<=",
                    TRAIN_MAX_SPEED_KMH, avgSpeedKmh, speedMaxCheck));
            checks.add(new ThresholdCheck("Duration (min hours)", ">=",
                    TRAIN_MIN_DURATION_HOURS, hours, durationCheck));

            boolean passed = distanceMinCheck && distanceMaxCheck && speedMinCheck && speedMaxCheck && durationCheck;
            String confidenceLevel = avgSpeedKmh > 150 ? "high-speed rail" : "regional/intercity";
            String reason = passed
                    ? String.format("Train detected (inferred trip - %s): distance %.0f km, speed %.1f km/h, duration %.1f hours. %s",
                    confidenceLevel, distanceKm, avgSpeedKmh, hours, speedSource)
                    : String.format("Not a train (inferred trip): distance/speed/duration outside train patterns. %s", speedSource);

            return new ClassificationStep("TRAIN", true, passed, reason, checks);
        } else {
            // Standard GPS-based classification
            boolean avgMinCheck = avgSpeedKmh >= config.getTrainMinAvgSpeed();
            boolean avgMaxCheck = avgSpeedKmh <= config.getTrainMaxAvgSpeed();
            boolean maxMinCheck = maxSpeedKmh >= config.getTrainMinMaxSpeed();
            boolean maxMaxCheck = maxSpeedKmh <= config.getTrainMaxMaxSpeed();
            boolean varianceCheck = speedVariance != null && speedVariance < config.getTrainMaxSpeedVariance();

            checks.add(new ThresholdCheck("Average Speed (min)", ">=",
                    config.getTrainMinAvgSpeed(), avgSpeedKmh, avgMinCheck));
            checks.add(new ThresholdCheck("Average Speed (max)", "<=",
                    config.getTrainMaxAvgSpeed(), avgSpeedKmh, avgMaxCheck));
            checks.add(new ThresholdCheck("Max Speed (min)", ">=",
                    config.getTrainMinMaxSpeed(), maxSpeedKmh, maxMinCheck));
            checks.add(new ThresholdCheck("Max Speed (max)", "<=",
                    config.getTrainMaxMaxSpeed(), maxSpeedKmh, maxMaxCheck));
            checks.add(new ThresholdCheck("Speed Variance", "<",
                    config.getTrainMaxSpeedVariance(), speedVariance, varianceCheck));

            boolean passed = avgMinCheck && avgMaxCheck && maxMinCheck && maxMaxCheck && varianceCheck;
            String reason = passed
                    ? String.format("Train detected: speed in range (%.1f-%.1f km/h) with low variance (%.1f). %s",
                    avgSpeedKmh, maxSpeedKmh, speedVariance, speedSource)
                    : String.format("Not a train: speeds or variance outside train profile. %s", speedSource);

            return new ClassificationStep("TRAIN", true, passed, reason, checks);
        }
    }

    private ClassificationStep checkBoat(double avgSpeedKmh,
                                         double maxSpeedKmh,
                                         TimelineConfig config,
                                         TripStatistics statistics) {
        if (!Boolean.TRUE.equals(config.getBoatEnabled())) {
            return new ClassificationStep("BOAT", false, false,
                    "Boat detection is not enabled in configuration", List.of());
        }

        double minWaterRatio = config.getBoatMinWaterRatio() != null ? config.getBoatMinWaterRatio() : 0.60;
        double minWaterDistanceMeters = config.getBoatMinWaterDistanceMeters() != null
                ? config.getBoatMinWaterDistanceMeters()
                : 2000.0;
        double minContinuousWaterDistanceMeters = config.getBoatMinContinuousWaterDistanceMeters() != null
                ? config.getBoatMinContinuousWaterDistanceMeters()
                : 1000.0;
        double maxPlausibleSpeed = config.getBoatMaxPlausibleSpeed() != null
                ? config.getBoatMaxPlausibleSpeed()
                : 120.0;

        boolean evidenceCheck = Boolean.TRUE.equals(statistics.waterEvidenceAvailable());
        boolean ratioCheck = statistics.waterDistanceRatio() != null
                && statistics.waterDistanceRatio() >= minWaterRatio;
        boolean distanceCheck = statistics.waterDistanceMeters() != null
                && statistics.waterDistanceMeters() >= minWaterDistanceMeters;
        boolean continuousCheck = statistics.longestWaterSegmentMeters() != null
                && statistics.longestWaterSegmentMeters() >= minContinuousWaterDistanceMeters;
        boolean speedCheck = avgSpeedKmh <= maxPlausibleSpeed && maxSpeedKmh <= maxPlausibleSpeed;

        List<ThresholdCheck> checks = new ArrayList<>();
        checks.add(new ThresholdCheck("Water Evidence Available", "==", 1.0, evidenceCheck ? 1.0 : 0.0, evidenceCheck));
        checks.add(new ThresholdCheck("Water Distance Ratio", ">=",
                minWaterRatio, statistics.waterDistanceRatio(), ratioCheck));
        checks.add(new ThresholdCheck("Water Distance", ">=",
                minWaterDistanceMeters, statistics.waterDistanceMeters(), distanceCheck));
        checks.add(new ThresholdCheck("Continuous Water Distance", ">=",
                minContinuousWaterDistanceMeters, statistics.longestWaterSegmentMeters(), continuousCheck));
        checks.add(new ThresholdCheck("Max Plausible Speed", "<=",
                maxPlausibleSpeed, Math.max(avgSpeedKmh, maxSpeedKmh), speedCheck));

        boolean passed = evidenceCheck && ratioCheck && distanceCheck && continuousCheck && speedCheck;
        String reason = passed
                ? String.format("Boat detected: %.0f m on water, %.0f%% water ratio, and %.0f m continuous water segment.",
                statistics.waterDistanceMeters(), statistics.waterDistanceRatio() * 100.0, statistics.longestWaterSegmentMeters())
                : "Not a boat: water evidence is missing or below the configured sustained-water thresholds.";

        return new ClassificationStep("BOAT", true, passed, reason, checks);
    }

    private ClassificationStep checkBicycle(double avgSpeedKmh, double maxSpeedKmh, TimelineConfig config, String speedSource) {
        if (!Boolean.TRUE.equals(config.getBicycleEnabled())) {
            return new ClassificationStep("BICYCLE", false, false,
                    "Bicycle detection is not enabled in configuration", List.of());
        }

        List<ThresholdCheck> checks = new ArrayList<>();
        boolean avgMinCheck = avgSpeedKmh >= config.getBicycleMinAvgSpeed();
        boolean avgMaxCheck = avgSpeedKmh <= config.getBicycleMaxAvgSpeed();
        boolean maxCheck = maxSpeedKmh <= config.getBicycleMaxMaxSpeed();

        checks.add(new ThresholdCheck("Average Speed (min)", ">=",
                config.getBicycleMinAvgSpeed(), avgSpeedKmh, avgMinCheck));
        checks.add(new ThresholdCheck("Average Speed (max)", "<=",
                config.getBicycleMaxAvgSpeed(), avgSpeedKmh, avgMaxCheck));
        checks.add(new ThresholdCheck("Max Speed", "<=",
                config.getBicycleMaxMaxSpeed(), maxSpeedKmh, maxCheck));

        boolean passed = avgMinCheck && avgMaxCheck && maxCheck;
        String reason = passed
                ? String.format("Bicycle detected: average speed %.1f km/h within bicycle range (%.1f-%.1f km/h). %s",
                avgSpeedKmh, config.getBicycleMinAvgSpeed(), config.getBicycleMaxAvgSpeed(), speedSource)
                : String.format("Not a bicycle: speeds outside bicycle range. %s", speedSource);

        return new ClassificationStep("BICYCLE", true, passed, reason, checks);
    }

    private ClassificationStep checkRunning(double avgSpeedKmh, double maxSpeedKmh, TimelineConfig config, String speedSource) {
        if (!Boolean.TRUE.equals(config.getRunningEnabled())) {
            return new ClassificationStep("RUNNING", false, false,
                    "Running detection is not enabled in configuration", List.of());
        }

        List<ThresholdCheck> checks = new ArrayList<>();
        boolean avgMinCheck = avgSpeedKmh >= config.getRunningMinAvgSpeed();
        boolean avgMaxCheck = avgSpeedKmh <= config.getRunningMaxAvgSpeed();
        boolean maxCheck = maxSpeedKmh <= config.getRunningMaxMaxSpeed();

        checks.add(new ThresholdCheck("Average Speed (min)", ">=",
                config.getRunningMinAvgSpeed(), avgSpeedKmh, avgMinCheck));
        checks.add(new ThresholdCheck("Average Speed (max)", "<=",
                config.getRunningMaxAvgSpeed(), avgSpeedKmh, avgMaxCheck));
        checks.add(new ThresholdCheck("Max Speed", "<=",
                config.getRunningMaxMaxSpeed(), maxSpeedKmh, maxCheck));

        boolean passed = avgMinCheck && avgMaxCheck && maxCheck;
        String reason = passed
                ? String.format("Running detected: average speed %.1f km/h within running range (%.1f-%.1f km/h). %s",
                avgSpeedKmh, config.getRunningMinAvgSpeed(), config.getRunningMaxAvgSpeed(), speedSource)
                : String.format("Not running: speeds outside running range. %s", speedSource);

        return new ClassificationStep("RUNNING", true, passed, reason, checks);
    }

    private ClassificationStep checkMotorVehicle(double avgSpeedKmh, double maxSpeedKmh, TimelineConfig config, String speedSource) {
        String resolvedType = resolveMotorizedTripType(config);
        if (!isMotorVehicleEnabled(config)) {
            return new ClassificationStep(resolvedType, false, false,
                    "Motor vehicle detection is not enabled in configuration", List.of());
        }

        List<ThresholdCheck> checks = new ArrayList<>();
        boolean avgCheck = avgSpeedKmh >= config.getCarMinAvgSpeed();
        boolean maxCheck = maxSpeedKmh >= config.getCarMinMaxSpeed();

        checks.add(new ThresholdCheck("Average Speed", ">=",
                config.getCarMinAvgSpeed(), avgSpeedKmh, avgCheck));
        checks.add(new ThresholdCheck("Max Speed", ">=",
                config.getCarMinMaxSpeed(), maxSpeedKmh, maxCheck));

        boolean passed = avgCheck || maxCheck; // OR logic
        String reason = passed
                ? String.format("Motor vehicle detected and labeled as %s: average speed (%.1f km/h) OR max speed (%.1f km/h) exceeds motor vehicle thresholds. %s",
                resolvedType, avgSpeedKmh, maxSpeedKmh, speedSource)
                : String.format("Not a motor vehicle: average speed (%.1f km/h) < %.1f km/h AND max speed (%.1f km/h) < %.1f km/h. %s",
                avgSpeedKmh, config.getCarMinAvgSpeed(), maxSpeedKmh, config.getCarMinMaxSpeed(), speedSource);

        return new ClassificationStep(resolvedType, true, passed, reason, checks);
    }

    private boolean isCarEnabled(TimelineConfig config) {
        return !Boolean.FALSE.equals(config.getCarEnabled());
    }

    private boolean isMotorcycleEnabled(TimelineConfig config) {
        return Boolean.TRUE.equals(config.getMotorcycleEnabled());
    }

    private boolean isMotorVehicleEnabled(TimelineConfig config) {
        return isCarEnabled(config) || isMotorcycleEnabled(config);
    }

    private String normalizePreferredMotorizedType(TimelineConfig config) {
        return "MOTORCYCLE".equalsIgnoreCase(config.getPreferredMotorizedType()) ? "MOTORCYCLE" : "CAR";
    }

    private String resolveMotorizedTripType(TimelineConfig config) {
        boolean carEnabled = isCarEnabled(config);
        boolean motorcycleEnabled = isMotorcycleEnabled(config);

        if (carEnabled && motorcycleEnabled) {
            return normalizePreferredMotorizedType(config);
        }
        if (motorcycleEnabled) {
            return "MOTORCYCLE";
        }
        return carEnabled ? "CAR" : "CAR";
    }

    private ClassificationStep checkWalk(double avgSpeedKmh, double maxSpeedKmh, TimelineConfig config, String speedSource) {
        List<ThresholdCheck> checks = new ArrayList<>();
        boolean avgCheck = avgSpeedKmh <= config.getWalkingMaxAvgSpeed();
        boolean maxCheck = maxSpeedKmh <= config.getWalkingMaxMaxSpeed();

        checks.add(new ThresholdCheck("Average Speed", "<=",
                config.getWalkingMaxAvgSpeed(), avgSpeedKmh, avgCheck));
        checks.add(new ThresholdCheck("Max Speed", "<=",
                config.getWalkingMaxMaxSpeed(), maxSpeedKmh, maxCheck));

        boolean passed = avgCheck && maxCheck; // AND logic
        String reason = passed
                ? String.format("Walk detected: average speed %.1f km/h and max speed %.1f km/h both within walking limits. %s",
                avgSpeedKmh, maxSpeedKmh, speedSource)
                : String.format("Not a walk: speeds exceed walking limits. %s", speedSource);

        return new ClassificationStep("WALK", true, passed, reason, checks);
    }

    private ClassificationStep checkUnknown() {
        return new ClassificationStep("UNKNOWN", true, true,
                "Fallback classification when no other type matches", List.of());
    }

    /**
     * Generate a user-friendly final reason for the classification.
     */
    private String generateFinalReason(String effectiveClassification,
                                       String algorithmClassification,
                                       String movementTypeSource,
                                       TripStatistics statistics) {
        double avgSpeed = statistics.gpsReliable() && statistics.avgGpsSpeedKmh() != null
                ? statistics.avgGpsSpeedKmh()
                : statistics.calculatedAvgSpeedKmh();

        String autoReason = switch (algorithmClassification) {
            case "FLIGHT" -> String.format("Automatic classification was FLIGHT because speed (%.1f km/h) matches air travel.", avgSpeed);
            case "TRAIN" -> String.format("Automatic classification was TRAIN due to sustained high speeds (%.1f km/h) and consistency.", avgSpeed);
            case "BOAT" -> String.format("Automatic classification was BOAT because the GPS path has sustained water evidence (%.0f m on water, %.0f%% water ratio).",
                    statistics.waterDistanceMeters() != null ? statistics.waterDistanceMeters() : 0.0,
                    statistics.waterDistanceRatio() != null ? statistics.waterDistanceRatio() * 100.0 : 0.0);
            case "BICYCLE" -> String.format("Automatic classification was BICYCLE based on moderate speeds (%.1f km/h).", avgSpeed);
            case "RUNNING" -> String.format("Automatic classification was RUNNING based on speeds (%.1f km/h) consistent with running.", avgSpeed);
            case "CAR" -> String.format("Automatic classification was CAR due to motor-vehicle speed profile (avg %.1f km/h).", avgSpeed);
            case "MOTORCYCLE" -> String.format("Automatic classification was MOTORCYCLE due to motor-vehicle speed profile (avg %.1f km/h).", avgSpeed);
            case "WALK" -> String.format("Automatic classification was WALK based on low speeds (%.1f km/h).", avgSpeed);
            default -> "Automatic classification resulted in UNKNOWN.";
        };

        if (MovementTypeSource.MANUAL.name().equals(movementTypeSource)) {
            return String.format(
                    "Manual override is active: effective classification is %s. %s",
                    effectiveClassification,
                    autoReason
            );
        }

        return autoReason;
    }
}
