package at.procon.eventhub.esperpoc.service;

import at.procon.eventhub.config.EventHubProperties;
import at.procon.eventhub.esperpoc.dto.ActivityIntervalDto;
import at.procon.eventhub.esperpoc.dto.DrivingInterruptionDto;
import at.procon.eventhub.esperpoc.dto.EsperOperatingPeriodEngineMode;
import at.procon.eventhub.esperpoc.dto.EsperOperatingPeriodRequest;
import at.procon.eventhub.esperpoc.dto.EsperOperatingPeriodResultDto;
import at.procon.eventhub.esperpoc.dto.EsperSourceSelectionMode;
import at.procon.eventhub.esperpoc.dto.EsperUnknownTreatmentMode;
import at.procon.eventhub.esperpoc.dto.NonDrivingIntervalDto;
import at.procon.eventhub.esperpoc.dto.OperatingPeriodActivityIntervalDto;
import at.procon.eventhub.esperpoc.dto.OperatingPeriodDto;
import at.procon.eventhub.esperpoc.dto.RawActivityEventDto;
import at.procon.eventhub.esperpoc.dto.ShiftDrivingEvaluationDto;
import at.procon.eventhub.esperpoc.persistence.EsperPocActivityRepository;
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.Map;
import java.util.Objects;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class EsperOperatingPeriodEvaluationService {

    private static final Logger log = LoggerFactory.getLogger(EsperOperatingPeriodEvaluationService.class);

    private final EsperPocActivityRepository activityRepository;
    private final EsperDriverActivityEngine activityEngine;
    private final EsperOperatingPeriodEngine operatingPeriodEngine;
    private final EventHubProperties properties;

    public EsperOperatingPeriodEvaluationService(
            EsperPocActivityRepository activityRepository,
            EsperDriverActivityEngine activityEngine,
            EsperOperatingPeriodEngine operatingPeriodEngine
    ) {
        this(activityRepository, activityEngine, operatingPeriodEngine, null);
    }

    @Autowired
    public EsperOperatingPeriodEvaluationService(
            EsperPocActivityRepository activityRepository,
            EsperDriverActivityEngine activityEngine,
            EsperOperatingPeriodEngine operatingPeriodEngine,
            EventHubProperties properties
    ) {
        this.activityRepository = activityRepository;
        this.activityEngine = activityEngine;
        this.operatingPeriodEngine = operatingPeriodEngine;
        this.properties = properties;
    }

    public EsperOperatingPeriodResultDto evaluate(EsperOperatingPeriodRequest request) {
        long startedNanos = System.nanoTime();
        OffsetDateTime requestedFrom = utc(request.occurredFrom());
        OffsetDateTime requestedTo = utc(request.occurredTo());
        OffsetDateTime loadedFrom = requestedFrom.minusHours(request.guardHours());
        OffsetDateTime loadedTo = requestedTo.plusHours(request.guardHours());
        Duration splitIdleThreshold = Duration.ofHours(resolveOperatingSplitIdleHours(request));
        Duration significantDrivingThreshold = Duration.ofMinutes(resolveSignificantDrivingMinutes(request));
        Duration mergeGapTolerance = Duration.ofSeconds(resolveMergeGapSeconds(request));
        Duration gapDetectionTolerance = Duration.ofSeconds(resolveGapDetectionToleranceSeconds(request));
        EsperSourceSelectionMode sourceSelectionMode = resolveSourceSelectionMode(request);
        EsperUnknownTreatmentMode unknownTreatmentMode = resolveUnknownTreatmentMode(request);
        EsperOperatingPeriodEngineMode engineMode = resolveEngineMode(request);

        long dbStartedNanos = System.nanoTime();
        List<RawActivityEventDto> rawEvents = activityRepository.findDriverActivityEvents(
                request.tenantKey(),
                request.driverId(),
                loadedFrom,
                loadedTo
        );
        long dbElapsedMs = elapsedMillis(dbStartedNanos);
        List<RawActivityEventDto> driverCardRawEvents = rawEvents.stream()
                .filter(event -> "DRIVER_CARD".equals(event.sourceKind()))
                .toList();
        List<RawActivityEventDto> vehicleUnitRawEvents = sourceSelectionMode == EsperSourceSelectionMode.DRIVER_CARD_ONLY
                ? List.of()
                : rawEvents.stream()
                .filter(event -> "VEHICLE_UNIT".equals(event.sourceKind()))
                .toList();
        List<RawActivityEventDto> selectedRawEvents = sourceSelectionMode == EsperSourceSelectionMode.DRIVER_CARD_ONLY
                ? driverCardRawEvents
                : rawEvents;

        long cardIntervalsStartedNanos = System.nanoTime();
        List<ActivityIntervalDto> driverCardRawIntervals = activityEngine.buildIntervals(driverCardRawEvents);
        long cardIntervalsElapsedMs = elapsedMillis(cardIntervalsStartedNanos);
        long vuIntervalsStartedNanos = System.nanoTime();
        List<ActivityIntervalDto> vehicleUnitRawIntervals = activityEngine.buildIntervals(vehicleUnitRawEvents);
        long vuIntervalsElapsedMs = elapsedMillis(vuIntervalsStartedNanos);

        long vuGapFillStartedNanos = System.nanoTime();
        List<ActivityIntervalDto> resolvedKnownLoadedIntervals = sourceSelectionMode == EsperSourceSelectionMode.DRIVER_CARD_ONLY
                ? driverCardRawIntervals
                : resolveVuFillGaps(driverCardRawIntervals, vehicleUnitRawIntervals);
        long vuGapFillElapsedMs = elapsedMillis(vuGapFillStartedNanos);

        long unknownGapStartedNanos = System.nanoTime();
        List<ActivityIntervalDto> evaluationLoadedIntervals = synthesizeUnknownGaps(
                resolvedKnownLoadedIntervals,
                gapDetectionTolerance
        );
        long unknownGapElapsedMs = elapsedMillis(unknownGapStartedNanos);

        long periodizeStartedNanos = System.nanoTime();
        EsperOperatingPeriodEngine.EsperOperatingPeriodEvaluation evaluation = operatingPeriodEngine.evaluate(
                evaluationLoadedIntervals,
                splitIdleThreshold,
                engineMode
        );
        long periodizeElapsedMs = elapsedMillis(periodizeStartedNanos);

        long mergeStartedNanos = System.nanoTime();
        List<OperatingPeriodActivityIntervalDto> mergedLoadedIntervals = mergeConsecutiveActivities(
                evaluation.periodizedIntervals(),
                mergeGapTolerance
        );
        long mergeElapsedMs = elapsedMillis(mergeStartedNanos);

        long nonDrivingStartedNanos = System.nanoTime();
        List<NonDrivingIntervalDto> nonDrivingLoadedIntervals = buildNonDrivingIntervals(
                mergedLoadedIntervals,
                significantDrivingThreshold,
                unknownTreatmentMode
        );
        long nonDrivingElapsedMs = elapsedMillis(nonDrivingStartedNanos);

        List<OperatingPeriodActivityIntervalDto> periodizedIntervals = clipOperatingIntervals(
                evaluation.periodizedIntervals(),
                requestedFrom,
                requestedTo
        );
        List<OperatingPeriodActivityIntervalDto> mergedIntervals = clipOperatingIntervals(
                mergedLoadedIntervals,
                requestedFrom,
                requestedTo
        );
        List<NonDrivingIntervalDto> nonDrivingIntervals = clipNonDrivingIntervals(
                nonDrivingLoadedIntervals,
                requestedFrom,
                requestedTo
        );
        List<OperatingPeriodDto> operatingPeriods = buildOperatingPeriods(
                evaluation.closedPeriods(),
                periodizedIntervals,
                clipKnownActivities(resolvedKnownLoadedIntervals, requestedFrom, requestedTo),
                requestedFrom,
                requestedTo,
                mergeGapTolerance,
                significantDrivingThreshold
        );
        long totalElapsedMs = elapsedMillis(startedNanos);

        log.info("Esper operating-period evaluation tenant={} driverId={} requestedFrom={} requestedTo={} loadedFrom={} loadedTo={} sourceSelectionMode={} unknownMode={} engineMode={} rawEvents={} cardRawEvents={} vuRawEvents={} cardIntervals={} vuIntervals={} resolvedKnownIntervals={} evaluationIntervals={} periodizedIntervals={} mergedIntervals={} nonDrivingIntervals={} operatingPeriods={} timingsMs={{dbRetrieve={}, cardIntervalEsper={}, vuIntervalEsper={}, vuGapFill={}, synthUnknown={}, periodizeEsper={}, merge={}, nonDriving={}, total={}}}",
                request.tenantKey(),
                request.driverId(),
                requestedFrom,
                requestedTo,
                loadedFrom,
                loadedTo,
                sourceSelectionMode,
                unknownTreatmentMode,
                engineMode,
                selectedRawEvents.size(),
                driverCardRawEvents.size(),
                vehicleUnitRawEvents.size(),
                driverCardRawIntervals.size(),
                vehicleUnitRawIntervals.size(),
                resolvedKnownLoadedIntervals.size(),
                evaluationLoadedIntervals.size(),
                periodizedIntervals.size(),
                mergedIntervals.size(),
                nonDrivingIntervals.size(),
                operatingPeriods.size(),
                dbElapsedMs,
                cardIntervalsElapsedMs,
                vuIntervalsElapsedMs,
                vuGapFillElapsedMs,
                unknownGapElapsedMs,
                periodizeElapsedMs,
                mergeElapsedMs,
                nonDrivingElapsedMs,
                totalElapsedMs);

        return new EsperOperatingPeriodResultDto(
                request.tenantKey(),
                request.driverId(),
                requestedFrom,
                requestedTo,
                loadedFrom,
                loadedTo,
                selectedRawEvents.size(),
                driverCardRawEvents.size(),
                vehicleUnitRawEvents.size(),
                driverCardRawIntervals.size(),
                vehicleUnitRawIntervals.size(),
                resolvedKnownLoadedIntervals.size(),
                evaluationLoadedIntervals.size(),
                periodizedIntervals.size(),
                mergedIntervals.size(),
                nonDrivingIntervals.size(),
                operatingPeriods.size(),
                resolveOperatingSplitIdleHours(request),
                resolveSignificantDrivingMinutes(request),
                resolveMergeGapSeconds(request),
                resolveGapDetectionToleranceSeconds(request),
                sourceSelectionMode,
                unknownTreatmentMode,
                engineMode,
                selectedRawEvents,
                resolvedKnownLoadedIntervals,
                evaluationLoadedIntervals,
                periodizedIntervals,
                mergedIntervals,
                nonDrivingIntervals,
                operatingPeriods,
                notes(
                        engineMode,
                        unknownTreatmentMode,
                        resolveOperatingSplitIdleHours(request),
                        resolveSignificantDrivingMinutes(request),
                        resolveGapDetectionToleranceSeconds(request),
                        sourceSelectionMode
                )
        );
    }

    List<ActivityIntervalDto> synthesizeUnknownGaps(
            List<ActivityIntervalDto> resolvedKnownLoadedIntervals,
            Duration gapDetectionTolerance
    ) {
        List<ActivityIntervalDto> allKnown = sortedPositiveIntervals(resolvedKnownLoadedIntervals);
        List<ActivityIntervalDto> nonRestActivities = allKnown.stream()
                .filter(interval -> !"BREAK_REST".equals(interval.activityType()))
                .toList();
        if (nonRestActivities.isEmpty()) {
            return List.of();
        }

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

    List<OperatingPeriodActivityIntervalDto> mergeConsecutiveActivities(
            List<OperatingPeriodActivityIntervalDto> intervals,
            Duration mergeGapTolerance
    ) {
        if (intervals == null || intervals.isEmpty()) {
            return List.of();
        }
        List<OperatingPeriodActivityIntervalDto> sorted = intervals.stream()
                .filter(interval -> interval.endedAt().isAfter(interval.startedAt()))
                .sorted(Comparator.comparing(OperatingPeriodActivityIntervalDto::startedAt)
                        .thenComparing(OperatingPeriodActivityIntervalDto::endedAt)
                        .thenComparing(OperatingPeriodActivityIntervalDto::activityType, Comparator.nullsLast(String::compareTo)))
                .toList();
        List<OperatingPeriodActivityIntervalDto> result = new ArrayList<>();
        OperatingPeriodActivityIntervalDto current = null;
        List<String> currentSources = new ArrayList<>();
        for (OperatingPeriodActivityIntervalDto next : sorted) {
            if (current == null) {
                current = next;
                currentSources = new ArrayList<>(next.sourceRowIds());
                continue;
            }
            if (canMerge(current, next, mergeGapTolerance)) {
                currentSources.addAll(next.sourceRowIds());
                current = current.asMerged(max(current.endedAt(), next.endedAt()), List.copyOf(currentSources));
            } else {
                result.add(current.asMerged(current.endedAt(), List.copyOf(currentSources)));
                current = next;
                currentSources = new ArrayList<>(next.sourceRowIds());
            }
        }
        if (current != null) {
            result.add(current.asMerged(current.endedAt(), List.copyOf(currentSources)));
        }
        return result;
    }

    List<NonDrivingIntervalDto> buildNonDrivingIntervals(
            List<OperatingPeriodActivityIntervalDto> mergedIntervals,
            Duration significantDrivingThreshold,
            EsperUnknownTreatmentMode unknownTreatmentMode
    ) {
        if (mergedIntervals == null || mergedIntervals.isEmpty()) {
            return List.of();
        }
        List<NonDrivingIntervalDto> result = new ArrayList<>();
        NonDrivingAccumulator open = null;
        for (OperatingPeriodActivityIntervalDto interval : mergedIntervals) {
            if (open != null && open.operatingPeriodNo != interval.operatingPeriodNo()) {
                result.add(open.close("NEW_OPERATING_PERIOD"));
                open = null;
            }
            if (startsOrExtendsNonDriving(interval, unknownTreatmentMode)) {
                open = open == null ? NonDrivingAccumulator.open(interval) : open.extend(interval);
                continue;
            }
            if ("UNKNOWN".equals(interval.activityType()) && unknownTreatmentMode == EsperUnknownTreatmentMode.AS_UNKNOWN_NON_BREAK) {
                if (open != null) {
                    result.add(open.close("UNKNOWN_GAP"));
                    open = null;
                }
                continue;
            }
            if ("DRIVE".equals(interval.activityType())) {
                if (interval.durationSeconds() >= significantDrivingThreshold.getSeconds()) {
                    if (open != null) {
                        result.add(open.close("SIGNIFICANT_DRIVING"));
                        open = null;
                    }
                } else if (open != null) {
                    open = open.extend(interval);
                }
            }
        }
        if (open != null) {
            result.add(open.close("FLUSH"));
        }
        return result;
    }

    private boolean startsOrExtendsNonDriving(
            OperatingPeriodActivityIntervalDto interval,
            EsperUnknownTreatmentMode unknownTreatmentMode
    ) {
        if ("WORK".equals(interval.activityType()) || "AVAILABILITY".equals(interval.activityType())) {
            return true;
        }
        return "UNKNOWN".equals(interval.activityType())
                && unknownTreatmentMode == EsperUnknownTreatmentMode.AS_BREAK_REST;
    }

    private boolean canMerge(
            OperatingPeriodActivityIntervalDto left,
            OperatingPeriodActivityIntervalDto right,
            Duration mergeGapTolerance
    ) {
        long gapSeconds = Duration.between(left.endedAt(), right.startedAt()).getSeconds();
        return Objects.equals(left.driverId(), right.driverId())
                && left.operatingPeriodNo() == right.operatingPeriodNo()
                && Objects.equals(left.activityType(), right.activityType())
                && Objects.equals(left.cardSlot(), right.cardSlot())
                && Objects.equals(left.cardStatus(), right.cardStatus())
                && Objects.equals(left.drivingStatus(), right.drivingStatus())
                && Objects.equals(left.sourceKind(), right.sourceKind())
                && left.synthetic() == right.synthetic()
                && gapSeconds <= mergeGapTolerance.getSeconds();
    }

    private List<OperatingPeriodDto> buildOperatingPeriods(
            List<EsperClosedOperatingPeriod> closedPeriods,
            List<OperatingPeriodActivityIntervalDto> clippedPeriodizedIntervals,
            List<ActivityIntervalDto> clippedKnownActivities,
            OffsetDateTime requestedFrom,
            OffsetDateTime requestedTo,
            Duration mergeGapTolerance,
            Duration significantDrivingThreshold
    ) {
        Map<Long, List<OperatingPeriodActivityIntervalDto>> intervalsByPeriod = new LinkedHashMap<>();
        for (OperatingPeriodActivityIntervalDto interval : clippedPeriodizedIntervals) {
            intervalsByPeriod.computeIfAbsent(interval.operatingPeriodNo(), ignored -> new ArrayList<>()).add(interval);
        }
        List<OperatingPeriodDto> result = new ArrayList<>();
        for (EsperClosedOperatingPeriod 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<OperatingPeriodActivityIntervalDto> intervals = intervalsByPeriod.getOrDefault(closedPeriod.operatingPeriodNo(), List.of());
            List<ActivityIntervalDto> rawActivities = clipKnownActivitiesToPeriod(clippedKnownActivities, start, end);
            long breakRestSeconds = rawActivities.stream()
                    .filter(activity -> "BREAK_REST".equals(activity.activityType()))
                    .mapToLong(ActivityIntervalDto::durationSeconds)
                    .sum();
            ShiftDrivingEvaluationDto drivingEvaluation = evaluateSignificantDrivingWithEsper(
                    rawActivities,
                    mergeGapTolerance,
                    significantDrivingThreshold
            );
            long drivingSeconds = sumActivitySeconds(intervals, "DRIVE");
            long workSeconds = sumActivitySeconds(intervals, "WORK");
            long availabilitySeconds = sumActivitySeconds(intervals, "AVAILABILITY");
            long unknownSeconds = sumActivitySeconds(intervals, "UNKNOWN");
            result.add(new OperatingPeriodDto(
                    closedPeriod.operatingPeriodNo(),
                    start,
                    end,
                    Duration.between(start, end).getSeconds(),
                    closedPeriod.closedBy(),
                    rawActivities,
                    breakRestSeconds,
                    drivingSeconds,
                    workSeconds,
                    availabilitySeconds,
                    unknownSeconds,
                    intervals.size(),
                    drivingEvaluation,
                    !start.equals(closedPeriod.startedAt()) || !end.equals(closedPeriod.endedAt())
            ));
        }
        return result;
    }

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

    private List<OperatingPeriodActivityIntervalDto> clipOperatingIntervals(
            List<OperatingPeriodActivityIntervalDto> intervals,
            OffsetDateTime requestedFrom,
            OffsetDateTime requestedTo
    ) {
        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 interval.withTime(start, end, clipped);
                })
                .filter(Objects::nonNull)
                .toList();
    }

    private List<ActivityIntervalDto> clipKnownActivities(
            List<ActivityIntervalDto> activities,
            OffsetDateTime requestedFrom,
            OffsetDateTime requestedTo
    ) {
        return clipKnownActivitiesToPeriod(activities, requestedFrom, requestedTo);
    }

    private List<ActivityIntervalDto> clipKnownActivitiesToPeriod(
            List<ActivityIntervalDto> activities,
            OffsetDateTime periodFrom,
            OffsetDateTime periodTo
    ) {
        return activities.stream()
                .map(activity -> {
                    OffsetDateTime start = max(activity.startedAt(), periodFrom);
                    OffsetDateTime end = min(activity.endedAt(), periodTo);
                    if (!end.isAfter(start)) {
                        return null;
                    }
                    boolean clipped = activity.clippedToRequestedPeriod()
                            || !start.equals(activity.startedAt())
                            || !end.equals(activity.endedAt());
                    return activity.withTime(start, end, clipped);
                })
                .filter(Objects::nonNull)
                .sorted(Comparator.comparing(ActivityIntervalDto::startedAt)
                        .thenComparing(ActivityIntervalDto::endedAt)
                        .thenComparing(ActivityIntervalDto::activityType, Comparator.nullsLast(String::compareTo)))
                .toList();
    }

    private List<NonDrivingIntervalDto> clipNonDrivingIntervals(
            List<NonDrivingIntervalDto> intervals,
            OffsetDateTime requestedFrom,
            OffsetDateTime requestedTo
    ) {
        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 NonDrivingIntervalDto(
                            interval.driverId(),
                            interval.vehicleId(),
                            interval.vehicleRegistrationId(),
                            interval.cardSlot(),
                            start,
                            end,
                            Duration.between(start, end).getSeconds(),
                            interval.operatingPeriodNo(),
                            interval.operatingPeriodStartedAt(),
                            interval.closedBy(),
                            interval.containsUnknown(),
                            interval.unknownSeconds(),
                            !start.equals(interval.startedAt()) || !end.equals(interval.endedAt())
                    );
                })
                .filter(Objects::nonNull)
                .toList();
    }

    private ActivityIntervalDto unknownGapTemplate(
            ActivityIntervalDto previous,
            ActivityIntervalDto next,
            OffsetDateTime gapStart,
            OffsetDateTime gapEnd
    ) {
        return new ActivityIntervalDto(
                previous.driverEntityId(),
                Objects.equals(previous.vehicleId(), next.vehicleId()) ? previous.vehicleId() : null,
                Objects.equals(previous.vehicleRegistrationId(), next.vehicleRegistrationId()) ? previous.vehicleRegistrationId() : null,
                "UNKNOWN",
                Objects.equals(previous.cardSlot(), next.cardSlot()) ? previous.cardSlot() : null,
                previous.cardStatus(),
                "UNKNOWN",
                "SYNTHETIC_GAP",
                gapStart,
                gapEnd,
                Duration.between(gapStart, gapEnd).getSeconds(),
                null,
                List.of(),
                false,
                "UNKNOWN_GAP"
        );
    }

    private List<ActivityIntervalDto> resolveVuFillGaps(
            List<ActivityIntervalDto> driverCardRawIntervals,
            List<ActivityIntervalDto> vehicleUnitRawIntervals
    ) {
        List<ActivityIntervalDto> driverCard = sortedPositiveIntervals(driverCardRawIntervals);
        List<ActivityIntervalDto> vehicleUnit = sortedPositiveIntervals(vehicleUnitRawIntervals);
        if (driverCard.isEmpty()) {
            return vehicleUnit;
        }
        if (vehicleUnit.isEmpty()) {
            return driverCard;
        }

        List<ActivityIntervalDto> resolved = new ArrayList<>(driverCard);
        for (ActivityIntervalDto vuInterval : vehicleUnit) {
            resolved.addAll(subtractCoverage(vuInterval, driverCard));
        }
        return resolved.stream()
                .sorted(Comparator.comparing(ActivityIntervalDto::startedAt)
                        .thenComparing(ActivityIntervalDto::endedAt)
                        .thenComparing(ActivityIntervalDto::sourceKind, Comparator.nullsLast(String::compareTo)))
                .toList();
    }

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

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

    private ShiftDrivingEvaluationDto evaluateSignificantDrivingWithEsper(
            List<ActivityIntervalDto> rawActivities,
            Duration mergeGapTolerance,
            Duration significantDrivingThreshold
    ) {
        if (rawActivities.isEmpty()) {
            return new ShiftDrivingEvaluationDto(
                    (int) significantDrivingThreshold.toMinutes(),
                    null,
                    null,
                    null,
                    null,
                    List.of()
            );
        }
        List<ActivityIntervalDto> mergedActivities = activityEngine.mergeConsecutiveIdenticalActivities(
                rawActivities,
                mergeGapTolerance
        );
        List<ActivityIntervalDto> significantDrivingPeriods = mergedActivities.stream()
                .filter(activity -> "DRIVE".equals(activity.activityType()))
                .filter(activity -> activity.durationSeconds() > significantDrivingThreshold.getSeconds())
                .sorted(Comparator.comparing(ActivityIntervalDto::startedAt))
                .toList();
        if (significantDrivingPeriods.isEmpty()) {
            return new ShiftDrivingEvaluationDto(
                    (int) significantDrivingThreshold.toMinutes(),
                    null,
                    null,
                    null,
                    null,
                    List.of()
            );
        }
        List<DrivingInterruptionDto> interruptions = new ArrayList<>();
        for (int index = 1; index < significantDrivingPeriods.size(); index++) {
            ActivityIntervalDto previous = significantDrivingPeriods.get(index - 1);
            ActivityIntervalDto next = significantDrivingPeriods.get(index);
            if (next.startedAt().isAfter(previous.endedAt())) {
                interruptions.add(new DrivingInterruptionDto(
                        previous.endedAt(),
                        next.startedAt(),
                        Duration.between(previous.endedAt(), next.startedAt()).getSeconds(),
                        previous.sourceRowId(),
                        next.sourceRowId()
                ));
            }
        }
        ActivityIntervalDto first = significantDrivingPeriods.get(0);
        ActivityIntervalDto last = significantDrivingPeriods.get(significantDrivingPeriods.size() - 1);
        return new ShiftDrivingEvaluationDto(
                (int) significantDrivingThreshold.toMinutes(),
                first.startedAt(),
                last.endedAt(),
                first,
                last,
                interruptions
        );
    }

    private int resolveOperatingSplitIdleHours(EsperOperatingPeriodRequest request) {
        if (request.operatingSplitIdleHours() != null) {
            return request.operatingSplitIdleHours();
        }
        return properties == null ? 7 : properties.getEsperPoc().getOperatingPeriodEvaluation().getOperatingSplitIdleHours();
    }

    private int resolveSignificantDrivingMinutes(EsperOperatingPeriodRequest request) {
        if (request.significantDrivingMinutes() != null) {
            return request.significantDrivingMinutes();
        }
        return properties == null ? 3 : properties.getEsperPoc().getOperatingPeriodEvaluation().getSignificantDrivingMinutes();
    }

    private int resolveMergeGapSeconds(EsperOperatingPeriodRequest request) {
        if (request.mergeGapSeconds() != null) {
            return request.mergeGapSeconds();
        }
        return properties == null ? 0 : properties.getEsperPoc().getOperatingPeriodEvaluation().getMergeGapSeconds();
    }

    private int resolveGapDetectionToleranceSeconds(EsperOperatingPeriodRequest request) {
        if (request.gapDetectionToleranceSeconds() != null) {
            return request.gapDetectionToleranceSeconds();
        }
        return properties == null ? 0 : properties.getEsperPoc().getOperatingPeriodEvaluation().getGapDetectionToleranceSeconds();
    }

    private EsperUnknownTreatmentMode resolveUnknownTreatmentMode(EsperOperatingPeriodRequest request) {
        if (request.unknownTreatmentMode() != null) {
            return request.unknownTreatmentMode();
        }
        return properties == null
                ? EsperUnknownTreatmentMode.AS_BREAK_REST
                : properties.getEsperPoc().getOperatingPeriodEvaluation().getUnknownTreatmentMode();
    }

    private EsperOperatingPeriodEngineMode resolveEngineMode(EsperOperatingPeriodRequest request) {
        if (request.engineMode() != null) {
            return request.engineMode();
        }
        return properties == null
                ? EsperOperatingPeriodEngineMode.STREAM_COLLECTOR
                : properties.getEsperPoc().getOperatingPeriodEvaluation().getEngineMode();
    }

    private EsperSourceSelectionMode resolveSourceSelectionMode(EsperOperatingPeriodRequest request) {
        return request.sourceSelectionMode() == null ? EsperSourceSelectionMode.MIXED : request.sourceSelectionMode();
    }

    private List<String> notes(
            EsperOperatingPeriodEngineMode engineMode,
            EsperUnknownTreatmentMode unknownTreatmentMode,
            int operatingSplitIdleHours,
            int significantDrivingMinutes,
            int gapDetectionToleranceSeconds,
            EsperSourceSelectionMode sourceSelectionMode
    ) {
        return List.of(
                "This endpoint runs in parallel to the existing working-shift PoC and does not change its semantics.",
                "Source selection mode is " + sourceSelectionMode + ".",
                "BREAK_REST events are ignored for activity evaluation but still prevent synthetic UNKNOWN intervals from being created over covered rest spans.",
                "Synthetic UNKNOWN intervals are created only for uncovered gaps between non-rest activities.",
                "UNKNOWN treatment mode is " + unknownTreatmentMode + ".",
                "Operating-period engine mode is " + engineMode + ".",
                "Operating periods split after " + operatingSplitIdleHours + " hours of no non-rest activity; significant driving closes non-driving intervals from " + significantDrivingMinutes + " minutes onward.",
                "Synthetic UNKNOWN gaps are only emitted when uncovered time exceeds " + gapDetectionToleranceSeconds + " seconds."
        );
    }

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

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

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

    private long elapsedMillis(long startedNanos) {
        return Math.round((System.nanoTime() - startedNanos) / 1_000_000.0d);
    }

    private static final class NonDrivingAccumulator {
        private final UUID driverId;
        private UUID vehicleId;
        private UUID vehicleRegistrationId;
        private final String cardSlot;
        private final OffsetDateTime startedAt;
        private OffsetDateTime endedAt;
        private final long operatingPeriodNo;
        private final OffsetDateTime operatingPeriodStartedAt;
        private boolean containsUnknown;
        private long unknownSeconds;

        private NonDrivingAccumulator(OperatingPeriodActivityIntervalDto interval) {
            this.driverId = interval.driverId();
            this.vehicleId = interval.vehicleId();
            this.vehicleRegistrationId = interval.vehicleRegistrationId();
            this.cardSlot = interval.cardSlot();
            this.startedAt = interval.startedAt();
            this.endedAt = interval.endedAt();
            this.operatingPeriodNo = interval.operatingPeriodNo();
            this.operatingPeriodStartedAt = interval.operatingPeriodStartedAt();
            this.containsUnknown = "UNKNOWN".equals(interval.activityType());
            this.unknownSeconds = "UNKNOWN".equals(interval.activityType()) ? interval.durationSeconds() : 0L;
        }

        private static NonDrivingAccumulator open(OperatingPeriodActivityIntervalDto interval) {
            return new NonDrivingAccumulator(interval);
        }

        private NonDrivingAccumulator extend(OperatingPeriodActivityIntervalDto interval) {
            if (interval.vehicleId() != null) {
                this.vehicleId = interval.vehicleId();
            }
            if (interval.vehicleRegistrationId() != null) {
                this.vehicleRegistrationId = interval.vehicleRegistrationId();
            }
            if (interval.endedAt().isAfter(this.endedAt)) {
                this.endedAt = interval.endedAt();
            }
            if ("UNKNOWN".equals(interval.activityType())) {
                this.containsUnknown = true;
                this.unknownSeconds += interval.durationSeconds();
            }
            return this;
        }

        private NonDrivingIntervalDto close(String closedBy) {
            return new NonDrivingIntervalDto(
                    driverId,
                    vehicleId,
                    vehicleRegistrationId,
                    cardSlot,
                    startedAt,
                    endedAt,
                    Duration.between(startedAt, endedAt).getSeconds(),
                    operatingPeriodNo,
                    operatingPeriodStartedAt,
                    closedBy,
                    containsUnknown,
                    unknownSeconds,
                    false
            );
        }
    }
}
