package at.procon.eventhub.tachographfilesession.service;

import com.fasterxml.jackson.databind.JsonNode;
import at.procon.eventhub.dto.EventHubEventDto;
import at.procon.eventhub.processing.service.UnifiedEventTimelineReconstructor;
import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline;
import at.procon.eventhub.tachographfilesession.model.ResolvedVehicleUsageInterval;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class TachographEventTimelineReconstructionHelper {

    private final UnifiedEventTimelineReconstructor timelineReconstructor;

    public TachographEventTimelineReconstructionHelper() {
        this(new UnifiedEventTimelineReconstructor());
    }

    @Autowired
    public TachographEventTimelineReconstructionHelper(
            UnifiedEventTimelineReconstructor timelineReconstructor
    ) {
        this.timelineReconstructor = timelineReconstructor == null
                ? new UnifiedEventTimelineReconstructor()
                : timelineReconstructor;
    }

    public Map<String, List<EventHubEventDto>> groupEventsByDriverKey(List<EventHubEventDto> events) {
        Map<String, List<EventHubEventDto>> grouped = new LinkedHashMap<>();
        for (EventHubEventDto event : safe(events)) {
            String driverKey = eventDriverKey(event);
            if (driverKey == null || driverKey.isBlank()) {
                continue;
            }
            grouped.computeIfAbsent(driverKey, ignored -> new ArrayList<>()).add(event);
        }
        return grouped;
    }

    public ResolvedDriverTimeline reconstructMergedTimelineFromEvents(
            UUID fallbackSessionId,
            String fallbackDriverKey,
            List<EventHubEventDto> events
    ) {
        ResolvedDriverTimeline reconstructed = timelineReconstructor.reconstruct(
                fallbackSessionId == null ? new UUID(0L, 0L) : fallbackSessionId,
                fallbackDriverKey,
                safe(events)
        );
        return new ResolvedDriverTimeline(
                reconstructed.sourceKind(),
                reconstructed.loadedFrom(),
                reconstructed.loadedTo(),
                mergeVehicleUsageIntervals(reconstructed.vehicleUsageIntervals(), reconstructed.sourceKind()),
                reconstructed.activityIntervals(),
                reconstructed.supportEvents(),
                reconstructed.warnings()
        );
    }

    private String eventDriverKey(EventHubEventDto event) {
        if (event == null) {
            return null;
        }
        JsonNode payload = event.payload();
        JsonNode raw = payload == null || payload.isNull() ? null : (payload.get("raw") == null ? payload : payload.get("raw"));
        return firstNonBlank(text(raw, "driverKey"), driverKey(event));
    }

    private String driverKey(EventHubEventDto event) {
        if (event.driverRef() == null) {
            return null;
        }
        if (event.driverRef().driverCard() != null && event.driverRef().driverCard().hasValue()) {
            return event.driverRef().driverCard().stableKey();
        }
        return event.driverRef().sourceEntityId();
    }

    private List<ResolvedVehicleUsageInterval> mergeVehicleUsageIntervals(
            List<ResolvedVehicleUsageInterval> intervals,
            String sourceKind
    ) {
        List<ResolvedVehicleUsageInterval> sorted = safe(intervals).stream()
                .sorted(Comparator.comparing(ResolvedVehicleUsageInterval::from)
                        .thenComparing(ResolvedVehicleUsageInterval::to, Comparator.nullsLast(Comparator.naturalOrder())))
                .toList();
        if (sorted.isEmpty()) {
            return List.of();
        }

        List<ResolvedVehicleUsageInterval> result = new ArrayList<>();
        ResolvedVehicleUsageInterval current = sorted.getFirst();
        List<String> currentSources = new ArrayList<>(current.sourceIntervalIds());
        for (int i = 1; i < sorted.size(); i++) {
            ResolvedVehicleUsageInterval next = sorted.get(i);
            if (canMergeVehicleUsage(current, next)) {
                currentSources.addAll(next.sourceIntervalIds());
                current = ResolvedVehicleUsageInterval.resolved(
                        current.sessionId(),
                        current.driverKey(),
                        current.intervalId() + "+" + next.intervalId(),
                        current.from(),
                        mergedTo(current.to(), next.to()),
                        current.odometerBeginKm(),
                        mergedOdometerEnd(current, next),
                        current.registrationKey(),
                        current.vehicleKey(),
                        sourceKind,
                        currentSources
                );
            } else {
                result.add(current);
                current = next;
                currentSources = new ArrayList<>(current.sourceIntervalIds());
            }
        }
        result.add(current);
        return result;
    }

    private boolean canMergeVehicleUsage(ResolvedVehicleUsageInterval left, ResolvedVehicleUsageInterval right) {
        return Objects.equals(left.registrationKey(), right.registrationKey())
                && Objects.equals(left.vehicleKey(), right.vehicleKey())
                && !right.from().isAfter(mergeBoundary(left.to()));
    }

    private Long mergedOdometerEnd(
            ResolvedVehicleUsageInterval current,
            ResolvedVehicleUsageInterval next
    ) {
        if (current == null) {
            return next == null ? null : next.odometerEndKm();
        }
        if (next == null) {
            return current.odometerEndKm();
        }
        if (current.to() == null || next.to() == null) {
            return next.odometerEndKm() != null ? next.odometerEndKm() : current.odometerEndKm();
        }
        if (next.to().isAfter(current.to()) || next.to().isEqual(current.to())) {
            return next.odometerEndKm() != null ? next.odometerEndKm() : current.odometerEndKm();
        }
        return current.odometerEndKm() != null ? current.odometerEndKm() : next.odometerEndKm();
    }

    private OffsetDateTime mergedTo(OffsetDateTime left, OffsetDateTime right) {
        if (left == null || right == null) {
            return null;
        }
        return left.isAfter(right) ? left : right;
    }

    private OffsetDateTime mergeBoundary(OffsetDateTime endInclusive) {
        return endInclusive == null ? OffsetDateTime.MAX : endInclusive.plusSeconds(1);
    }

    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;
    }

    private String firstNonBlank(String... values) {
        if (values == null) {
            return null;
        }
        for (String value : values) {
            if (value != null && !value.isBlank()) {
                return value.trim();
            }
        }
        return null;
    }

    private <T> List<T> safe(List<T> values) {
        return values == null ? List.of() : values;
    }
}
