package at.procon.eventhub.processing.service;

import at.procon.eventhub.config.EventHubProperties;
import at.procon.eventhub.dto.DriverRefDto;
import at.procon.eventhub.dto.EventHubEventDto;
import at.procon.eventhub.processing.dto.UnifiedRuntimeDerivedProjectionResultDto;
import at.procon.eventhub.processing.dto.RuntimeSupportEvidenceNormalizationDebugDto;
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeActivityInterval;
import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeProcessingInput;
import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeVehicleUsageInterval;
import at.procon.eventhub.processing.driverworkingtime.service.DriverWorkingTimeDerivedProjectionEngine;
import at.procon.eventhub.processing.driverworkingtime.service.DriverWorkingTimeProcessingCore;
import at.procon.eventhub.processing.driverworkingtime.service.DriverWorkingTimeReusableProjectionBuilder;
import at.procon.eventhub.processing.eventprocessing.support.RuntimeSupportEvidenceNormalizationResult;
import at.procon.eventhub.processing.eventprocessing.support.RuntimeSupportEvidenceEvent;
import at.procon.eventhub.processing.eventprocessing.support.RuntimeSupportEvidenceNormalizer;
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBundle;
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
import at.procon.eventhub.processing.driverworkingtime.dto.DriverWorkingTimeProcessingResultDto;
import at.procon.eventhub.tachographfilesession.service.TachographDriverWorkingTimeAdapter;
import at.procon.eventhub.tachographfilesession.model.ExtractedSupportEvent;
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.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 at.procon.eventhub.processing.support.TachographRuntimeIdentityResolver;
import java.time.Duration;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Comparator;
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 UnifiedRuntimeDerivedProjectionService {

    private final UnifiedRuntimeEventAssemblyService runtimeEventAssemblyService;
    private final UnifiedEventTimelineReconstructor timelineReconstructor;
    private final DriverWorkingTimeProcessingCore workingTimeProcessingCore;
    private final RuntimeSupportEvidenceNormalizer supportEvidenceNormalizer;
    private final EventHubProperties properties;

    public UnifiedRuntimeDerivedProjectionService(
            UnifiedRuntimeEventAssemblyService runtimeEventAssemblyService,
            UnifiedEventTimelineReconstructor timelineReconstructor,
            EventHubProperties properties,
            DriverWorkingTimeReusableProjectionBuilder reusableProjectionBuilder,
            RuntimeSupportEvidenceNormalizer supportEvidenceNormalizer
    ) {
        this(
                runtimeEventAssemblyService,
                timelineReconstructor,
                properties,
                new DriverWorkingTimeProcessingCore(new DriverWorkingTimeDerivedProjectionEngine(
                        reusableProjectionBuilder
                )),
                supportEvidenceNormalizer
        );
    }

    @Autowired
    public UnifiedRuntimeDerivedProjectionService(
            UnifiedRuntimeEventAssemblyService runtimeEventAssemblyService,
            UnifiedEventTimelineReconstructor timelineReconstructor,
            EventHubProperties properties,
            DriverWorkingTimeProcessingCore workingTimeProcessingCore,
            RuntimeSupportEvidenceNormalizer supportEvidenceNormalizer
    ) {
        this.runtimeEventAssemblyService = runtimeEventAssemblyService;
        this.timelineReconstructor = timelineReconstructor;
        this.properties = properties;
        this.workingTimeProcessingCore = workingTimeProcessingCore;
        this.supportEvidenceNormalizer = supportEvidenceNormalizer;
    }

    public UnifiedRuntimeDerivedProjectionResultDto loadDriverDerivedProjections(
            UnifiedRuntimeProcessingApiRequest apiRequest
    ) {
        UnifiedRuntimeProcessingRequest request = apiRequest.toRuntimeRequest();
        UnifiedRuntimeEventBundle eventBundle = runtimeEventAssemblyService.assembleDriverScopedEvents(request);
        return buildDriverDerivedProjection(apiRequest, request, eventBundle, null);
    }

    public UnifiedRuntimeDerivedProjectionResultDto buildDriverDerivedProjection(
            UnifiedRuntimeProcessingApiRequest apiRequest,
            UnifiedRuntimeProcessingRequest request,
            UnifiedRuntimeEventBundle eventBundle,
            String explicitDriverKey
    ) {
        String driverKey = explicitDriverKey == null
                ? resolveDriverKey(request, eventBundle.mergedEvents())
                : explicitDriverKey;
        RuntimeSupportEvidenceNormalizationResult normalizationResult = supportEvidenceNormalizer.normalizeForTachographDriver(
                driverKey,
                eventBundle.mergedEvents()
        );
        List<EventHubEventDto> normalizedEvents = normalizationResult.normalizedEvents();
        ResolvedDriverTimeline timeline = timelineReconstructor.reconstruct(
                runtimeSessionId(request),
                driverKey,
                normalizedEvents
        );

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

        int significantDrivingMinutes = apiRequest.significantDrivingMinutes() == null
                ? processingProperties().getSignificantDrivingMinutes()
                : Math.max(1, apiRequest.significantDrivingMinutes());
        int minimumRestPeriodMinutes = apiRequest.minimumRestPeriodMinutes() == null
                ? processingProperties().getMinimumRestPeriodMinutes()
                : Math.max(1, apiRequest.minimumRestPeriodMinutes());

        List<String> notes = new ArrayList<>(eventBundle.notes());
        notes.addAll(normalizationResult.notes());
        notes.add("Runtime derived projections were evaluated from the unified merged event stream using normalized support evidence and the shared driver working-time processing core.");
        notes.add("Significant driving threshold minutes: " + significantDrivingMinutes + ".");
        notes.add("Minimum rest candidate period minutes: " + minimumRestPeriodMinutes + ".");
        if (request.occurredFrom() != null || request.occurredTo() != null) {
            notes.add("Projection results are filtered to the requested runtime window. For intervals crossing the boundary, include enough source-event padding in the request.");
        }

        DriverWorkingTimeProcessingInput processingInput = new DriverWorkingTimeProcessingInput(
                runtimeSessionId(request),
                driverKey,
                timeline.sourceKind(),
                timeline.loadedFrom(),
                timeline.loadedTo(),
                requestedFrom,
                requestedTo,
                significantDrivingMinutes,
                minimumRestPeriodMinutes,
                timeline.activityIntervals().stream()
                        .map(interval -> TachographDriverWorkingTimeAdapter.toActivityInterval(
                                runtimeSessionId(request),
                                driverKey,
                                interval
                        ))
                        .filter(Objects::nonNull)
                        .toList(),
                timeline.vehicleUsageIntervals().stream()
                        .map(TachographDriverWorkingTimeAdapter::toVehicleUsageInterval)
                        .filter(Objects::nonNull)
                        .toList(),
                timeline.supportEvents().stream()
                        .map(this::toSupportEvidenceEvent)
                        .filter(Objects::nonNull)
                        .toList(),
                notes
        );
        DriverWorkingTimeProcessingResultDto projection = workingTimeProcessingCore.process(processingInput);
        notes = projection.notes();

        RuntimeSupportEvidenceNormalizationDebugDto normalizationDebug = new RuntimeSupportEvidenceNormalizationDebugDto(
                normalizationResult.inputEventCount(),
                normalizationResult.normalizedSupportEvidenceEventCount(),
                normalizationResult.unchangedEventCount(),
                normalizationResult.notes()
        );

        return new UnifiedRuntimeDerivedProjectionResultDto(
                request,
                eventBundle.driverSeedEvents().size(),
                eventBundle.discoveredVehicles().size(),
                eventBundle.expandedVehicleEvents().size(),
                eventBundle.mergedEvents().size(),
                eventBundle.discoveredVehicles(),
                projection,
                notes,
                normalizationDebug
        );
    }

    private EventHubProperties.Processing processingProperties() {
        return properties.getTachographFileSession().getProcessing();
    }

    private UUID runtimeSessionId(UnifiedRuntimeProcessingRequest request) {
        if (request.compositeSessionId() != null || request.sessionIds().size() > 1) {
            return null;
        }
        return request.sessionIds().size() == 1 ? request.sessionIds().get(0) : request.sessionId();
    }

    private RuntimeSupportEvidenceEvent toSupportEvidenceEvent(ExtractedSupportEvent supportEvent) {
        if (supportEvent == null || supportEvent.occurredAt() == null) {
            return null;
        }
        return new RuntimeSupportEvidenceEvent(
                supportEvent.eventId(),
                null,
                null,
                supportEvent.eventDomain(),
                supportEvent.eventType(),
                supportEvent.eventLifecycle(),
                supportEvent.driverKey(),
                supportEvent.vehicleKey(),
                supportEvent.registrationKey(),
                supportEvent.occurredAt(),
                supportEvent.occurredAt().toEpochSecond(),
                supportEvent.latitude(),
                supportEvent.longitude(),
                supportEvent.country(),
                supportEvent.region(),
                supportEvent.countryFrom(),
                supportEvent.countryTo(),
                supportEvent.operation(),
                supportEvent.odometerKm(),
                supportEvent.avgSpeedKmh(),
                supportEvent.maxSpeedKmh(),
                java.util.Map.of("rawRecordPath", supportEvent.rawRecordPath())
        );
    }

    private String resolveDriverKey(
            UnifiedRuntimeProcessingRequest request,
            List<EventHubEventDto> events
    ) {
        return TachographRuntimeIdentityResolver.requestDriverKey(request, events);
    }


    private List<TachographEsperVehicleUsageIntervalEvent> mergeVehicleUsageIntervals(
            List<TachographEsperVehicleUsageIntervalEvent> intervals
    ) {
        if (intervals == null || intervals.isEmpty()) {
            return List.of();
        }
        List<TachographEsperVehicleUsageIntervalEvent> sorted = intervals.stream()
                .sorted(Comparator.comparing(TachographEsperVehicleUsageIntervalEvent::startedAt)
                        .thenComparing(TachographEsperVehicleUsageIntervalEvent::endedAt, Comparator.nullsLast(Comparator.naturalOrder()))
                        .thenComparing(TachographEsperVehicleUsageIntervalEvent::intervalId, Comparator.nullsLast(String::compareTo)))
                .toList();
        List<TachographEsperVehicleUsageIntervalEvent> merged = new ArrayList<>();
        for (TachographEsperVehicleUsageIntervalEvent next : sorted) {
            if (merged.isEmpty()) {
                merged.add(next);
                continue;
            }
            TachographEsperVehicleUsageIntervalEvent current = merged.get(merged.size() - 1);
            if (canMergeVehicleUsage(current, next)) {
                merged.set(merged.size() - 1, mergeVehicleUsage(current, next));
            } else {
                merged.add(next);
            }
        }
        return List.copyOf(merged);
    }

    private boolean canMergeVehicleUsage(
            TachographEsperVehicleUsageIntervalEvent left,
            TachographEsperVehicleUsageIntervalEvent right
    ) {
        if (left == null || right == null || left.endedAt() == null || right.startedAt() == null) {
            return false;
        }
        return Objects.equals(left.driverKey(), right.driverKey())
                && Objects.equals(left.registrationKey(), right.registrationKey())
                && Objects.equals(left.vehicleKey(), right.vehicleKey())
                && !right.startedAt().isAfter(left.endedAt().plusSeconds(1));
    }

    private TachographEsperVehicleUsageIntervalEvent mergeVehicleUsage(
            TachographEsperVehicleUsageIntervalEvent left,
            TachographEsperVehicleUsageIntervalEvent right
    ) {
        List<String> sourceIntervalIds = new ArrayList<>();
        if (left.sourceIntervalIds() != null) {
            sourceIntervalIds.addAll(left.sourceIntervalIds());
        }
        if (right.sourceIntervalIds() != null) {
            for (String sourceIntervalId : right.sourceIntervalIds()) {
                if (!sourceIntervalIds.contains(sourceIntervalId)) {
                    sourceIntervalIds.add(sourceIntervalId);
                }
            }
        }
        OffsetDateTime end = right.endedAt() == null || right.endedAt().isBefore(left.endedAt())
                ? left.endedAt()
                : right.endedAt();
        return new TachographEsperVehicleUsageIntervalEvent(
                left.sessionId(),
                left.driverKey(),
                left.intervalId(),
                left.startedAt(),
                end,
                Duration.between(left.startedAt(), end).getSeconds(),
                left.odometerBeginKm(),
                right.odometerEndKm() == null ? left.odometerEndKm() : right.odometerEndKm(),
                left.registrationKey(),
                left.vehicleKey(),
                left.sourceKind(),
                sourceIntervalIds
        );
    }

    private List<TachographEsperActivityIntervalEvent> clipActivityIntervals(
            List<TachographEsperActivityIntervalEvent> intervals,
            OffsetDateTime requestedFrom,
            OffsetDateTime requestedTo
    ) {
        if (requestedFrom == null || requestedTo == null) {
            return intervals == null ? List.of() : List.copyOf(intervals);
        }
        return (intervals == null ? List.<TachographEsperActivityIntervalEvent>of() : intervals).stream()
                .map(interval -> {
                    if (!intersects(interval.startedAt(), interval.endedAt(), requestedFrom, requestedTo)) {
                        return null;
                    }
                    OffsetDateTime start = max(interval.startedAt(), requestedFrom);
                    OffsetDateTime end = min(interval.endedAt(), requestedTo);
                    if (start == null || end == null || !end.isAfter(start)) {
                        return null;
                    }
                    boolean clipped = interval.clippedToRequestedPeriod()
                            || !Objects.equals(start, interval.startedAt())
                            || !Objects.equals(end, 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))
                .toList();
    }

    private List<TachographEsperVehicleUsageIntervalEvent> clipVehicleUsageIntervals(
            List<TachographEsperVehicleUsageIntervalEvent> intervals,
            OffsetDateTime requestedFrom,
            OffsetDateTime requestedTo
    ) {
        if (requestedFrom == null || requestedTo == null) {
            return intervals == null ? List.of() : List.copyOf(intervals);
        }
        return (intervals == null ? List.<TachographEsperVehicleUsageIntervalEvent>of() : intervals).stream()
                .map(interval -> {
                    if (!intersects(interval.startedAt(), interval.endedAt(), requestedFrom, requestedTo)) {
                        return null;
                    }
                    OffsetDateTime start = max(interval.startedAt(), requestedFrom);
                    OffsetDateTime end = min(interval.endedAt(), requestedTo);
                    if (start == null || end == null || !end.isAfter(start)) {
                        return null;
                    }
                    boolean startClipped = !Objects.equals(start, interval.startedAt());
                    boolean endClipped = !Objects.equals(end, 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))
                .toList();
    }

    private List<TachographEsperDrivingInterruptionIntervalEvent> clipDrivingIntervals(
            List<TachographEsperDrivingInterruptionIntervalEvent> intervals,
            OffsetDateTime requestedFrom,
            OffsetDateTime requestedTo
    ) {
        return filterIntersecting(
                intervals,
                requestedFrom,
                requestedTo,
                TachographEsperDrivingInterruptionIntervalEvent::startedAt,
                TachographEsperDrivingInterruptionIntervalEvent::endedAt
        );
    }

    private List<TachographEsperVuCardAbsentIntervalEvent> clipVuCardAbsentIntervals(
            List<TachographEsperVuCardAbsentIntervalEvent> intervals,
            OffsetDateTime requestedFrom,
            OffsetDateTime requestedTo
    ) {
        return filterIntersecting(
                intervals,
                requestedFrom,
                requestedTo,
                TachographEsperVuCardAbsentIntervalEvent::startedAt,
                TachographEsperVuCardAbsentIntervalEvent::endedAt
        );
    }

    private List<TachographEsperSupportGeoEvent> clipSupportGeoEvents(
            List<ExtractedSupportEvent> supportEvents,
            String driverKey,
            OffsetDateTime requestedFrom,
            OffsetDateTime requestedTo
    ) {
        return (supportEvents == null ? List.<ExtractedSupportEvent>of() : supportEvents).stream()
                .filter(event -> event.occurredAt() != null)
                .filter(event -> driverKey == null || event.driverKey() == null || Objects.equals(driverKey, event.driverKey()))
                .filter(event -> requestedFrom == null || !event.occurredAt().isBefore(requestedFrom))
                .filter(event -> requestedTo == null || !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::eventId, Comparator.nullsLast(String::compareTo)))
                .toList();
    }

    private <T> List<T> filterIntersecting(
            List<T> intervals,
            OffsetDateTime requestedFrom,
            OffsetDateTime requestedTo,
            TimeAccessor<T> startAccessor,
            TimeAccessor<T> endAccessor
    ) {
        if (intervals == null || intervals.isEmpty()) {
            return List.of();
        }
        if (requestedFrom == null || requestedTo == null) {
            return List.copyOf(intervals);
        }
        return intervals.stream()
                .filter(interval -> intersects(startAccessor.get(interval), endAccessor.get(interval), requestedFrom, requestedTo))
                .toList();
    }

    private boolean intersects(
            OffsetDateTime intervalStart,
            OffsetDateTime intervalEnd,
            OffsetDateTime requestedFrom,
            OffsetDateTime requestedTo
    ) {
        if (intervalStart == null || intervalEnd == null || requestedFrom == null || requestedTo == null) {
            return false;
        }
        return intervalEnd.isAfter(requestedFrom) && intervalStart.isBefore(requestedTo);
    }

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

    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 utc(OffsetDateTime value) {
        return value == null ? null : value.withOffsetSameInstant(ZoneOffset.UTC);
    }

    @FunctionalInterface
    private interface TimeAccessor<T> {
        OffsetDateTime get(T value);
    }
}
