package at.procon.eventhub.tachographfilesession.service;

import at.procon.eventhub.config.EventHubProperties;
import at.procon.eventhub.tachographfilesession.dto.TachographEsperEventsProcessingRequest;
import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto;
import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingRequest;
import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingResultDto;
import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession;
import at.procon.eventhub.tachographfilesession.model.ExtractedSupportEvent;
import at.procon.eventhub.tachographfilesession.model.PeriodizedDriverActivityInterval;
import at.procon.eventhub.tachographfilesession.model.ProcessedDrivingInterruption;
import at.procon.eventhub.tachographfilesession.model.ProcessedOperatingPeriod;
import at.procon.eventhub.tachographfilesession.model.ProcessedShiftDrivingEvaluation;
import at.procon.eventhub.tachographfilesession.model.ResolvedActivityInterval;
import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline;
import at.procon.eventhub.tachographfilesession.model.TachographEsperActivityIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingDerivedProjectionBundle;
import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
import at.procon.eventhub.tachographfilesession.model.TachographEsperGeoEvidenceEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleOvernightStayIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleTripIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperSupportGeoEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent;
import java.time.Duration;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class TachographFileSessionProcessingService {

    private final TachographFileSessionRepository repository;
    private final DriverTimelineBuilder driverTimelineBuilder;
    private final EventBackedDriverTimelineBuilder eventBackedDriverTimelineBuilder;
    private final TachographEsperProcessingCore esperProcessingCore;
    private final EventHubProperties properties;

    public TachographFileSessionProcessingService(
            TachographFileSessionRepository repository,
            DriverTimelineBuilder driverTimelineBuilder,
            TachographDerivedProjectionCompatibilityFacade compatibilityFacade,
            EventBackedDriverTimelineBuilder eventBackedDriverTimelineBuilder,
            EventHubProperties properties
    ) {
        this(
                repository,
                driverTimelineBuilder,
                eventBackedDriverTimelineBuilder,
                properties,
                new TachographEsperProcessingCore(driverTimelineBuilder, compatibilityFacade, properties)
        );
    }

    public TachographFileSessionProcessingService(
            TachographFileSessionRepository repository,
            DriverTimelineBuilder driverTimelineBuilder,
            DriverTimelineReusableProjectionBuilder reusableProjectionBuilder,
            EventBackedDriverTimelineBuilder eventBackedDriverTimelineBuilder,
            EventHubProperties properties
    ) {
        this(
                repository,
                driverTimelineBuilder,
                eventBackedDriverTimelineBuilder,
                properties,
                new TachographEsperProcessingCore(driverTimelineBuilder, reusableProjectionBuilder, properties)
        );
    }

    @Autowired
    public TachographFileSessionProcessingService(
            TachographFileSessionRepository repository,
            DriverTimelineBuilder driverTimelineBuilder,
            EventBackedDriverTimelineBuilder eventBackedDriverTimelineBuilder,
            EventHubProperties properties,
            TachographEsperProcessingCore esperProcessingCore
    ) {
        this.repository = repository;
        this.driverTimelineBuilder = driverTimelineBuilder;
        this.eventBackedDriverTimelineBuilder = eventBackedDriverTimelineBuilder;
        this.properties = properties;
        this.esperProcessingCore = esperProcessingCore;
    }

    public TachographOperatingPeriodsProcessingResultDto evaluateOperatingPeriods(
            UUID sessionId,
            String driverKey,
            TachographOperatingPeriodsProcessingRequest request
    ) {
        TachographOperatingPeriodsProcessingRequest effectiveRequest = request == null
                ? new TachographOperatingPeriodsProcessingRequest(null, null, null, null, null, null)
                : request;
        TachographFileSession session = repository.find(sessionId)
                .orElseThrow(() -> new TachographFileSessionNotFoundException(sessionId));
        DriverExtractionSession driver = session.driversByKey().get(driverKey);
        if (driver == null) {
            throw new DriverNotFoundInSessionException(sessionId, driverKey);
        }

        ResolvedDriverTimeline timeline = resolveTimeline(session, driver);
        OffsetDateTime loadedFrom = timeline.loadedFrom();
        OffsetDateTime loadedTo = timeline.loadedTo();
        OffsetDateTime requestedFrom = effectiveRequest.occurredFrom() == null ? loadedFrom : utc(effectiveRequest.occurredFrom());
        OffsetDateTime requestedTo = effectiveRequest.occurredTo() == null ? loadedTo : utc(effectiveRequest.occurredTo());
        if (requestedFrom != null && requestedTo != null && requestedTo.isBefore(requestedFrom)) {
            throw new IllegalArgumentException("occurredTo must not be before occurredFrom.");
        }

        Duration operatingSplitIdleThreshold = Duration.ofHours(resolveOperatingSplitIdleHours(effectiveRequest));
        Duration significantDrivingThreshold = Duration.ofMinutes(resolveSignificantDrivingMinutes(effectiveRequest));
        Duration mergeGapTolerance = Duration.ofSeconds(resolveMergeGapSeconds(effectiveRequest));
        Duration gapDetectionTolerance = Duration.ofSeconds(resolveGapDetectionToleranceSeconds(effectiveRequest));

        List<ResolvedActivityInterval> knownLoadedIntervals = sortedPositiveIntervals(timeline.activityIntervals());
        List<ResolvedActivityInterval> evaluationLoadedIntervals = synthesizeUnknownGaps(knownLoadedIntervals, gapDetectionTolerance);
        PeriodizationResult periodization = periodize(evaluationLoadedIntervals, operatingSplitIdleThreshold);
        List<PeriodizedDriverActivityInterval> mergedLoadedIntervals = mergeConsecutiveActivities(
                periodization.periodizedIntervals(),
                mergeGapTolerance
        );

        List<ResolvedActivityInterval> evaluationIntervals = clipResolvedIntervals(evaluationLoadedIntervals, requestedFrom, requestedTo);
        List<PeriodizedDriverActivityInterval> periodizedIntervals = clipPeriodizedIntervals(periodization.periodizedIntervals(), requestedFrom, requestedTo);
        List<PeriodizedDriverActivityInterval> mergedIntervals = clipPeriodizedIntervals(mergedLoadedIntervals, requestedFrom, requestedTo);
        List<ProcessedOperatingPeriod> operatingPeriods = buildOperatingPeriods(
                periodization.closedPeriods(),
                periodizedIntervals,
                knownLoadedIntervals,
                requestedFrom,
                requestedTo,
                mergeGapTolerance,
                significantDrivingThreshold
        );

        return new TachographOperatingPeriodsProcessingResultDto(
                sessionId,
                driverKey,
                loadedFrom,
                loadedTo,
                requestedFrom,
                requestedTo,
                timeline.activityIntervals().size(),
                evaluationIntervals.size(),
                periodizedIntervals.size(),
                mergedIntervals.size(),
                operatingPeriods.size(),
                resolveOperatingSplitIdleHours(effectiveRequest),
                resolveSignificantDrivingMinutes(effectiveRequest),
                resolveMergeGapSeconds(effectiveRequest),
                resolveGapDetectionToleranceSeconds(effectiveRequest),
                timeline,
                evaluationIntervals,
                periodizedIntervals,
                mergedIntervals,
                operatingPeriods,
                notes()
        );
    }

    public TachographEsperDriverProcessingResultDto getEsperDriverProcessingResults(
            UUID sessionId,
            String driverKey
    ) {
        return getEsperDriverProcessingResults(sessionId, driverKey, null);
    }

    public TachographEsperDriverProcessingResultDto getEsperDriverProcessingResults(
            UUID sessionId,
            String driverKey,
            TachographEsperEventsProcessingRequest request
    ) {
        TachographEsperEventsProcessingRequest effectiveRequest = request == null
                ? new TachographEsperEventsProcessingRequest(null, null, null, null)
                : request;
        TachographFileSession session = repository.find(sessionId)
                .orElseThrow(() -> new TachographFileSessionNotFoundException(sessionId));
        DriverExtractionSession driver = session.driversByKey().get(driverKey);
        if (driver == null) {
            throw new DriverNotFoundInSessionException(sessionId, driverKey);
        }

        ResolvedDriverTimeline timeline = resolveTimeline(session, driver);
        OffsetDateTime requestedFrom = effectiveRequest.occurredFrom() == null
                ? timeline.loadedFrom()
                : utc(effectiveRequest.occurredFrom());
        OffsetDateTime requestedTo = effectiveRequest.occurredTo() == null
                ? timeline.loadedTo()
                : utc(effectiveRequest.occurredTo());
        if (requestedFrom != null && requestedTo != null && requestedTo.isBefore(requestedFrom)) {
            throw new IllegalArgumentException("occurredTo must not be before occurredFrom.");
        }

        return esperProcessingCore.process(TachographEsperProcessingInput.fromFileSession(
                session,
                driver,
                timeline,
                requestedFrom,
                requestedTo,
                resolveEsperSignificantDrivingMinutes(effectiveRequest),
                resolveMinimumRestPeriodMinutes(effectiveRequest),
                List.of()
        ));
    }

    private ResolvedDriverTimeline resolveTimeline(
            TachographFileSession session,
            DriverExtractionSession driver
    ) {
        if (properties.getTachographFileSession().getProcessing().getTimelineInputMode()
                == EventHubProperties.TimelineInputMode.EVENTS) {
            return eventBackedDriverTimelineBuilder.build(session, driver);
        }
        return driverTimelineBuilder.build(session, driver);
    }

    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 -> 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<ResolvedActivityInterval> synthesizeUnknownGaps(
            List<ResolvedActivityInterval> knownIntervals,
            Duration gapDetectionTolerance
    ) {
        List<ResolvedActivityInterval> allKnown = sortedPositiveIntervals(knownIntervals);
        List<ResolvedActivityInterval> nonRestActivities = allKnown.stream()
                .filter(interval -> !"BREAK_REST".equals(interval.activityType()))
                .toList();
        if (nonRestActivities.isEmpty()) {
            return List.of();
        }

        List<ResolvedActivityInterval> result = new ArrayList<>();
        for (int index = 0; index < nonRestActivities.size(); index++) {
            ResolvedActivityInterval current = nonRestActivities.get(index);
            result.add(current);
            if (index + 1 >= nonRestActivities.size()) {
                continue;
            }
            ResolvedActivityInterval next = nonRestActivities.get(index + 1);
            if (!next.from().isAfter(current.to())) {
                continue;
            }
            OffsetDateTime gapStart = current.to();
            OffsetDateTime gapEnd = next.from();
            List<ResolvedActivityInterval> uncoveredGapSegments = subtractCoverage(
                    unknownGapTemplate(current, next, gapStart, gapEnd),
                    allKnown
            );
            for (ResolvedActivityInterval gap : uncoveredGapSegments) {
                if (gap.durationSeconds() > gapDetectionTolerance.getSeconds()) {
                    result.add(gap);
                }
            }
        }
        return result.stream()
                .sorted(Comparator.comparing(ResolvedActivityInterval::from)
                        .thenComparing(ResolvedActivityInterval::to)
                        .thenComparing(ResolvedActivityInterval::activityType, Comparator.nullsLast(String::compareTo)))
                .toList();
    }

    private PeriodizationResult periodize(
            List<ResolvedActivityInterval> intervals,
            Duration operatingSplitIdleThreshold
    ) {
        List<ResolvedActivityInterval> sorted = sortedPositiveIntervals(intervals);
        if (sorted.isEmpty()) {
            return new PeriodizationResult(List.of(), List.of());
        }

        List<PeriodizedDriverActivityInterval> periodizedIntervals = new ArrayList<>();
        List<ClosedOperatingPeriod> closedPeriods = new ArrayList<>();
        boolean hasOpenPeriod = false;
        long operatingPeriodNo = 0L;
        OffsetDateTime operatingPeriodStartedAt = null;
        OffsetDateTime lastKnownActivityEndAt = null;

        for (ResolvedActivityInterval interval : sorted) {
            if ("UNKNOWN".equals(interval.activityType())) {
                if (!hasOpenPeriod) {
                    continue;
                }
                if (interval.durationSeconds() >= operatingSplitIdleThreshold.getSeconds()) {
                    closedPeriods.add(closeCurrent(operatingPeriodNo, operatingPeriodStartedAt, lastKnownActivityEndAt, "UNKNOWN_GAP"));
                    hasOpenPeriod = false;
                    operatingPeriodStartedAt = null;
                    lastKnownActivityEndAt = null;
                    continue;
                }
                periodizedIntervals.add(periodized(interval, operatingPeriodNo, operatingPeriodStartedAt, false,
                        Math.max(0L, Duration.between(lastKnownActivityEndAt, interval.from()).getSeconds())));
                continue;
            }

            if (!hasOpenPeriod) {
                operatingPeriodNo = operatingPeriodNo < 1 ? 1 : operatingPeriodNo + 1;
                hasOpenPeriod = true;
                operatingPeriodStartedAt = interval.from();
                lastKnownActivityEndAt = interval.to();
                periodizedIntervals.add(periodized(interval, operatingPeriodNo, operatingPeriodStartedAt, true, null));
                continue;
            }

            long gapSeconds = Math.max(0L, Duration.between(lastKnownActivityEndAt, interval.from()).getSeconds());
            if (gapSeconds >= operatingSplitIdleThreshold.getSeconds()) {
                closedPeriods.add(closeCurrent(operatingPeriodNo, operatingPeriodStartedAt, lastKnownActivityEndAt, "IDLE_GAP"));
                operatingPeriodNo++;
                hasOpenPeriod = true;
                operatingPeriodStartedAt = interval.from();
                lastKnownActivityEndAt = interval.to();
                periodizedIntervals.add(periodized(interval, operatingPeriodNo, operatingPeriodStartedAt, true, gapSeconds));
                continue;
            }

            periodizedIntervals.add(periodized(interval, operatingPeriodNo, operatingPeriodStartedAt, false, gapSeconds));
            if (interval.to().isAfter(lastKnownActivityEndAt)) {
                lastKnownActivityEndAt = interval.to();
            }
        }

        if (hasOpenPeriod) {
            closedPeriods.add(closeCurrent(operatingPeriodNo, operatingPeriodStartedAt, lastKnownActivityEndAt, "FLUSH"));
        }

        return new PeriodizationResult(
                periodizedIntervals.stream()
                        .sorted(Comparator.comparing(PeriodizedDriverActivityInterval::from)
                                .thenComparing(PeriodizedDriverActivityInterval::to)
                                .thenComparing(PeriodizedDriverActivityInterval::activityType, Comparator.nullsLast(String::compareTo)))
                        .toList(),
                closedPeriods.stream()
                        .sorted(Comparator.comparing(ClosedOperatingPeriod::startedAt))
                        .toList()
        );
    }

    private PeriodizedDriverActivityInterval periodized(
            ResolvedActivityInterval interval,
            long operatingPeriodNo,
            OffsetDateTime operatingPeriodStartedAt,
            boolean newOperatingPeriod,
            Long gapSincePreviousActivitySeconds
    ) {
        return new PeriodizedDriverActivityInterval(
                interval.intervalId(),
                interval.from(),
                interval.to(),
                interval.durationSeconds(),
                interval.activityType(),
                interval.slot(),
                interval.cardStatus(),
                interval.drivingStatus(),
                interval.registrationKey(),
                interval.vehicleKey(),
                interval.sourceKind(),
                interval.sourceIntervalIds(),
                interval.synthetic(),
                interval.clippedToRequestedPeriod(),
                interval.level(),
                operatingPeriodNo,
                operatingPeriodStartedAt,
                newOperatingPeriod,
                gapSincePreviousActivitySeconds
        );
    }

    private ClosedOperatingPeriod closeCurrent(
            long operatingPeriodNo,
            OffsetDateTime operatingPeriodStartedAt,
            OffsetDateTime lastKnownActivityEndAt,
            String closedBy
    ) {
        return new ClosedOperatingPeriod(
                operatingPeriodNo,
                operatingPeriodStartedAt,
                lastKnownActivityEndAt,
                Duration.between(operatingPeriodStartedAt, lastKnownActivityEndAt).getSeconds(),
                closedBy
        );
    }

    private List<PeriodizedDriverActivityInterval> mergeConsecutiveActivities(
            List<PeriodizedDriverActivityInterval> intervals,
            Duration mergeGapTolerance
    ) {
        if (intervals == null || intervals.isEmpty()) {
            return List.of();
        }
        List<PeriodizedDriverActivityInterval> sorted = intervals.stream()
                .filter(interval -> interval.to().isAfter(interval.from()))
                .sorted(Comparator.comparing(PeriodizedDriverActivityInterval::from)
                        .thenComparing(PeriodizedDriverActivityInterval::to)
                        .thenComparing(PeriodizedDriverActivityInterval::activityType, Comparator.nullsLast(String::compareTo)))
                .toList();

        List<PeriodizedDriverActivityInterval> result = new ArrayList<>();
        PeriodizedDriverActivityInterval current = null;
        List<String> currentSources = new ArrayList<>();
        for (PeriodizedDriverActivityInterval next : sorted) {
            if (current == null) {
                current = next;
                currentSources = new ArrayList<>(next.sourceIntervalIds());
                continue;
            }
            if (canMerge(current, next, mergeGapTolerance)) {
                currentSources.addAll(next.sourceIntervalIds());
                current = current.asMerged(current.intervalId() + "+" + next.intervalId(), max(current.to(), next.to()), currentSources);
            } else {
                result.add(current.asMerged(current.intervalId(), current.to(), currentSources));
                current = next;
                currentSources = new ArrayList<>(next.sourceIntervalIds());
            }
        }
        if (current != null) {
            result.add(current.asMerged(current.intervalId(), current.to(), currentSources));
        }
        return result;
    }

    private boolean canMerge(
            PeriodizedDriverActivityInterval left,
            PeriodizedDriverActivityInterval right,
            Duration mergeGapTolerance
    ) {
        long gapSeconds = Duration.between(left.to(), right.from()).getSeconds();
        return left.operatingPeriodNo() == right.operatingPeriodNo()
                && Objects.equals(left.activityType(), right.activityType())
                && Objects.equals(left.slot(), right.slot())
                && Objects.equals(left.cardStatus(), right.cardStatus())
                && Objects.equals(left.drivingStatus(), right.drivingStatus())
                && Objects.equals(left.registrationKey(), right.registrationKey())
                && Objects.equals(left.vehicleKey(), right.vehicleKey())
                && Objects.equals(left.sourceKind(), right.sourceKind())
                && left.synthetic() == right.synthetic()
                && gapSeconds <= mergeGapTolerance.getSeconds();
    }

    private List<ProcessedOperatingPeriod> buildOperatingPeriods(
            List<ClosedOperatingPeriod> closedPeriods,
            List<PeriodizedDriverActivityInterval> clippedPeriodizedIntervals,
            List<ResolvedActivityInterval> knownLoadedIntervals,
            OffsetDateTime requestedFrom,
            OffsetDateTime requestedTo,
            Duration mergeGapTolerance,
            Duration significantDrivingThreshold
    ) {
        if (requestedFrom == null || requestedTo == null) {
            return List.of();
        }

        LinkedHashMap<Long, List<PeriodizedDriverActivityInterval>> intervalsByPeriod = new LinkedHashMap<>();
        for (PeriodizedDriverActivityInterval interval : clippedPeriodizedIntervals) {
            intervalsByPeriod.computeIfAbsent(interval.operatingPeriodNo(), ignored -> new ArrayList<>()).add(interval);
        }

        List<ProcessedOperatingPeriod> result = new ArrayList<>();
        for (ClosedOperatingPeriod closedPeriod : closedPeriods) {
            if (!closedPeriod.endedAt().isAfter(requestedFrom) || !closedPeriod.startedAt().isBefore(requestedTo)) {
                continue;
            }
            OffsetDateTime start = max(closedPeriod.startedAt(), requestedFrom);
            OffsetDateTime end = min(closedPeriod.endedAt(), requestedTo);
            if (!end.isAfter(start)) {
                continue;
            }

            List<PeriodizedDriverActivityInterval> intervals = intervalsByPeriod.getOrDefault(closedPeriod.operatingPeriodNo(), List.of());
            List<ResolvedActivityInterval> rawActivities = clipResolvedIntervals(knownLoadedIntervals, start, end);
            long breakRestSeconds = rawActivities.stream()
                    .filter(activity -> "BREAK_REST".equals(activity.activityType()))
                    .mapToLong(ResolvedActivityInterval::durationSeconds)
                    .sum();
            ProcessedShiftDrivingEvaluation drivingEvaluation = evaluateSignificantDriving(
                    rawActivities,
                    mergeGapTolerance,
                    significantDrivingThreshold
            );
            result.add(new ProcessedOperatingPeriod(
                    closedPeriod.operatingPeriodNo(),
                    start,
                    end,
                    Duration.between(start, end).getSeconds(),
                    closedPeriod.closedBy(),
                    rawActivities,
                    breakRestSeconds,
                    sumActivitySeconds(intervals, "DRIVE"),
                    sumActivitySeconds(intervals, "WORK"),
                    sumActivitySeconds(intervals, "AVAILABILITY"),
                    sumActivitySeconds(intervals, "UNKNOWN"),
                    intervals.size(),
                    drivingEvaluation,
                    !start.equals(closedPeriod.startedAt()) || !end.equals(closedPeriod.endedAt())
            ));
        }
        return result;
    }

    private ProcessedShiftDrivingEvaluation evaluateSignificantDriving(
            List<ResolvedActivityInterval> rawActivities,
            Duration mergeGapTolerance,
            Duration significantDrivingThreshold
    ) {
        if (rawActivities.isEmpty()) {
            return new ProcessedShiftDrivingEvaluation(
                    (int) significantDrivingThreshold.toMinutes(),
                    null,
                    null,
                    null,
                    null,
                    List.of()
            );
        }
        List<ResolvedActivityInterval> mergedActivities = mergeConsecutiveRawActivities(rawActivities, mergeGapTolerance);
        List<ResolvedActivityInterval> significantDrivingPeriods = mergedActivities.stream()
                .filter(activity -> "DRIVE".equals(activity.activityType()))
                .filter(activity -> activity.durationSeconds() > significantDrivingThreshold.getSeconds())
                .sorted(Comparator.comparing(ResolvedActivityInterval::from))
                .toList();
        if (significantDrivingPeriods.isEmpty()) {
            return new ProcessedShiftDrivingEvaluation(
                    (int) significantDrivingThreshold.toMinutes(),
                    null,
                    null,
                    null,
                    null,
                    List.of()
            );
        }

        List<ProcessedDrivingInterruption> interruptions = new ArrayList<>();
        for (int i = 1; i < significantDrivingPeriods.size(); i++) {
            ResolvedActivityInterval previous = significantDrivingPeriods.get(i - 1);
            ResolvedActivityInterval next = significantDrivingPeriods.get(i);
            if (next.from().isAfter(previous.to())) {
                interruptions.add(new ProcessedDrivingInterruption(
                        previous.to(),
                        next.from(),
                        Duration.between(previous.to(), next.from()).getSeconds(),
                        previous.sourceIntervalIds().isEmpty() ? null : previous.sourceIntervalIds().get(previous.sourceIntervalIds().size() - 1),
                        next.sourceIntervalIds().isEmpty() ? null : next.sourceIntervalIds().get(0)
                ));
            }
        }

        ResolvedActivityInterval first = significantDrivingPeriods.get(0);
        ResolvedActivityInterval last = significantDrivingPeriods.get(significantDrivingPeriods.size() - 1);
        return new ProcessedShiftDrivingEvaluation(
                (int) significantDrivingThreshold.toMinutes(),
                first.from(),
                last.to(),
                first,
                last,
                interruptions
        );
    }

    private List<ResolvedActivityInterval> mergeConsecutiveRawActivities(
            List<ResolvedActivityInterval> intervals,
            Duration mergeGapTolerance
    ) {
        if (intervals == null || intervals.isEmpty()) {
            return List.of();
        }
        List<ResolvedActivityInterval> sorted = intervals.stream()
                .filter(interval -> interval.to().isAfter(interval.from()))
                .sorted(Comparator.comparing(ResolvedActivityInterval::from)
                        .thenComparing(ResolvedActivityInterval::to)
                        .thenComparing(ResolvedActivityInterval::activityType, Comparator.nullsLast(String::compareTo)))
                .toList();

        List<ResolvedActivityInterval> result = new ArrayList<>();
        ResolvedActivityInterval current = null;
        List<String> currentSources = new ArrayList<>();
        for (ResolvedActivityInterval next : sorted) {
            if (current == null) {
                current = next;
                currentSources = new ArrayList<>(next.sourceIntervalIds());
                continue;
            }
            long gapSeconds = Duration.between(current.to(), next.from()).getSeconds();
            if (Objects.equals(current.activityType(), next.activityType())
                    && Objects.equals(current.slot(), next.slot())
                    && Objects.equals(current.cardStatus(), next.cardStatus())
                    && Objects.equals(current.drivingStatus(), next.drivingStatus())
                    && Objects.equals(current.registrationKey(), next.registrationKey())
                    && Objects.equals(current.vehicleKey(), next.vehicleKey())
                    && Objects.equals(current.sourceKind(), next.sourceKind())
                    && current.synthetic() == next.synthetic()
                    && gapSeconds <= mergeGapTolerance.getSeconds()) {
                currentSources.addAll(next.sourceIntervalIds());
                current = current.asMerged(current.intervalId() + "+" + next.intervalId(), max(current.to(), next.to()), currentSources);
            } else {
                result.add(current.asMerged(current.intervalId(), current.to(), currentSources));
                current = next;
                currentSources = new ArrayList<>(next.sourceIntervalIds());
            }
        }
        if (current != null) {
            result.add(current.asMerged(current.intervalId(), current.to(), currentSources));
        }
        return result;
    }

    private long sumActivitySeconds(List<PeriodizedDriverActivityInterval> intervals, String activityType) {
        return intervals.stream()
                .filter(interval -> activityType.equals(interval.activityType()))
                .mapToLong(PeriodizedDriverActivityInterval::durationSeconds)
                .sum();
    }

    private List<ResolvedActivityInterval> clipResolvedIntervals(
            List<ResolvedActivityInterval> intervals,
            OffsetDateTime requestedFrom,
            OffsetDateTime requestedTo
    ) {
        if (requestedFrom == null || requestedTo == null) {
            return List.of();
        }
        return intervals.stream()
                .map(interval -> {
                    OffsetDateTime start = max(interval.from(), requestedFrom);
                    OffsetDateTime end = min(interval.to(), requestedTo);
                    if (!end.isAfter(start)) {
                        return null;
                    }
                    boolean clipped = interval.clippedToRequestedPeriod()
                            || !start.equals(interval.from())
                            || !end.equals(interval.to());
                    return interval.withTime(start, end, clipped);
                })
                .filter(Objects::nonNull)
                .sorted(Comparator.comparing(ResolvedActivityInterval::from)
                        .thenComparing(ResolvedActivityInterval::to)
                        .thenComparing(ResolvedActivityInterval::activityType, Comparator.nullsLast(String::compareTo)))
                .toList();
    }

    private List<PeriodizedDriverActivityInterval> clipPeriodizedIntervals(
            List<PeriodizedDriverActivityInterval> intervals,
            OffsetDateTime requestedFrom,
            OffsetDateTime requestedTo
    ) {
        if (requestedFrom == null || requestedTo == null) {
            return List.of();
        }
        return intervals.stream()
                .map(interval -> {
                    OffsetDateTime start = max(interval.from(), requestedFrom);
                    OffsetDateTime end = min(interval.to(), requestedTo);
                    if (!end.isAfter(start)) {
                        return null;
                    }
                    boolean clipped = interval.clippedToRequestedPeriod()
                            || !start.equals(interval.from())
                            || !end.equals(interval.to());
                    return interval.withTime(start, end, clipped);
                })
                .filter(Objects::nonNull)
                .sorted(Comparator.comparing(PeriodizedDriverActivityInterval::from)
                        .thenComparing(PeriodizedDriverActivityInterval::to)
                        .thenComparing(PeriodizedDriverActivityInterval::activityType, Comparator.nullsLast(String::compareTo)))
                .toList();
    }

    private ResolvedActivityInterval unknownGapTemplate(
            ResolvedActivityInterval previous,
            ResolvedActivityInterval next,
            OffsetDateTime gapStart,
            OffsetDateTime gapEnd
    ) {
        return new ResolvedActivityInterval(
                "UNKNOWN-" + gapStart.toEpochSecond(),
                gapStart,
                gapEnd,
                Duration.between(gapStart, gapEnd).getSeconds(),
                "UNKNOWN",
                Objects.equals(previous.slot(), next.slot()) ? previous.slot() : null,
                previous.cardStatus(),
                "UNKNOWN",
                Objects.equals(previous.registrationKey(), next.registrationKey()) ? previous.registrationKey() : null,
                Objects.equals(previous.vehicleKey(), next.vehicleKey()) ? previous.vehicleKey() : null,
                "SYNTHETIC_GAP",
                List.of(),
                true,
                false,
                "UNKNOWN_GAP"
        );
    }

    private List<ResolvedActivityInterval> subtractCoverage(
            ResolvedActivityInterval candidate,
            List<ResolvedActivityInterval> coverage
    ) {
        List<ResolvedActivityInterval> result = new ArrayList<>();
        OffsetDateTime cursor = candidate.from();
        for (ResolvedActivityInterval covered : coverage) {
            if (!covered.to().isAfter(cursor)) {
                continue;
            }
            if (!covered.from().isBefore(candidate.to())) {
                break;
            }
            OffsetDateTime overlapStart = max(cursor, covered.from());
            if (overlapStart.isAfter(cursor)) {
                result.add(candidate.withTime(cursor, overlapStart, candidate.clippedToRequestedPeriod()));
            }
            if (covered.to().isAfter(cursor)) {
                cursor = max(cursor, covered.to());
            }
            if (!candidate.to().isAfter(cursor)) {
                break;
            }
        }
        if (candidate.to().isAfter(cursor)) {
            result.add(candidate.withTime(cursor, candidate.to(), candidate.clippedToRequestedPeriod()));
        }
        return result.stream()
                .filter(interval -> interval.to().isAfter(interval.from()))
                .toList();
    }

    private List<ResolvedActivityInterval> sortedPositiveIntervals(List<ResolvedActivityInterval> intervals) {
        if (intervals == null || intervals.isEmpty()) {
            return List.of();
        }
        return intervals.stream()
                .filter(interval -> interval.to().isAfter(interval.from()))
                .sorted(Comparator.comparing(ResolvedActivityInterval::from)
                        .thenComparing(ResolvedActivityInterval::to)
                        .thenComparing(ResolvedActivityInterval::activityType, Comparator.nullsLast(String::compareTo)))
                .toList();
    }

    private int resolveOperatingSplitIdleHours(TachographOperatingPeriodsProcessingRequest request) {
        return request.operatingSplitIdleHours() == null
                ? properties.getTachographFileSession().getProcessing().getOperatingSplitIdleHours()
                : request.operatingSplitIdleHours();
    }

    private int resolveSignificantDrivingMinutes(TachographOperatingPeriodsProcessingRequest request) {
        return request.significantDrivingMinutes() == null
                ? properties.getTachographFileSession().getProcessing().getSignificantDrivingMinutes()
                : request.significantDrivingMinutes();
    }

    private int resolveMergeGapSeconds(TachographOperatingPeriodsProcessingRequest request) {
        return request.mergeGapSeconds() == null
                ? properties.getTachographFileSession().getProcessing().getMergeGapSeconds()
                : request.mergeGapSeconds();
    }

    private int resolveGapDetectionToleranceSeconds(TachographOperatingPeriodsProcessingRequest request) {
        return request.gapDetectionToleranceSeconds() == null
                ? properties.getTachographFileSession().getProcessing().getGapDetectionToleranceSeconds()
                : request.gapDetectionToleranceSeconds();
    }

    private int resolveEsperSignificantDrivingMinutes(TachographEsperEventsProcessingRequest request) {
        return request.significantDrivingMinutes() == null
                ? properties.getTachographFileSession().getProcessing().getSignificantDrivingMinutes()
                : request.significantDrivingMinutes();
    }

    private int resolveMinimumRestPeriodMinutes(TachographEsperEventsProcessingRequest request) {
        return request.minimumRestPeriodMinutes() == null
                ? properties.getTachographFileSession().getProcessing().getMinimumRestPeriodMinutes()
                : request.minimumRestPeriodMinutes();
    }

    private List<String> notes() {
        return List.of(
                "This endpoint evaluates operating periods from the in-memory tachograph file-session model.",
                "BREAK_REST intervals are excluded from periodization but still block synthetic UNKNOWN gaps.",
                "Synthetic UNKNOWN intervals are created only for uncovered gaps between non-rest activities.",
                "Vehicle usage is normalized before activity processing and merged across touching intervals with the same vehicle identity."
        );
    }

    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 long overlapSeconds(
            OffsetDateTime intervalStart,
            OffsetDateTime intervalEnd,
            List<TachographEsperVuCardAbsentIntervalEvent> unknownIntervals,
            String driverKey
    ) {
        if (unknownIntervals == null || unknownIntervals.isEmpty()) {
            return 0L;
        }
        long total = 0L;
        for (TachographEsperVuCardAbsentIntervalEvent unknown : unknownIntervals) {
            if (!Objects.equals(driverKey, unknown.driverKey())) {
                continue;
            }
            OffsetDateTime overlapStart = max(intervalStart, unknown.startedAt());
            OffsetDateTime overlapEnd = min(intervalEnd, unknown.endedAt());
            if (overlapEnd.isAfter(overlapStart)) {
                total += Duration.between(overlapStart, overlapEnd).getSeconds();
            }
        }
        return total;
    }

    private long overlapVehicleUsageSeconds(
            OffsetDateTime intervalStart,
            OffsetDateTime intervalEnd,
            List<TachographEsperVehicleUsageIntervalEvent> vehicleUsageIntervals,
            String driverKey,
            String registrationKey
    ) {
        if (vehicleUsageIntervals == null || vehicleUsageIntervals.isEmpty()) {
            return 0L;
        }
        long total = 0L;
        for (TachographEsperVehicleUsageIntervalEvent vehicleUsage : vehicleUsageIntervals) {
            if (!Objects.equals(driverKey, vehicleUsage.driverKey())) {
                continue;
            }
            if (registrationKey != null && !Objects.equals(registrationKey, vehicleUsage.registrationKey())) {
                continue;
            }
            if (vehicleUsage.endedAt() == null) {
                continue;
            }
            OffsetDateTime overlapStart = max(intervalStart, vehicleUsage.startedAt());
            OffsetDateTime overlapEnd = min(intervalEnd, vehicleUsage.endedAt());
            if (overlapEnd.isAfter(overlapStart)) {
                total += Duration.between(overlapStart, overlapEnd).getSeconds();
            }
        }
        return total;
    }

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

    private record PeriodizationResult(
            List<PeriodizedDriverActivityInterval> periodizedIntervals,
            List<ClosedOperatingPeriod> closedPeriods
    ) {
    }

    private record ClosedOperatingPeriod(
            long operatingPeriodNo,
            OffsetDateTime startedAt,
            OffsetDateTime endedAt,
            long durationSeconds,
            String closedBy
    ) {
    }
}
