package at.procon.eventhub.processing.service;

import at.procon.eventhub.dto.EventDomain;
import at.procon.eventhub.dto.EventHubEventDto;
import at.procon.eventhub.dto.EventLifecycle;
import at.procon.eventhub.dto.EventType;
import at.procon.eventhub.tachographfilesession.model.ExtractedSupportEvent;
import at.procon.eventhub.tachographfilesession.model.ExtractionWarning;
import at.procon.eventhub.tachographfilesession.model.ResolvedActivityInterval;
import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline;
import at.procon.eventhub.tachographfilesession.model.ResolvedVehicleUsageInterval;
import com.fasterxml.jackson.databind.JsonNode;
import java.math.BigDecimal;
import java.time.Duration;
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.Map;
import java.util.UUID;
import org.springframework.stereotype.Service;

@Service
public class UnifiedEventTimelineReconstructor {

    public ResolvedDriverTimeline reconstruct(
            UUID sessionId,
            String driverKey,
            List<EventHubEventDto> events
    ) {
        return reconstruct(sessionId, driverKey, events, List.of(), null);
    }

    public ResolvedDriverTimeline reconstruct(
            UUID sessionId,
            String driverKey,
            List<EventHubEventDto> events,
            List<ExtractionWarning> warnings,
            String sourceKind
    ) {
        List<EventHubEventDto> safeEvents = events == null ? List.of() : List.copyOf(events);
        List<ResolvedActivityInterval> activityIntervals = reconstructActivityIntervals(safeEvents);
        List<ResolvedVehicleUsageInterval> vehicleUsageIntervals =
                reconstructVehicleUsageIntervals(sessionId, driverKey, safeEvents);
        List<ExtractedSupportEvent> supportEvents = reconstructSupportEvents(safeEvents);
        List<ExtractionWarning> mergedWarnings = warnings == null ? List.of() : List.copyOf(new LinkedHashSet<>(warnings));

        OffsetDateTime loadedFrom = minTimestamp(activityIntervals, vehicleUsageIntervals, supportEvents);
        OffsetDateTime loadedTo = maxTimestamp(activityIntervals, vehicleUsageIntervals, supportEvents);
        return new ResolvedDriverTimeline(
                resolveSourceKind(safeEvents, sourceKind),
                loadedFrom,
                loadedTo,
                vehicleUsageIntervals,
                activityIntervals,
                supportEvents,
                mergedWarnings
        );
    }

    private List<ResolvedActivityInterval> reconstructActivityIntervals(List<EventHubEventDto> events) {
        Map<String, ActivityAccumulator> byIntervalId = new LinkedHashMap<>();
        for (EventHubEventDto event : events) {
            if (event.eventDomain() != EventDomain.DRIVER_ACTIVITY) {
                continue;
            }
            JsonNode raw = raw(event);
            String intervalId = firstNonBlank(
                    text(raw, "intervalId"),
                    text(raw, "sourceRowId"),
                    event.externalSourceEventId()
            );
            ActivityAccumulator accumulator = byIntervalId.computeIfAbsent(
                    intervalId,
                    ignored -> new ActivityAccumulator(intervalId)
            );
            accumulator.accept(event, raw);
        }
        return byIntervalId.values().stream()
                .map(ActivityAccumulator::finish)
                .filter(interval -> interval != null)
                .sorted(Comparator.comparing(ResolvedActivityInterval::from)
                        .thenComparing(ResolvedActivityInterval::to)
                        .thenComparing(ResolvedActivityInterval::activityType, Comparator.nullsLast(String::compareTo)))
                .toList();
    }

    private List<ResolvedVehicleUsageInterval> reconstructVehicleUsageIntervals(
            UUID sessionId,
            String driverKey,
            List<EventHubEventDto> events
    ) {
        Map<String, VehicleUsageAccumulator> byIntervalId = new LinkedHashMap<>();
        for (EventHubEventDto event : events) {
            if (event.eventDomain() != EventDomain.DRIVER_CARD) {
                continue;
            }
            if (event.eventType() != EventType.CARD_INSERTED && event.eventType() != EventType.CARD_WITHDRAWN) {
                continue;
            }
            JsonNode raw = raw(event);
            String intervalId = firstNonBlank(
                    text(raw, "intervalId"),
                    text(raw, "sourceRowId"),
                    event.externalSourceEventId()
            );
            VehicleUsageAccumulator accumulator = byIntervalId.computeIfAbsent(
                    intervalId,
                    ignored -> new VehicleUsageAccumulator(sessionId, driverKey, intervalId)
            );
            accumulator.accept(event, raw);
        }
        return byIntervalId.values().stream()
                .map(VehicleUsageAccumulator::finish)
                .filter(interval -> interval != null)
                .sorted(Comparator.comparing(ResolvedVehicleUsageInterval::from)
                        .thenComparing(ResolvedVehicleUsageInterval::to, Comparator.nullsLast(Comparator.naturalOrder())))
                .toList();
    }

    private List<ExtractedSupportEvent> reconstructSupportEvents(List<EventHubEventDto> events) {
        List<ExtractedSupportEvent> result = new ArrayList<>();
        for (EventHubEventDto event : events) {
            if (event.eventDomain() == EventDomain.DRIVER_ACTIVITY) {
                continue;
            }
            if (event.eventDomain() == EventDomain.DRIVER_CARD
                    && (event.eventType() == EventType.CARD_INSERTED || event.eventType() == EventType.CARD_WITHDRAWN)) {
                continue;
            }
            JsonNode raw = raw(event);
            String eventId = firstNonBlank(text(raw, "supportEventId"), event.externalSourceEventId());
            BigDecimal latitude = event.position() == null ? null : event.position().latitude();
            BigDecimal longitude = event.position() == null ? null : event.position().longitude();
            result.add(new ExtractedSupportEvent(
                    eventId,
                    text(raw, "driverKey"),
                    event.occurredAt(),
                    event.eventDomain().name(),
                    text(raw, "supportEventType") == null ? event.eventType().name() : text(raw, "supportEventType"),
                    event.lifecycle().name(),
                    text(raw, "slot"),
                    text(raw, "registrationKey"),
                    text(raw, "vehicleKey"),
                    firstNonBlank(text(raw, "country"), detailText(event, "country")),
                    text(raw, "region"),
                    firstNonBlank(text(raw, "countryFrom"), detailText(event, "countryFrom")),
                    firstNonBlank(text(raw, "countryTo"), detailText(event, "countryTo")),
                    text(raw, "operation"),
                    latitude,
                    longitude,
                    text(raw, "authenticationStatus"),
                    longValue(raw, "odometerKm"),
                    text(raw, "code"),
                    decimal(raw, "avgSpeedKmh"),
                    decimal(raw, "maxSpeedKmh"),
                    text(raw, "rawRecordPath")
            ));
        }
        result.sort(Comparator.comparing(ExtractedSupportEvent::occurredAt)
                .thenComparing(ExtractedSupportEvent::eventDomain, Comparator.nullsLast(String::compareTo))
                .thenComparing(ExtractedSupportEvent::eventId, Comparator.nullsLast(String::compareTo)));
        return List.copyOf(result);
    }

    private String resolveSourceKind(List<EventHubEventDto> events, String preferredSourceKind) {
        if (preferredSourceKind != null && !preferredSourceKind.isBlank()) {
            return preferredSourceKind;
        }
        LinkedHashSet<String> sourceKinds = new LinkedHashSet<>();
        for (EventHubEventDto event : events) {
            JsonNode raw = raw(event);
            String sourceKind = text(raw, "sourceKind");
            if (sourceKind == null && event.packageInfo() != null && event.packageInfo().eventSource() != null) {
                sourceKind = event.packageInfo().eventSource().sourceKind();
            }
            if (sourceKind != null && !sourceKind.isBlank()) {
                sourceKinds.add(sourceKind);
            }
        }
        if (sourceKinds.isEmpty()) {
            return "UNIFIED_EVENT_STREAM";
        }
        return sourceKinds.size() == 1 ? sourceKinds.iterator().next() : "UNIFIED_EVENT_STREAM";
    }

    private OffsetDateTime minTimestamp(
            List<ResolvedActivityInterval> activityIntervals,
            List<ResolvedVehicleUsageInterval> vehicleUsageIntervals,
            List<ExtractedSupportEvent> supportEvents
    ) {
        OffsetDateTime min = null;
        for (ResolvedActivityInterval interval : activityIntervals) {
            min = min(min, interval.from());
        }
        for (ResolvedVehicleUsageInterval interval : vehicleUsageIntervals) {
            min = min(min, interval.from());
        }
        for (ExtractedSupportEvent supportEvent : supportEvents) {
            min = min(min, supportEvent.occurredAt());
        }
        return min;
    }

    private OffsetDateTime maxTimestamp(
            List<ResolvedActivityInterval> activityIntervals,
            List<ResolvedVehicleUsageInterval> vehicleUsageIntervals,
            List<ExtractedSupportEvent> supportEvents
    ) {
        OffsetDateTime max = null;
        for (ResolvedActivityInterval interval : activityIntervals) {
            max = max(max, interval.to());
        }
        for (ResolvedVehicleUsageInterval interval : vehicleUsageIntervals) {
            max = max(max, interval.to());
        }
        for (ExtractedSupportEvent supportEvent : supportEvents) {
            max = max(max, supportEvent.occurredAt());
        }
        return max;
    }

    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 JsonNode raw(EventHubEventDto event) {
        JsonNode payload = event.payload();
        if (payload == null || payload.isMissingNode()) {
            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);
        return value == null || value.isNull() ? null : value.asText(null);
    }

    private boolean booleanValue(JsonNode node, String field) {
        if (node == null || field == null) {
            return false;
        }
        JsonNode value = node.get(field);
        return value != null && !value.isNull() && value.asBoolean(false);
    }

    private Long longValue(JsonNode node, String field) {
        if (node == null || field == null) {
            return null;
        }
        JsonNode value = node.get(field);
        if (value == null || value.isNull()) {
            return null;
        }
        return value.isNumber() ? value.asLong() : Long.parseLong(value.asText());
    }

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

    private List<String> stringList(JsonNode node, String field) {
        if (node == null || field == null) {
            return List.of();
        }
        JsonNode value = node.get(field);
        if (value == null || value.isNull() || !value.isArray()) {
            return List.of();
        }
        List<String> result = new ArrayList<>();
        value.forEach(item -> {
            String text = item == null || item.isNull() ? null : item.asText(null);
            if (text != null && !text.isBlank()) {
                result.add(text);
            }
        });
        return List.copyOf(result);
    }

    private String detailText(EventHubEventDto event, String field) {
        if (event.eventDetails() == null || event.eventDetails().attributes() == null) {
            return null;
        }
        JsonNode value = event.eventDetails().attributes().get(field);
        return value == null || value.isNull() ? null : value.asText(null);
    }

    private String activityType(EventHubEventDto event) {
        return switch (event.eventType()) {
            case DRIVE -> "DRIVE";
            case WORK -> "WORK";
            case AVAILABILITY -> "AVAILABILITY";
            case BREAK_REST -> "BREAK_REST";
            default -> "UNKNOWN";
        };
    }

    private Long toKilometers(Long meters) {
        return meters == null ? null : meters / 1_000L;
    }

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

    private final class ActivityAccumulator {
        private final String intervalId;
        private OffsetDateTime startedAt;
        private OffsetDateTime endedAt;
        private EventHubEventDto sample;
        private JsonNode raw;

        private ActivityAccumulator(String intervalId) {
            this.intervalId = intervalId;
        }

        private void accept(EventHubEventDto event, JsonNode raw) {
            if (sample == null) {
                sample = event;
                this.raw = raw;
            }
            if (event.lifecycle() == EventLifecycle.START) {
                startedAt = event.occurredAt();
            } else if (event.lifecycle() == EventLifecycle.END) {
                endedAt = event.occurredAt();
            }
        }

        private ResolvedActivityInterval finish() {
            if (sample == null || startedAt == null || endedAt == null || !endedAt.isAfter(startedAt)) {
                return null;
            }
            return new ResolvedActivityInterval(
                    intervalId,
                    startedAt,
                    endedAt,
                    Duration.between(startedAt, endedAt).getSeconds(),
                    activityType(sample),
                    detailText(sample, "cardSlot"),
                    detailText(sample, "cardStatus"),
                    detailText(sample, "drivingStatus"),
                    text(raw, "registrationKey"),
                    text(raw, "vehicleKey"),
                    text(raw, "sourceKind"),
                    stringList(raw, "sourceRowIds"),
                    booleanValue(raw, "synthetic"),
                    booleanValue(raw, "clippedToRequestedPeriod"),
                    text(raw, "level") == null ? "RAW_INTERVAL" : text(raw, "level")
            );
        }
    }

    private final class VehicleUsageAccumulator {
        private final UUID sessionId;
        private final String driverKey;
        private final String intervalId;
        private OffsetDateTime startedAt;
        private OffsetDateTime endedAt;
        private Long odometerBeginKm;
        private Long odometerEndKm;
        private JsonNode raw;

        private VehicleUsageAccumulator(UUID sessionId, String driverKey, String intervalId) {
            this.sessionId = sessionId;
            this.driverKey = driverKey;
            this.intervalId = intervalId;
        }

        private void accept(EventHubEventDto event, JsonNode raw) {
            if (this.raw == null) {
                this.raw = raw;
            }
            if (event.eventType() == EventType.CARD_INSERTED) {
                startedAt = event.occurredAt();
                odometerBeginKm = toKilometers(event.odometerM());
            } else if (event.eventType() == EventType.CARD_WITHDRAWN) {
                endedAt = event.occurredAt();
                odometerEndKm = toKilometers(event.odometerM());
            }
        }

        private ResolvedVehicleUsageInterval finish() {
            if (startedAt == null) {
                return null;
            }
            return ResolvedVehicleUsageInterval.resolved(
                    sessionId,
                    driverKey,
                    intervalId,
                    startedAt,
                    endedAt,
                    odometerBeginKm,
                    odometerEndKm,
                    text(raw, "registrationKey"),
                    text(raw, "vehicleKey"),
                    text(raw, "sourceKind"),
                    stringList(raw, "sourceRowIds")
            );
        }
    }
}
