package at.procon.eventhub.tachographfilesession.service;

import at.procon.eventhub.config.EventHubProperties;
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.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.TachographFileSession;
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.stereotype.Service;

@Service
public class TachographFileSessionProcessingService {

    private final TachographFileSessionRepository repository;
    private final DriverTimelineBuilder driverTimelineBuilder;
    private final EventHubProperties properties;

    public TachographFileSessionProcessingService(
            TachographFileSessionRepository repository,
            DriverTimelineBuilder driverTimelineBuilder,
            EventHubProperties properties
    ) {
        this.repository = repository;
        this.driverTimelineBuilder = driverTimelineBuilder;
        this.properties = properties;
    }

    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 = driverTimelineBuilder.build(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
    ) {
        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 = driverTimelineBuilder.build(session, driver);
        List<TachographEsperActivityIntervalEvent> activityIntervals =
                driverTimelineBuilder.buildEsperActivityIntervalEvents(sessionId, driverKey, timeline);
        List<TachographEsperActivityIntervalEvent> drivingIntervals =
                driverTimelineBuilder.buildEsperDrivingIntervalEvents(sessionId, driverKey, timeline);
        List<TachographEsperVehicleUsageIntervalEvent> vehicleUsageIntervals =
                driverTimelineBuilder.buildEsperVehicleUsageIntervalEvents(timeline);
        List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals =
                driverTimelineBuilder.buildEsperVuCardAbsentIntervalEvents(timeline);

        return new TachographEsperDriverProcessingResultDto(
                sessionId,
                driverKey,
                timeline.sourceKind(),
                timeline.loadedFrom(),
                timeline.loadedTo(),
                activityIntervals.size(),
                drivingIntervals.size(),
                vehicleUsageIntervals.size(),
                vuCardAbsentIntervals.size(),
                activityIntervals,
                drivingIntervals,
                vehicleUsageIntervals,
                vuCardAbsentIntervals,
                esperProjectionNotes()
        );
    }

    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 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.",
                "VU card-absent intervals are gaps between consecutive normalized vehicle-usage intervals for the same driver."
        );
    }

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

    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
    ) {
    }
}
