package at.procon.eventhub.tachographfilesession.service;

import at.procon.eventhub.config.EventHubProperties;
import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto;
import at.procon.eventhub.tachographfilesession.model.*;
import java.time.Duration;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import org.springframework.stereotype.Service;

@Service
public class TachographEsperProcessingCore {

    private final DriverTimelineBuilder driverTimelineBuilder;
    private final DriverTimelineReusableProjectionBuilder reusableProjectionBuilder;
    private final EventHubProperties properties;

    public TachographEsperProcessingCore(
            DriverTimelineBuilder driverTimelineBuilder,
            DriverTimelineReusableProjectionBuilder reusableProjectionBuilder,
            EventHubProperties properties
    ) {
        this.driverTimelineBuilder = driverTimelineBuilder;
        this.reusableProjectionBuilder = reusableProjectionBuilder;
        this.properties = properties;
    }

    public TachographEsperDriverProcessingResultDto process(TachographEsperProcessingInput input) {
        Objects.requireNonNull(input, "input must not be null");
        ResolvedDriverTimeline timeline = Objects.requireNonNull(input.timeline(), "timeline must not be null");
        String driverKey = input.driverKey();
        OffsetDateTime requestedFrom = input.requestedFrom() == null ? timeline.loadedFrom() : utc(input.requestedFrom());
        OffsetDateTime requestedTo = input.requestedTo() == null ? timeline.loadedTo() : utc(input.requestedTo());
        if (requestedFrom != null && requestedTo != null && requestedTo.isBefore(requestedFrom)) {
            throw new IllegalArgumentException("occurredTo must not be before occurredFrom.");
        }
        int significantDrivingMinutes = Math.max(1, input.significantDrivingMinutes());
        int minimumRestPeriodMinutes = Math.max(1, input.minimumRestPeriodMinutes());

        List<TachographEsperActivityIntervalEvent> activityIntervals = clipEsperActivityIntervalEvents(
                driverTimelineBuilder.buildEsperActivityIntervalEvents(input.sessionId(), driverKey, timeline),
                requestedFrom,
                requestedTo
        );
        List<TachographEsperActivityIntervalEvent> drivingIntervals = clipEsperActivityIntervalEvents(
                driverTimelineBuilder.buildEsperDrivingIntervalEvents(input.sessionId(), driverKey, timeline),
                requestedFrom,
                requestedTo
        );

        TachographEsperDrivingDerivedProjectionBundle derivedProjectionBundle = buildDerivedProjection(
                input,
                timeline,
                significantDrivingMinutes,
                minimumRestPeriodMinutes
        );

        List<TachographEsperDrivingInterruptionIntervalEvent> rawDrivingInterruptionIntervals =
                derivedProjectionBundle.drivingInterruptionIntervals();
        List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionIntervals =
                clipEsperDrivingInterruptionIntervalEvents(rawDrivingInterruptionIntervals, requestedFrom, requestedTo);
        List<TachographEsperDrivingInterruptionIntervalEvent> rawDailyWeeklyRestCandidateIntervals =
                derivedProjectionBundle.dailyWeeklyRestCandidateIntervals();
        List<TachographEsperDrivingInterruptionIntervalEvent> dailyWeeklyRestCandidateIntervals =
                clipEsperDrivingInterruptionIntervalEvents(rawDailyWeeklyRestCandidateIntervals, requestedFrom, requestedTo);
        List<TachographEsperDrivingInterruptionIntervalEvent> rawDrivingInterruptionVehicleChangeIntervals =
                derivedProjectionBundle.drivingInterruptionVehicleChangeIntervals();
        List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionVehicleChangeIntervals =
                clipEsperDrivingInterruptionIntervalEvents(rawDrivingInterruptionVehicleChangeIntervals, requestedFrom, requestedTo);

        List<TachographEsperVehicleUsageIntervalEvent> rawVehicleUsageIntervals =
                driverTimelineBuilder.buildEsperVehicleUsageIntervalEvents(timeline);
        List<TachographEsperVuCardAbsentIntervalEvent> rawVuCardAbsentIntervals =
                derivedProjectionBundle.vuCardAbsentIntervals();

        List<TachographEsperPotentialHomeOvernightStayIntervalEvent> potentialHomeOvernightStayIntervals =
                clipEsperPotentialHomeOvernightStayIntervalEvents(
                        derivedProjectionBundle.potentialHomeOvernightStayIntervals(),
                        rawVuCardAbsentIntervals,
                        rawVehicleUsageIntervals,
                        requestedFrom,
                        requestedTo
                );
        List<TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent> dailyWeeklyRestCandidateCoverageIntervals =
                clipEsperDailyWeeklyRestCandidateCoverageIntervalEvents(
                        derivedProjectionBundle.dailyWeeklyRestCandidateCoverageIntervals(),
                        rawVuCardAbsentIntervals,
                        rawVehicleUsageIntervals,
                        requestedFrom,
                        requestedTo
                );
        List<TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent> unclassifiedDailyWeeklyRestCandidateCoverageIntervals =
                clipEsperDailyWeeklyRestCandidateCoverageIntervalEvents(
                        derivedProjectionBundle.unclassifiedDailyWeeklyRestCandidateCoverageIntervals(),
                        rawVuCardAbsentIntervals,
                        rawVehicleUsageIntervals,
                        requestedFrom,
                        requestedTo
                );
        List<TachographEsperPotentialInVehicleOvernightStayIntervalEvent> potentialInVehicleOvernightStayIntervals =
                clipEsperPotentialInVehicleOvernightStayIntervalEvents(
                        derivedProjectionBundle.potentialInVehicleOvernightStayIntervals(),
                        rawVuCardAbsentIntervals,
                        rawVehicleUsageIntervals,
                        requestedFrom,
                        requestedTo
                );
        List<TachographEsperPotentialInVehicleTripIntervalEvent> potentialInVehicleTripIntervals =
                clipEsperPotentialInVehicleTripIntervalEvents(
                        derivedProjectionBundle.potentialInVehicleTripIntervals(),
                        potentialInVehicleOvernightStayIntervals,
                        requestedFrom,
                        requestedTo
                );
        List<TachographEsperVehicleUsageIntervalEvent> vehicleUsageIntervals = clipEsperVehicleUsageIntervalEvents(
                rawVehicleUsageIntervals,
                requestedFrom,
                requestedTo
        );
        List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals = clipEsperVuCardAbsentIntervalEvents(
                rawVuCardAbsentIntervals,
                requestedFrom,
                requestedTo
        );
        List<TachographEsperSupportGeoEvent> supportGeoEvents = clipEsperSupportGeoEvents(
                timeline.supportEvents(),
                driverKey,
                requestedFrom,
                requestedTo
        );

        return new TachographEsperDriverProcessingResultDto(
                input.sessionId(),
                driverKey,
                timeline.sourceKind(),
                timeline.loadedFrom(),
                timeline.loadedTo(),
                requestedFrom,
                requestedTo,
                activityIntervals.size(),
                drivingIntervals.size(),
                drivingInterruptionIntervals.size(),
                drivingInterruptionVehicleChangeIntervals.size(),
                dailyWeeklyRestCandidateIntervals.size(),
                dailyWeeklyRestCandidateCoverageIntervals.size(),
                unclassifiedDailyWeeklyRestCandidateCoverageIntervals.size(),
                potentialHomeOvernightStayIntervals.size(),
                potentialInVehicleOvernightStayIntervals.size(),
                potentialInVehicleTripIntervals.size(),
                vehicleUsageIntervals.size(),
                vuCardAbsentIntervals.size(),
                supportGeoEvents.size(),
                activityIntervals,
                drivingIntervals,
                drivingInterruptionIntervals,
                drivingInterruptionVehicleChangeIntervals,
                dailyWeeklyRestCandidateIntervals,
                dailyWeeklyRestCandidateCoverageIntervals,
                unclassifiedDailyWeeklyRestCandidateCoverageIntervals,
                potentialHomeOvernightStayIntervals,
                potentialInVehicleOvernightStayIntervals,
                potentialInVehicleTripIntervals,
                vehicleUsageIntervals,
                vuCardAbsentIntervals,
                supportGeoEvents,
                combinedNotes(input.notes())
        );
    }

    private TachographEsperDrivingDerivedProjectionBundle buildDerivedProjection(
            TachographEsperProcessingInput input,
            ResolvedDriverTimeline timeline,
            int significantDrivingMinutes,
            int minimumRestPeriodMinutes
    ) {
        if (input.forceEventInput() || input.hasEventInputEvents()) {
            return reusableProjectionBuilder.buildEsperDrivingDerivedProjectionBundleFromEvents(
                    input.sessionId(),
                    input.driverKey(),
                    input.eventInputEvents(),
                    significantDrivingMinutes,
                    minimumRestPeriodMinutes
            );
        }
        if (properties.getTachographFileSession().getProcessing().getDrivingDerivedProjectionInputMode()
                == EventHubProperties.DrivingDerivedProjectionInputMode.EVENTS
                && input.session() != null
                && input.driverSession() != null) {
            return reusableProjectionBuilder.buildEsperDrivingDerivedProjectionBundle(
                    input.session(),
                    input.driverSession(),
                    significantDrivingMinutes,
                    minimumRestPeriodMinutes
            );
        }
        return reusableProjectionBuilder.buildEsperDrivingDerivedProjectionBundle(
                input.sessionId(),
                input.driverKey(),
                timeline,
                significantDrivingMinutes,
                minimumRestPeriodMinutes
        );
    }

    private List<String> combinedNotes(List<String> extraNotes) {
        List<String> notes = new ArrayList<>();
        notes.addAll(esperProjectionNotes());
        if (extraNotes != null) {
            notes.addAll(extraNotes);
        }
        return List.copyOf(notes);
    }


    private List<TachographEsperActivityIntervalEvent> clipEsperActivityIntervalEvents(
            List<TachographEsperActivityIntervalEvent> intervals,
            OffsetDateTime requestedFrom,
            OffsetDateTime requestedTo
    ) {
        if (requestedFrom == null || requestedTo == null) {
            return List.of();
        }
        return intervals.stream()
                .map(interval -> {
                    OffsetDateTime start = max(interval.startedAt(), requestedFrom);
                    OffsetDateTime end = min(interval.endedAt(), requestedTo);
                    if (!end.isAfter(start)) {
                        return null;
                    }
                    boolean clipped = interval.clippedToRequestedPeriod()
                            || !start.equals(interval.startedAt())
                            || !end.equals(interval.endedAt());
                    return new TachographEsperActivityIntervalEvent(
                            interval.sessionId(),
                            interval.driverKey(),
                            interval.intervalId(),
                            interval.activityType(),
                            interval.cardSlot(),
                            interval.cardStatus(),
                            interval.drivingStatus(),
                            interval.registrationKey(),
                            interval.vehicleKey(),
                            interval.sourceKind(),
                            start,
                            end,
                            Duration.between(start, end).getSeconds(),
                            interval.sourceIntervalIds(),
                            interval.synthetic(),
                            clipped,
                            interval.level()
                    );
                })
                .filter(Objects::nonNull)
                .sorted(Comparator.comparing(TachographEsperActivityIntervalEvent::startedAt)
                        .thenComparing(TachographEsperActivityIntervalEvent::endedAt)
                        .thenComparing(TachographEsperActivityIntervalEvent::activityType, Comparator.nullsLast(String::compareTo)))
                .toList();
    }

    private List<TachographEsperVehicleUsageIntervalEvent> clipEsperVehicleUsageIntervalEvents(
            List<TachographEsperVehicleUsageIntervalEvent> intervals,
            OffsetDateTime requestedFrom,
            OffsetDateTime requestedTo
    ) {
        if (requestedFrom == null || requestedTo == null) {
            return List.of();
        }
        return intervals.stream()
                .map(interval -> {
                    OffsetDateTime start = max(interval.startedAt(), requestedFrom);
                    OffsetDateTime end = min(interval.endedAt(), requestedTo);
                    if (!end.isAfter(start)) {
                        return null;
                    }
                    boolean startClipped = !start.equals(interval.startedAt());
                    boolean endClipped = !end.equals(interval.endedAt());
                    return new TachographEsperVehicleUsageIntervalEvent(
                            interval.sessionId(),
                            interval.driverKey(),
                            interval.intervalId(),
                            start,
                            end,
                            Duration.between(start, end).getSeconds(),
                            startClipped ? null : interval.odometerBeginKm(),
                            endClipped ? null : interval.odometerEndKm(),
                            interval.registrationKey(),
                            interval.vehicleKey(),
                            interval.sourceKind(),
                            interval.sourceIntervalIds()
                    );
                })
                .filter(Objects::nonNull)
                .sorted(Comparator.comparing(TachographEsperVehicleUsageIntervalEvent::startedAt)
                        .thenComparing(TachographEsperVehicleUsageIntervalEvent::endedAt)
                        .thenComparing(TachographEsperVehicleUsageIntervalEvent::intervalId, Comparator.nullsLast(String::compareTo)))
                .toList();
    }

    private List<TachographEsperSupportGeoEvent> clipEsperSupportGeoEvents(
            List<ExtractedSupportEvent> supportEvents,
            String driverKey,
            OffsetDateTime requestedFrom,
            OffsetDateTime requestedTo
    ) {
        if (supportEvents == null || supportEvents.isEmpty() || requestedFrom == null || requestedTo == null) {
            return List.of();
        }
        return supportEvents.stream()
                .filter(event -> event.driverKey() == null || Objects.equals(driverKey, event.driverKey()))
                .filter(event -> event.occurredAt() != null)
                .filter(event -> event.latitude() != null && event.longitude() != null)
                .filter(event -> !event.occurredAt().isBefore(requestedFrom) && !event.occurredAt().isAfter(requestedTo))
                .map(event -> new TachographEsperSupportGeoEvent(
                        event.eventId(),
                        event.driverKey(),
                        event.occurredAt(),
                        event.eventDomain(),
                        event.eventType(),
                        event.eventLifecycle(),
                        event.registrationKey(),
                        event.vehicleKey(),
                        event.country(),
                        event.region(),
                        event.countryFrom(),
                        event.countryTo(),
                        event.operation(),
                        event.latitude(),
                        event.longitude(),
                        event.odometerKm(),
                        event.rawRecordPath()
                ))
                .sorted(Comparator.comparing(TachographEsperSupportGeoEvent::occurredAt)
                        .thenComparing(TachographEsperSupportGeoEvent::eventDomain, Comparator.nullsLast(String::compareTo))
                        .thenComparing(TachographEsperSupportGeoEvent::eventId, Comparator.nullsLast(String::compareTo)))
                .toList();
    }

    private List<TachographEsperDrivingInterruptionIntervalEvent> clipEsperDrivingInterruptionIntervalEvents(
            List<TachographEsperDrivingInterruptionIntervalEvent> intervals,
            OffsetDateTime requestedFrom,
            OffsetDateTime requestedTo
    ) {
        if (requestedFrom == null || requestedTo == null) {
            return List.of();
        }
        return intervals.stream()
                .map(interval -> {
                    OffsetDateTime start = max(interval.startedAt(), requestedFrom);
                    OffsetDateTime end = min(interval.endedAt(), requestedTo);
                    if (!end.isAfter(start)) {
                        return null;
                    }
                    return new TachographEsperDrivingInterruptionIntervalEvent(
                            interval.sessionId(),
                            interval.driverKey(),
                            start,
                            end,
                            Duration.between(start, end).getSeconds(),
                            interval.previousDrivingSourceIntervalId(),
                            interval.nextDrivingSourceIntervalId(),
                            interval.previousRegistrationKey(),
                            interval.nextRegistrationKey(),
                            interval.previousVehicleKey(),
                            interval.nextVehicleKey()
                    );
                })
                .filter(Objects::nonNull)
                .sorted(Comparator.comparing(TachographEsperDrivingInterruptionIntervalEvent::startedAt)
                        .thenComparing(TachographEsperDrivingInterruptionIntervalEvent::endedAt))
                .toList();
    }

    private List<TachographEsperVuCardAbsentIntervalEvent> clipEsperVuCardAbsentIntervalEvents(
            List<TachographEsperVuCardAbsentIntervalEvent> intervals,
            OffsetDateTime requestedFrom,
            OffsetDateTime requestedTo
    ) {
        if (requestedFrom == null || requestedTo == null) {
            return List.of();
        }
        return intervals.stream()
                .map(interval -> {
                    OffsetDateTime start = max(interval.startedAt(), requestedFrom);
                    OffsetDateTime end = min(interval.endedAt(), requestedTo);
                    if (!end.isAfter(start)) {
                        return null;
                    }
                    return new TachographEsperVuCardAbsentIntervalEvent(
                            interval.sessionId(),
                            interval.driverKey(),
                            start,
                            end,
                            Duration.between(start, end).getSeconds(),
                            interval.previousUsageIntervalId(),
                            interval.nextUsageIntervalId(),
                            interval.previousRegistrationKey(),
                            interval.nextRegistrationKey(),
                            interval.previousVehicleKey(),
                            interval.nextVehicleKey()
                    );
                })
                .filter(Objects::nonNull)
                .sorted(Comparator.comparing(TachographEsperVuCardAbsentIntervalEvent::startedAt)
                        .thenComparing(TachographEsperVuCardAbsentIntervalEvent::endedAt))
                .toList();
    }

    private List<TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent> clipEsperDailyWeeklyRestCandidateCoverageIntervalEvents(
            List<TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent> intervals,
            List<TachographEsperVuCardAbsentIntervalEvent> rawVuCardAbsentIntervals,
            List<TachographEsperVehicleUsageIntervalEvent> rawVehicleUsageIntervals,
            OffsetDateTime requestedFrom,
            OffsetDateTime requestedTo
    ) {
        if (requestedFrom == null || requestedTo == null) {
            return List.of();
        }
        return intervals.stream()
                .map(interval -> {
                    OffsetDateTime start = max(interval.startedAt(), requestedFrom);
                    OffsetDateTime end = min(interval.endedAt(), requestedTo);
                    if (!end.isAfter(start)) {
                        return null;
                    }
                    long durationSeconds = Duration.between(start, end).getSeconds();
                    boolean beginBoundaryChanged = !start.equals(interval.startedAt());
                    boolean endBoundaryChanged = !end.equals(interval.endedAt());
                    return new TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent(
                            interval.sessionId(),
                            interval.driverKey(),
                            start,
                            end,
                            durationSeconds,
                            interval.cardAbsentDurationSeconds(),
                            interval.cardAbsentCoveragePercent(),
                            interval.previousDrivingSourceIntervalId(),
                            interval.nextDrivingSourceIntervalId(),
                            interval.previousRegistrationKey(),
                            interval.nextRegistrationKey(),
                            interval.previousVehicleKey(),
                            interval.nextVehicleKey(),
                            beginBoundaryChanged ? null : interval.beginBoundaryOdometerKm(),
                            endBoundaryChanged ? null : interval.endBoundaryOdometerKm(),
                            beginBoundaryChanged ? null : interval.beginGeoEventId(),
                            beginBoundaryChanged ? null : interval.beginGeoEventDomain(),
                            beginBoundaryChanged ? null : interval.beginGeoOccurredAt(),
                            beginBoundaryChanged ? null : interval.beginLatitude(),
                            beginBoundaryChanged ? null : interval.beginLongitude(),
                            beginBoundaryChanged ? null : interval.beginGeoDistanceSeconds(),
                            beginBoundaryChanged ? null : interval.beginGeoOdometerKm(),
                            endBoundaryChanged ? null : interval.endGeoEventId(),
                            endBoundaryChanged ? null : interval.endGeoEventDomain(),
                            endBoundaryChanged ? null : interval.endGeoOccurredAt(),
                            endBoundaryChanged ? null : interval.endLatitude(),
                            endBoundaryChanged ? null : interval.endLongitude(),
                            endBoundaryChanged ? null : interval.endGeoDistanceSeconds(),
                            endBoundaryChanged ? null : interval.endGeoOdometerKm(),
                            beginBoundaryChanged || endBoundaryChanged ? null : interval.geoEvidenceMovementMeters(),
                            beginBoundaryChanged || endBoundaryChanged ? "UNKNOWN" : interval.geoEvidenceMovementCategory(),
                            beginBoundaryChanged ? null : geoEvidenceEvent(interval.beginGeoEventId(), interval.beginGeoEventDomain(), interval.beginGeoOccurredAt(), interval.beginLatitude(), interval.beginLongitude(), interval.beginGeoDistanceSeconds(), interval.beginGeoOdometerKm()),
                            endBoundaryChanged ? null : geoEvidenceEvent(interval.endGeoEventId(), interval.endGeoEventDomain(), interval.endGeoOccurredAt(), interval.endLatitude(), interval.endLongitude(), interval.endGeoDistanceSeconds(), interval.endGeoOdometerKm())
                    );
                })
                .filter(Objects::nonNull)
                .sorted(Comparator.comparing(TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent::startedAt)
                        .thenComparing(TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent::endedAt))
                .toList();
    }

    private List<TachographEsperPotentialHomeOvernightStayIntervalEvent> clipEsperPotentialHomeOvernightStayIntervalEvents(
            List<TachographEsperPotentialHomeOvernightStayIntervalEvent> intervals,
            List<TachographEsperVuCardAbsentIntervalEvent> rawVuCardAbsentIntervals,
            List<TachographEsperVehicleUsageIntervalEvent> rawVehicleUsageIntervals,
            OffsetDateTime requestedFrom,
            OffsetDateTime requestedTo
    ) {
        if (requestedFrom == null || requestedTo == null) {
            return List.of();
        }
        return intervals.stream()
                .map(interval -> {
                    OffsetDateTime start = max(interval.startedAt(), requestedFrom);
                    OffsetDateTime end = min(interval.endedAt(), requestedTo);
                    if (!end.isAfter(start)) {
                        return null;
                    }
                    long durationSeconds = Duration.between(start, end).getSeconds();
                    boolean beginBoundaryChanged = !start.equals(interval.startedAt());
                    boolean endBoundaryChanged = !end.equals(interval.endedAt());
                    return new TachographEsperPotentialHomeOvernightStayIntervalEvent(
                            interval.sessionId(),
                            interval.driverKey(),
                            start,
                            end,
                            durationSeconds,
                            interval.cardAbsentDurationSeconds(),
                            interval.cardAbsentCoveragePercent(),
                            interval.previousDrivingSourceIntervalId(),
                            interval.nextDrivingSourceIntervalId(),
                            interval.previousRegistrationKey(),
                            interval.nextRegistrationKey(),
                            interval.previousVehicleKey(),
                            interval.nextVehicleKey(),
                            beginBoundaryChanged ? null : interval.beginBoundaryOdometerKm(),
                            endBoundaryChanged ? null : interval.endBoundaryOdometerKm(),
                            beginBoundaryChanged ? null : interval.beginGeoEventId(),
                            beginBoundaryChanged ? null : interval.beginGeoEventDomain(),
                            beginBoundaryChanged ? null : interval.beginGeoOccurredAt(),
                            beginBoundaryChanged ? null : interval.beginLatitude(),
                            beginBoundaryChanged ? null : interval.beginLongitude(),
                            beginBoundaryChanged ? null : interval.beginGeoDistanceSeconds(),
                            beginBoundaryChanged ? null : interval.beginGeoOdometerKm(),
                            endBoundaryChanged ? null : interval.endGeoEventId(),
                            endBoundaryChanged ? null : interval.endGeoEventDomain(),
                            endBoundaryChanged ? null : interval.endGeoOccurredAt(),
                            endBoundaryChanged ? null : interval.endLatitude(),
                            endBoundaryChanged ? null : interval.endLongitude(),
                            endBoundaryChanged ? null : interval.endGeoDistanceSeconds(),
                            endBoundaryChanged ? null : interval.endGeoOdometerKm(),
                            beginBoundaryChanged || endBoundaryChanged ? null : interval.geoEvidenceMovementMeters(),
                            beginBoundaryChanged || endBoundaryChanged ? "UNKNOWN" : interval.geoEvidenceMovementCategory(),
                            beginBoundaryChanged ? null : geoEvidenceEvent(interval.beginGeoEventId(), interval.beginGeoEventDomain(), interval.beginGeoOccurredAt(), interval.beginLatitude(), interval.beginLongitude(), interval.beginGeoDistanceSeconds(), interval.beginGeoOdometerKm()),
                            endBoundaryChanged ? null : geoEvidenceEvent(interval.endGeoEventId(), interval.endGeoEventDomain(), interval.endGeoOccurredAt(), interval.endLatitude(), interval.endLongitude(), interval.endGeoDistanceSeconds(), interval.endGeoOdometerKm())
                    );
                })
                .filter(Objects::nonNull)
                .sorted(Comparator.comparing(TachographEsperPotentialHomeOvernightStayIntervalEvent::startedAt)
                        .thenComparing(TachographEsperPotentialHomeOvernightStayIntervalEvent::endedAt))
                .toList();
    }

    private List<TachographEsperPotentialInVehicleOvernightStayIntervalEvent> clipEsperPotentialInVehicleOvernightStayIntervalEvents(
            List<TachographEsperPotentialInVehicleOvernightStayIntervalEvent> intervals,
            List<TachographEsperVuCardAbsentIntervalEvent> rawVuCardAbsentIntervals,
            List<TachographEsperVehicleUsageIntervalEvent> rawVehicleUsageIntervals,
            OffsetDateTime requestedFrom,
            OffsetDateTime requestedTo
    ) {
        if (requestedFrom == null || requestedTo == null) {
            return List.of();
        }
        return intervals.stream()
                .map(interval -> {
                    OffsetDateTime start = max(interval.startedAt(), requestedFrom);
                    OffsetDateTime end = min(interval.endedAt(), requestedTo);
                    if (!end.isAfter(start)) {
                        return null;
                    }
                    long durationSeconds = Duration.between(start, end).getSeconds();
                    boolean beginBoundaryChanged = !start.equals(interval.startedAt());
                    boolean endBoundaryChanged = !end.equals(interval.endedAt());
                    return new TachographEsperPotentialInVehicleOvernightStayIntervalEvent(
                            interval.sessionId(),
                            interval.driverKey(),
                            start,
                            end,
                            durationSeconds,
                            interval.cardAbsentDurationSeconds(),
                            interval.cardAbsentCoveragePercent(),
                            interval.previousDrivingSourceIntervalId(),
                            interval.nextDrivingSourceIntervalId(),
                            interval.previousRegistrationKey(),
                            interval.nextRegistrationKey(),
                            interval.previousVehicleKey(),
                            interval.nextVehicleKey(),
                            beginBoundaryChanged ? null : interval.beginBoundaryOdometerKm(),
                            endBoundaryChanged ? null : interval.endBoundaryOdometerKm(),
                            beginBoundaryChanged ? null : interval.beginGeoEventId(),
                            beginBoundaryChanged ? null : interval.beginGeoEventDomain(),
                            beginBoundaryChanged ? null : interval.beginGeoOccurredAt(),
                            beginBoundaryChanged ? null : interval.beginLatitude(),
                            beginBoundaryChanged ? null : interval.beginLongitude(),
                            beginBoundaryChanged ? null : interval.beginGeoDistanceSeconds(),
                            beginBoundaryChanged ? null : interval.beginGeoOdometerKm(),
                            endBoundaryChanged ? null : interval.endGeoEventId(),
                            endBoundaryChanged ? null : interval.endGeoEventDomain(),
                            endBoundaryChanged ? null : interval.endGeoOccurredAt(),
                            endBoundaryChanged ? null : interval.endLatitude(),
                            endBoundaryChanged ? null : interval.endLongitude(),
                            endBoundaryChanged ? null : interval.endGeoDistanceSeconds(),
                            endBoundaryChanged ? null : interval.endGeoOdometerKm(),
                            beginBoundaryChanged || endBoundaryChanged ? null : interval.geoEvidenceMovementMeters(),
                            beginBoundaryChanged || endBoundaryChanged ? "UNKNOWN" : interval.geoEvidenceMovementCategory(),
                            beginBoundaryChanged ? null : geoEvidenceEvent(interval.beginGeoEventId(), interval.beginGeoEventDomain(), interval.beginGeoOccurredAt(), interval.beginLatitude(), interval.beginLongitude(), interval.beginGeoDistanceSeconds(), interval.beginGeoOdometerKm()),
                            endBoundaryChanged ? null : geoEvidenceEvent(interval.endGeoEventId(), interval.endGeoEventDomain(), interval.endGeoOccurredAt(), interval.endLatitude(), interval.endLongitude(), interval.endGeoDistanceSeconds(), interval.endGeoOdometerKm())
                    );
                })
                .filter(Objects::nonNull)
                .sorted(Comparator.comparing(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::startedAt)
                        .thenComparing(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::endedAt))
                .toList();
    }

    private List<TachographEsperPotentialInVehicleTripIntervalEvent> clipEsperPotentialInVehicleTripIntervalEvents(
            List<TachographEsperPotentialInVehicleTripIntervalEvent> intervals,
            List<TachographEsperPotentialInVehicleOvernightStayIntervalEvent> potentialInVehicleOvernightStayIntervals,
            OffsetDateTime requestedFrom,
            OffsetDateTime requestedTo
    ) {
        if (requestedFrom == null || requestedTo == null) {
            return List.of();
        }
        if (intervals == null || intervals.isEmpty()
                || potentialInVehicleOvernightStayIntervals == null || potentialInVehicleOvernightStayIntervals.isEmpty()) {
            return List.of();
        }
        return intervals.stream()
                .map(interval -> {
                    OffsetDateTime start = max(interval.startedAt(), requestedFrom);
                    OffsetDateTime end = min(interval.endedAt(), requestedTo);
                    if (!end.isAfter(start)) {
                        return null;
                    }
                    List<TachographEsperPotentialInVehicleOvernightStayIntervalEvent> containedIntervals =
                            potentialInVehicleOvernightStayIntervals.stream()
                                    .filter(candidate -> tripContainsPotentialInterval(
                                            interval.driverKey(),
                                            interval.registrationKey(),
                                            interval.vehicleKey(),
                                            start,
                                            end,
                                            candidate
                                    ))
                                    .sorted(Comparator.comparing(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::startedAt)
                                            .thenComparing(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::endedAt))
                                    .toList();
                    if (containedIntervals.isEmpty()) {
                        return null;
                    }
                    TachographEsperPotentialInVehicleOvernightStayIntervalEvent first = containedIntervals.get(0);
                    TachographEsperPotentialInVehicleOvernightStayIntervalEvent last =
                            containedIntervals.get(containedIntervals.size() - 1);
                    return new TachographEsperPotentialInVehicleTripIntervalEvent(
                            interval.sessionId(),
                            interval.driverKey(),
                            start,
                            end,
                            Duration.between(start, end).getSeconds(),
                            interval.registrationKey(),
                            interval.vehicleKey(),
                            containedIntervals.size(),
                            containedIntervals.stream()
                                    .mapToLong(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::durationSeconds)
                                    .sum(),
                            containedIntervals.stream()
                                    .mapToLong(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::cardAbsentDurationSeconds)
                                    .sum(),
                            first.startedAt(),
                            last.endedAt(),
                            first.previousDrivingSourceIntervalId(),
                            last.nextDrivingSourceIntervalId(),
                            containedIntervals
                    );
                })
                .filter(Objects::nonNull)
                .sorted(Comparator.comparing(TachographEsperPotentialInVehicleTripIntervalEvent::startedAt)
                        .thenComparing(TachographEsperPotentialInVehicleTripIntervalEvent::endedAt))
                .toList();
    }

    private boolean tripContainsPotentialInterval(
            String driverKey,
            String registrationKey,
            String vehicleKey,
            OffsetDateTime tripStartedAt,
            OffsetDateTime tripEndedAt,
            TachographEsperPotentialInVehicleOvernightStayIntervalEvent candidate
    ) {
        if (!Objects.equals(driverKey, candidate.driverKey())) {
            return false;
        }
        if (!Objects.equals(registrationKey, candidate.previousRegistrationKey())) {
            return false;
        }
        if (vehicleKey != null && candidate.previousVehicleKey() != null
                && !Objects.equals(vehicleKey, candidate.previousVehicleKey())) {
            return false;
        }
        return !candidate.startedAt().isBefore(tripStartedAt)
                && !candidate.endedAt().isAfter(tripEndedAt);
    }


    private List<String> esperProjectionNotes() {
        return List.of(
                "This endpoint returns Esper-backed per-driver interval projections from the in-memory tachograph file-session model.",
                "Driving intervals are a filtered projection of activity intervals where activityType = DRIVE.",
                "Driving interruption intervals are gaps between consecutive driving intervals longer than the configured significant-driving threshold.",
                "Driving interruption vehicle-change intervals are daily/weekly rest candidates where previousRegistrationKey differs from nextRegistrationKey.",
                "Daily/weekly rest candidate intervals are driving interruption intervals longer than the configured minimum rest-period threshold.",
                "Daily/weekly rest candidate coverage intervals enrich each rest candidate with card-present and card-absent coverage metrics computed from vehicle-usage and VU card-absent overlap.",
                "Daily/weekly rest candidate coverage intervals also attach begin/end geo evidence from nearby support events for the same driver and boundary-side vehicle identity.",
                "Boundary geo evidence prefers the nearest matching POSITION event, then PLACE, BORDER_CROSSING, and LOAD_UNLOAD within the configured lookback/lookahead windows.",
                "If both begin and end geo evidence carry odometer values, geoEvidenceMovementCategory classifies the interval as STATIONARY, MINOR, MOVED, or UNKNOWN.",
                "Unclassified daily/weekly rest candidate coverage intervals are the rest candidates that are neither potential home overnight stays nor potential in-vehicle overnight stays.",
                "Potential home overnight stay intervals are vehicle-change daily/weekly rest candidate coverage intervals where VU card-absent overlap covers at least 95% of the candidate interval.",
                "Potential in-vehicle overnight stay intervals are no-change daily/weekly rest candidate coverage intervals where card-present overlap covers the candidate rest period.",
                "Potential in-vehicle trip intervals span from the end of the coverage interval before a same-vehicle in-vehicle-overnight sequence to the start of the first coverage interval after that sequence.",
                "VU card-absent intervals are gaps between consecutive normalized vehicle-usage intervals for the same driver.",
                "occurredFrom and occurredTo clip the returned interval projections to the requested UTC time window.",
                "Vehicle-usage intervals clear clipped odometer endpoints because boundary odometer values cannot be recomputed safely from the source interval."
        );
    }

    private OffsetDateTime utc(OffsetDateTime value) {
        return value == null ? null : value.withOffsetSameInstant(java.time.ZoneOffset.UTC);
    }

    private TachographEsperGeoEvidenceEvent geoEvidenceEvent(
            String eventId,
            String eventDomain,
            OffsetDateTime occurredAt,
            Double latitude,
            Double longitude,
            Long distanceSeconds,
            Long odometerKm
    ) {
        if (eventId == null
                && eventDomain == null
                && occurredAt == null
                && latitude == null
                && longitude == null
                && distanceSeconds == null
                && odometerKm == null) {
            return null;
        }
        return new TachographEsperGeoEvidenceEvent(
                eventId,
                eventDomain,
                occurredAt,
                latitude,
                longitude,
                distanceSeconds,
                odometerKm
        );
    }

    private OffsetDateTime max(OffsetDateTime left, OffsetDateTime right) {
        if (left == null) {
            return right;
        }
        if (right == null) {
            return left;
        }
        return left.isAfter(right) ? left : right;
    }

    private OffsetDateTime min(OffsetDateTime left, OffsetDateTime right) {
        if (left == null) {
            return right;
        }
        if (right == null) {
            return left;
        }
        return left.isBefore(right) ? left : right;
    }
}
