package at.procon.eventhub.processing.service;

import at.procon.eventhub.dto.EventHubEventDto;
import at.procon.eventhub.dto.VehicleRefDto;
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventScopeClassifier;
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventScopeType;
import at.procon.eventhub.processing.model.RuntimeDriverVehicleEvidenceAttachmentResult;
import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline;
import at.procon.eventhub.tachographfilesession.model.ResolvedVehicleUsageInterval;
import com.fasterxml.jackson.databind.JsonNode;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import org.springframework.stereotype.Service;

@Service
public class RuntimeDriverVehicleEvidenceAttachmentService {

    private final UnifiedEventTimelineReconstructor timelineReconstructor;
    private final RuntimeEventScopeClassifier scopeClassifier;

    public RuntimeDriverVehicleEvidenceAttachmentService(
            UnifiedEventTimelineReconstructor timelineReconstructor,
            RuntimeEventScopeClassifier scopeClassifier
    ) {
        this.timelineReconstructor = timelineReconstructor;
        this.scopeClassifier = scopeClassifier;
    }

    public RuntimeDriverVehicleEvidenceAttachmentResult attachVehicleEvidence(
            String driverKey,
            List<EventHubEventDto> directDriverEvents,
            List<EventHubEventDto> runtimeScopeEvents,
            boolean attachVehicleOnlyEvents,
            int vehicleEvidencePaddingMinutes
    ) {
        List<EventHubEventDto> safeDriverEvents = directDriverEvents == null ? List.of() : List.copyOf(directDriverEvents);
        List<EventHubEventDto> safeScopeEvents = runtimeScopeEvents == null ? List.of() : List.copyOf(runtimeScopeEvents);
        int paddingMinutes = Math.max(0, vehicleEvidencePaddingMinutes);

        List<String> notes = new ArrayList<>();
        List<String> warnings = new ArrayList<>();
        if (!attachVehicleOnlyEvents) {
            notes.add("Vehicle-only evidence attachment is disabled for driver partition " + driverKey + ".");
            return new RuntimeDriverVehicleEvidenceAttachmentResult(
                    driverKey,
                    safeDriverEvents,
                    List.of(),
                    deduplicateAndSort(safeDriverEvents, List.of()),
                    0,
                    0,
                    0,
                    notes,
                    warnings
            );
        }

        ResolvedDriverTimeline timeline = timelineReconstructor.reconstruct(null, driverKey, safeDriverEvents);
        List<ResolvedVehicleUsageInterval> usageIntervals = mergeVehicleUsageIntervals(timeline.vehicleUsageIntervals());
        List<EventHubEventDto> candidateVehicleEvidence = safeScopeEvents.stream()
                .filter(event -> scopeClassifier.classify(event) == RuntimeEventScopeType.VEHICLE_SCOPED)
                .toList();
        List<EventHubEventDto> attached = new ArrayList<>();
        int ignored = 0;
        for (EventHubEventDto vehicleEvent : candidateVehicleEvidence) {
            List<ResolvedVehicleUsageInterval> matchingIntervals = matchingUsageIntervals(vehicleEvent, usageIntervals, paddingMinutes);
            if (matchingIntervals.isEmpty()) {
                ignored++;
                continue;
            }
            attached.add(vehicleEvent);
            if (matchingIntervals.size() > 1) {
                warnings.add("Vehicle-only event " + vehicleEvent.externalSourceEventId()
                        + " matched multiple vehicle-usage intervals for driver " + driverKey
                        + "; it was attached once after deduplication.");
            }
        }

        notes.add("Vehicle-only evidence attachment used " + usageIntervals.size()
                + " reconstructed vehicle-usage interval(s) for driver " + driverKey + ".");
        notes.add("Vehicle-only evidence padding minutes: " + paddingMinutes + ".");
        notes.add("Candidate vehicle-only evidence events: " + candidateVehicleEvidence.size() + ".");
        notes.add("Attached vehicle-only evidence events: " + attached.size() + ".");
        notes.add("Ignored vehicle-only evidence events: " + ignored + ".");
        if (usageIntervals.isEmpty() && !candidateVehicleEvidence.isEmpty()) {
            warnings.add("Vehicle-only evidence was available for driver " + driverKey
                    + ", but no driver vehicle-usage intervals were reconstructed; no vehicle-only evidence was attached.");
        }

        return new RuntimeDriverVehicleEvidenceAttachmentResult(
                driverKey,
                safeDriverEvents,
                attached,
                deduplicateAndSort(safeDriverEvents, attached),
                usageIntervals.size(),
                candidateVehicleEvidence.size(),
                ignored,
                notes,
                warnings
        );
    }

    private List<ResolvedVehicleUsageInterval> matchingUsageIntervals(
            EventHubEventDto vehicleEvent,
            List<ResolvedVehicleUsageInterval> usageIntervals,
            int paddingMinutes
    ) {
        if (vehicleEvent == null || vehicleEvent.occurredAt() == null || usageIntervals == null || usageIntervals.isEmpty()) {
            return List.of();
        }
        List<ResolvedVehicleUsageInterval> result = new ArrayList<>();
        for (ResolvedVehicleUsageInterval interval : usageIntervals) {
            if (matchesVehicle(vehicleEvent, interval) && timeInside(vehicleEvent.occurredAt(), interval, paddingMinutes)) {
                result.add(interval);
            }
        }
        return List.copyOf(result);
    }

    private boolean timeInside(OffsetDateTime occurredAt, ResolvedVehicleUsageInterval interval, int paddingMinutes) {
        if (occurredAt == null || interval == null || interval.from() == null) {
            return false;
        }
        OffsetDateTime from = interval.from().minusMinutes(paddingMinutes);
        OffsetDateTime to = interval.to() == null ? OffsetDateTime.MAX : interval.to().plusMinutes(paddingMinutes);
        return !occurredAt.isBefore(from) && !occurredAt.isAfter(to);
    }

    private boolean matchesVehicle(EventHubEventDto event, ResolvedVehicleUsageInterval interval) {
        if (event == null || interval == null) {
            return false;
        }
        Set<String> eventKeys = vehicleKeys(event);
        Set<String> intervalKeys = vehicleKeys(interval);
        if (eventKeys.isEmpty() || intervalKeys.isEmpty()) {
            return false;
        }
        return eventKeys.stream().anyMatch(intervalKeys::contains);
    }

    private Set<String> vehicleKeys(EventHubEventDto event) {
        LinkedHashSet<String> result = new LinkedHashSet<>();
        JsonNode raw = rawPayload(event);
        add(result, text(raw, "vehicleKey"));
        add(result, text(raw, "registrationKey"));
        VehicleRefDto vehicleRef = event.vehicleRef();
        if (vehicleRef != null) {
            add(result, vehicleRef.sourceVehicleEntityId());
            add(result, vehicleRef.sourceRegistrationEntityId());
            add(result, vehicleRef.vin());
            if (vehicleRef.vin() != null) {
                add(result, "VIN:" + vehicleRef.vin());
            }
            if (vehicleRef.vehicleRegistration() != null) {
                String registrationKey = vehicleRef.vehicleRegistration().stableKey();
                add(result, registrationKey);
                add(result, "VR:" + registrationKey);
            }
        }
        return Set.copyOf(result);
    }

    private Set<String> vehicleKeys(ResolvedVehicleUsageInterval interval) {
        LinkedHashSet<String> result = new LinkedHashSet<>();
        add(result, interval.vehicleKey());
        add(result, interval.registrationKey());
        if (interval.vehicleKey() != null) {
            add(result, "VIN:" + interval.vehicleKey());
        }
        if (interval.registrationKey() != null) {
            add(result, "VR:" + interval.registrationKey());
        }
        return Set.copyOf(result);
    }

    private void add(Set<String> keys, String value) {
        if (value != null && !value.isBlank()) {
            keys.add(value.trim());
        }
    }

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

    private boolean canMerge(ResolvedVehicleUsageInterval left, ResolvedVehicleUsageInterval right) {
        if (left == null || right == null || left.to() == null || right.from() == null) {
            return false;
        }
        return Objects.equals(left.driverKey(), right.driverKey())
                && Objects.equals(left.registrationKey(), right.registrationKey())
                && Objects.equals(left.vehicleKey(), right.vehicleKey())
                && !right.from().isAfter(left.to().plusSeconds(1));
    }

    private ResolvedVehicleUsageInterval merge(ResolvedVehicleUsageInterval left, ResolvedVehicleUsageInterval right) {
        LinkedHashSet<String> sourceIntervalIds = new LinkedHashSet<>();
        if (left.sourceIntervalIds() != null) {
            sourceIntervalIds.addAll(left.sourceIntervalIds());
        }
        if (right.sourceIntervalIds() != null) {
            sourceIntervalIds.addAll(right.sourceIntervalIds());
        }
        OffsetDateTime end = left.to();
        if (right.to() != null && (end == null || right.to().isAfter(end))) {
            end = right.to();
        }
        return ResolvedVehicleUsageInterval.resolved(
                left.sessionId(),
                left.driverKey(),
                left.intervalId(),
                left.from(),
                end,
                left.odometerBeginKm(),
                right.odometerEndKm() == null ? left.odometerEndKm() : right.odometerEndKm(),
                left.registrationKey(),
                left.vehicleKey(),
                left.sourceKind(),
                List.copyOf(sourceIntervalIds)
        );
    }

    private List<EventHubEventDto> deduplicateAndSort(
            List<EventHubEventDto> directDriverEvents,
            List<EventHubEventDto> vehicleEvidenceEvents
    ) {
        LinkedHashMap<String, EventHubEventDto> byKey = new LinkedHashMap<>();
        appendDeduplicated(byKey, directDriverEvents);
        appendDeduplicated(byKey, vehicleEvidenceEvents);
        return sort(new ArrayList<>(byKey.values()));
    }

    private void appendDeduplicated(LinkedHashMap<String, EventHubEventDto> byKey, List<EventHubEventDto> events) {
        for (EventHubEventDto event : events == null ? List.<EventHubEventDto>of() : events) {
            byKey.putIfAbsent(dedupKey(event), event);
        }
    }

    private String dedupKey(EventHubEventDto event) {
        String sourceKey = event.packageInfo() != null && event.packageInfo().eventSource() != null
                ? event.packageInfo().eventSource().stableKey()
                : "NO_SOURCE";
        return sourceKey + "|" + event.externalSourceEventId();
    }

    private List<EventHubEventDto> sort(List<EventHubEventDto> events) {
        return (events == null ? List.<EventHubEventDto>of() : events).stream()
                .sorted(Comparator.comparing(EventHubEventDto::occurredAt, Comparator.nullsLast(Comparator.naturalOrder()))
                        .thenComparing(event -> event.eventDomain() == null ? "" : event.eventDomain().name())
                        .thenComparing(event -> event.eventType() == null ? "" : event.eventType().name())
                        .thenComparing(event -> event.lifecycle() == null ? "" : event.lifecycle().name())
                        .thenComparing(EventHubEventDto::externalSourceEventId, Comparator.nullsLast(String::compareTo)))
                .toList();
    }

    private JsonNode rawPayload(EventHubEventDto event) {
        JsonNode payload = event.payload();
        if (payload == null || payload.isNull()) {
            return null;
        }
        JsonNode raw = payload.get("raw");
        return raw == null || raw.isNull() ? payload : raw;
    }

    private String text(JsonNode node, String field) {
        if (node == null || field == null) {
            return null;
        }
        JsonNode value = node.get(field);
        if (value == null || value.isNull()) {
            return null;
        }
        String text = value.asText(null);
        return text == null || text.isBlank() ? null : text.trim();
    }
}
