package at.procon.eventhub.processing.service;

import at.procon.eventhub.dto.EventHubEventDto;
import at.procon.eventhub.dto.VehicleRefDto;
import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeVehicleUsageInterval;
import at.procon.eventhub.processing.dto.RuntimeVehicleEvidenceAttachmentDecisionDto;
import at.procon.eventhub.processing.dto.RuntimeVehicleUsageIntervalDebugDto;
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.processing.support.TachographRuntimeIdentityResolver;
import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline;
import at.procon.eventhub.tachographfilesession.model.ResolvedVehicleUsageInterval;
import at.procon.eventhub.tachographfilesession.service.TachographDriverWorkingTimeAdapter;
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
    ) {
        return attachVehicleEvidence(
                driverKey,
                directDriverEvents,
                runtimeScopeEvents,
                attachVehicleOnlyEvents,
                vehicleEvidencePaddingMinutes,
                false
        );
    }

    public RuntimeDriverVehicleEvidenceAttachmentResult attachVehicleEvidence(
            String driverKey,
            List<EventHubEventDto> directDriverEvents,
            List<EventHubEventDto> runtimeScopeEvents,
            boolean attachVehicleOnlyEvents,
            int vehicleEvidencePaddingMinutes,
            boolean includeDebug
    ) {
        ResolvedDriverTimeline timeline = timelineReconstructor.reconstruct(
                null,
                driverKey,
                directDriverEvents == null ? List.of() : directDriverEvents
        );
        List<DriverWorkingTimeVehicleUsageInterval> vehicleUsageIntervals = mergeVehicleUsageIntervals(timeline.vehicleUsageIntervals())
                .stream()
                .map(TachographDriverWorkingTimeAdapter::toVehicleUsageInterval)
                .filter(Objects::nonNull)
                .toList();
        return attachVehicleEvidence(
                driverKey,
                directDriverEvents,
                runtimeScopeEvents,
                vehicleUsageIntervals,
                attachVehicleOnlyEvents,
                vehicleEvidencePaddingMinutes,
                includeDebug
        );
    }

    public RuntimeDriverVehicleEvidenceAttachmentResult attachVehicleEvidence(
            String driverKey,
            List<EventHubEventDto> directDriverEvents,
            List<EventHubEventDto> runtimeScopeEvents,
            List<DriverWorkingTimeVehicleUsageInterval> vehicleUsageIntervals,
            boolean attachVehicleOnlyEvents,
            int vehicleEvidencePaddingMinutes,
            boolean includeDebug
    ) {
        List<EventHubEventDto> safeDriverEvents = directDriverEvents == null ? List.of() : List.copyOf(directDriverEvents);
        List<EventHubEventDto> safeScopeEvents = runtimeScopeEvents == null ? List.of() : List.copyOf(runtimeScopeEvents);
        List<DriverWorkingTimeVehicleUsageInterval> safeVehicleUsageIntervals =
                mergeDriverWorkingTimeVehicleUsageIntervals(vehicleUsageIntervals);
        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 + ".");
            List<RuntimeVehicleEvidenceAttachmentDecisionDto> disabledDecisions = includeDebug
                    ? disabledDecisions(safeScopeEvents)
                    : List.of();
            return new RuntimeDriverVehicleEvidenceAttachmentResult(
                    driverKey,
                    safeDriverEvents,
                    List.of(),
                    deduplicateAndSort(safeDriverEvents, List.of()),
                    0,
                    disabledDecisions.size(),
                    disabledDecisions.size(),
                    List.of(),
                    disabledDecisions,
                    notes,
                    warnings
            );
        }

        List<RuntimeVehicleUsageIntervalDebugDto> usageIntervalDebug = includeDebug
                ? safeVehicleUsageIntervals.stream().map(RuntimeVehicleUsageIntervalDebugDto::from).filter(Objects::nonNull).toList()
                : List.of();
        List<RuntimeVehicleEvidenceAttachmentDecisionDto> decisions = includeDebug
                ? new ArrayList<>(directDriverDecisions(safeDriverEvents))
                : new ArrayList<>();
        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<DriverWorkingTimeVehicleUsageInterval> matchingIntervals =
                    matchingUsageIntervals(vehicleEvent, safeVehicleUsageIntervals, paddingMinutes);
            if (matchingIntervals.isEmpty()) {
                ignored++;
                if (includeDebug) {
                    decisions.add(decision(
                            "IGNORED_NO_OVERLAPPING_VEHICLE_USAGE",
                            "Vehicle-scoped event did not overlap any reconstructed vehicle-usage interval for driver " + driverKey + ".",
                            vehicleEvent,
                            List.of()
                    ));
                }
                continue;
            }
            attached.add(vehicleEvent);
            if (includeDebug) {
                decisions.add(decision(
                        "ATTACHED_VEHICLE_EVIDENCE",
                        "Vehicle-scoped event overlapped driver vehicle usage interval(s).",
                        vehicleEvent,
                        intervalIds(matchingIntervals)
                ));
            }
            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 " + safeVehicleUsageIntervals.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 (safeVehicleUsageIntervals.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),
                safeVehicleUsageIntervals.size(),
                candidateVehicleEvidence.size(),
                ignored,
                usageIntervalDebug,
                includeDebug ? decisions : List.of(),
                notes,
                warnings
        );
    }

    private List<RuntimeVehicleEvidenceAttachmentDecisionDto> disabledDecisions(List<EventHubEventDto> runtimeScopeEvents) {
        return (runtimeScopeEvents == null ? List.<EventHubEventDto>of() : runtimeScopeEvents).stream()
                .filter(event -> scopeClassifier.classify(event) == RuntimeEventScopeType.VEHICLE_SCOPED)
                .map(event -> decision(
                        "IGNORED_ATTACHMENT_DISABLED",
                        "Vehicle evidence attachment was disabled for this partition.",
                        event,
                        List.of()
                ))
                .toList();
    }

    private List<RuntimeVehicleEvidenceAttachmentDecisionDto> directDriverDecisions(List<EventHubEventDto> directDriverEvents) {
        return (directDriverEvents == null ? List.<EventHubEventDto>of() : directDriverEvents).stream()
                .map(event -> decision(
                        "DIRECT_DRIVER_EVENT",
                        "Event already carries the selected driver reference and belongs directly to the driver partition.",
                        event,
                        List.of()
                ))
                .toList();
    }

    private RuntimeVehicleEvidenceAttachmentDecisionDto decision(
            String decision,
            String reason,
            EventHubEventDto event,
            List<String> intervalIds
    ) {
        return new RuntimeVehicleEvidenceAttachmentDecisionDto(
                decision,
                reason,
                dedupKey(event),
                event == null ? null : event.externalSourceEventId(),
                event == null ? null : event.occurredAt(),
                event == null || event.eventDomain() == null ? null : event.eventDomain().name(),
                event == null || event.eventType() == null ? null : event.eventType().name(),
                event == null || event.lifecycle() == null ? null : event.lifecycle().name(),
                scopeClassifier.classify(event),
                vehicleKeys(event),
                intervalIds == null ? List.of() : List.copyOf(intervalIds)
        );
    }

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

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

    private boolean matchesVehicle(EventHubEventDto event, DriverWorkingTimeVehicleUsageInterval 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<>();
        String vehicleKey = TachographRuntimeIdentityResolver.vehicleKey(event);
        String registrationKey = TachographRuntimeIdentityResolver.registrationKey(event);
        add(result, vehicleKey);
        add(result, registrationKey);
        if (vehicleKey != null) {
            add(result, "VIN:" + vehicleKey);
        }
        if (registrationKey != null) {
            add(result, "VR:" + registrationKey);
        }
        return Set.copyOf(result);
    }

    private Set<String> vehicleKeys(DriverWorkingTimeVehicleUsageInterval 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 List<String> intervalIds(List<DriverWorkingTimeVehicleUsageInterval> intervals) {
        return (intervals == null ? List.<DriverWorkingTimeVehicleUsageInterval>of() : intervals).stream()
                .map(DriverWorkingTimeVehicleUsageInterval::intervalId)
                .filter(Objects::nonNull)
                .toList();
    }

    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<DriverWorkingTimeVehicleUsageInterval> mergeDriverWorkingTimeVehicleUsageIntervals(
            List<DriverWorkingTimeVehicleUsageInterval> intervals
    ) {
        if (intervals == null || intervals.isEmpty()) {
            return List.of();
        }
        List<DriverWorkingTimeVehicleUsageInterval> sorted = intervals.stream()
                .filter(Objects::nonNull)
                .sorted(Comparator.comparing(DriverWorkingTimeVehicleUsageInterval::startedAt, Comparator.nullsLast(Comparator.naturalOrder()))
                        .thenComparing(DriverWorkingTimeVehicleUsageInterval::endedAt, Comparator.nullsLast(Comparator.naturalOrder()))
                        .thenComparing(DriverWorkingTimeVehicleUsageInterval::intervalId, Comparator.nullsLast(String::compareTo)))
                .toList();
        List<DriverWorkingTimeVehicleUsageInterval> merged = new ArrayList<>();
        for (DriverWorkingTimeVehicleUsageInterval next : sorted) {
            if (merged.isEmpty()) {
                merged.add(next);
                continue;
            }
            DriverWorkingTimeVehicleUsageInterval 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(
            DriverWorkingTimeVehicleUsageInterval left,
            DriverWorkingTimeVehicleUsageInterval 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 DriverWorkingTimeVehicleUsageInterval merge(
            DriverWorkingTimeVehicleUsageInterval left,
            DriverWorkingTimeVehicleUsageInterval right
    ) {
        LinkedHashSet<String> sourceIntervalIds = new LinkedHashSet<>(left.sourceIntervalIds());
        sourceIntervalIds.addAll(right.sourceIntervalIds());
        OffsetDateTime end = left.endedAt();
        if (right.endedAt() != null && (end == null || right.endedAt().isAfter(end))) {
            end = right.endedAt();
        }
        return new DriverWorkingTimeVehicleUsageInterval(
                left.sessionId(),
                left.driverKey(),
                left.intervalId(),
                left.firstSourceIntervalId(),
                right.lastSourceIntervalId() == null ? left.lastSourceIntervalId() : right.lastSourceIntervalId(),
                left.startedAt(),
                end,
                left.startedAtEpochSecond(),
                end == null ? null : end.toEpochSecond(),
                end == null ? left.durationSeconds() : end.toEpochSecond() - left.startedAtEpochSecond(),
                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();
    }
}
