package at.procon.eventhub.tachographfilesession.service;

import at.procon.eventhub.dto.EventHubEventDto;
import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession;
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 at.procon.eventhub.tachographfilesession.model.TachographFileSession;
import at.procon.eventhub.tachographfilesession.model.TachographTimelineEventBundle;
import com.fasterxml.jackson.databind.JsonNode;
import java.math.BigDecimal;
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.Objects;
import java.util.UUID;
import org.springframework.stereotype.Component;

@Component
public class EventBackedDriverTimelineBuilder {

    private final DriverTimelineEventBuilder eventBuilder;

    public EventBackedDriverTimelineBuilder(DriverTimelineEventBuilder eventBuilder) {
        this.eventBuilder = eventBuilder;
    }

    public ResolvedDriverTimeline build(
            TachographFileSession session,
            DriverExtractionSession driverSession
    ) {
        TachographTimelineEventBundle bundle = eventBuilder.buildEventBundle(session, driverSession);
        List<ResolvedActivityInterval> activityIntervals =
                reconstructActivityIntervals(bundle.activityEvents());
        List<ResolvedVehicleUsageInterval> vehicleUsageIntervals =
                reconstructVehicleUsageIntervals(session.sessionId(), driverSession.driverKey(), bundle.vehicleUsageEvents());
        List<ExtractedSupportEvent> supportEvents =
                reconstructSupportEvents(bundle.supportEvents());
        List<ExtractionWarning> warnings = mergeWarnings(session.warnings(), driverSession.warnings());

        OffsetDateTime loadedFrom = minTimestamp(activityIntervals, vehicleUsageIntervals, supportEvents);
        OffsetDateTime loadedTo = maxTimestamp(activityIntervals, vehicleUsageIntervals, supportEvents);
        return new ResolvedDriverTimeline(
                resolveSourceKind(session, bundle),
                loadedFrom,
                loadedTo,
                vehicleUsageIntervals,
                activityIntervals,
                supportEvents,
                warnings
        );
    }

    private List<ResolvedActivityInterval> reconstructActivityIntervals(List<EventHubEventDto> activityEvents) {
        if (activityEvents == null || activityEvents.isEmpty()) {
            return List.of();
        }
        Map<String, ActivityAccumulator> byIntervalId = new LinkedHashMap<>();
        for (EventHubEventDto event : activityEvents) {
            if (!"DRIVER_ACTIVITY".equals(event.eventDomain().name())) {
                continue;
            }
            JsonNode raw = raw(event);
            String intervalId = text(raw, "intervalId");
            if (intervalId == null) {
                intervalId = text(raw, "sourceRowId");
            }
            if (intervalId == null) {
                intervalId = event.externalSourceEventId();
            }
            String resolvedIntervalId = intervalId;
            ActivityAccumulator accumulator = byIntervalId.computeIfAbsent(
                    resolvedIntervalId,
                    ignored -> new ActivityAccumulator(resolvedIntervalId)
            );
            accumulator.accept(event, raw);
        }
        List<ResolvedActivityInterval> result = new ArrayList<>(byIntervalId.size());
        for (ActivityAccumulator accumulator : byIntervalId.values()) {
            ResolvedActivityInterval interval = accumulator.finish();
            if (interval != null) {
                result.add(interval);
            }
        }
        result.sort(Comparator.comparing(ResolvedActivityInterval::from)
                .thenComparing(ResolvedActivityInterval::to)
                .thenComparing(ResolvedActivityInterval::activityType, Comparator.nullsLast(String::compareTo)));
        return List.copyOf(result);
    }

    private List<ResolvedVehicleUsageInterval> reconstructVehicleUsageIntervals(
            UUID sessionId,
            String driverKey,
            List<EventHubEventDto> vehicleUsageEvents
    ) {
        if (vehicleUsageEvents == null || vehicleUsageEvents.isEmpty()) {
            return List.of();
        }
        Map<String, VehicleUsageAccumulator> byIntervalId = new LinkedHashMap<>();
        for (EventHubEventDto event : vehicleUsageEvents) {
            if (!"DRIVER_CARD".equals(event.eventDomain().name())) {
                continue;
            }
            JsonNode raw = raw(event);
            String intervalId = text(raw, "intervalId");
            if (intervalId == null) {
                intervalId = text(raw, "sourceRowId");
            }
            if (intervalId == null) {
                intervalId = event.externalSourceEventId();
            }
            String resolvedIntervalId = intervalId;
            VehicleUsageAccumulator accumulator = byIntervalId.computeIfAbsent(
                    resolvedIntervalId,
                    ignored -> new VehicleUsageAccumulator(sessionId, driverKey, resolvedIntervalId)
            );
            accumulator.accept(event, raw);
        }
        List<ResolvedVehicleUsageInterval> result = new ArrayList<>(byIntervalId.size());
        for (VehicleUsageAccumulator accumulator : byIntervalId.values()) {
            ResolvedVehicleUsageInterval interval = accumulator.finish();
            if (interval != null) {
                result.add(interval);
            }
        }
        result.sort(Comparator.comparing(ResolvedVehicleUsageInterval::from)
                .thenComparing(ResolvedVehicleUsageInterval::to, Comparator.nullsLast(Comparator.naturalOrder())));
        return List.copyOf(result);
    }

    private List<ExtractedSupportEvent> reconstructSupportEvents(List<EventHubEventDto> supportEvents) {
        if (supportEvents == null || supportEvents.isEmpty()) {
            return List.of();
        }
        List<ExtractedSupportEvent> result = new ArrayList<>(supportEvents.size());
        for (EventHubEventDto event : supportEvents) {
            JsonNode raw = raw(event);
            String eventId = text(raw, "supportEventId");
            if (eventId == null) {
                eventId = 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,
                    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"),
                    text(raw, "country"),
                    text(raw, "region"),
                    text(raw, "countryFrom"),
                    text(raw, "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 List<ExtractionWarning> mergeWarnings(List<ExtractionWarning> sessionWarnings, List<ExtractionWarning> driverWarnings) {
        LinkedHashSet<ExtractionWarning> merged = new LinkedHashSet<>();
        if (sessionWarnings != null) {
            merged.addAll(sessionWarnings);
        }
        if (driverWarnings != null) {
            merged.addAll(driverWarnings);
        }
        return List.copyOf(merged);
    }

    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 String resolveSourceKind(TachographFileSession session, TachographTimelineEventBundle bundle) {
        for (EventHubEventDto event : bundle.allEvents()) {
            JsonNode raw = raw(event);
            String sourceKind = text(raw, "sourceKind");
            if (sourceKind != null) {
                return sourceKind;
            }
            if (event.packageInfo() != null && event.packageInfo().eventSource() != null
                    && event.packageInfo().eventSource().sourceKind() != null) {
                return event.packageInfo().eventSource().sourceKind();
            }
        }
        return session.metadata().driverCardFile() ? "DRIVER_CARD" : "VEHICLE_UNIT";
    }

    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 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() == at.procon.eventhub.dto.EventLifecycle.START) {
                startedAt = event.occurredAt();
            } else if (event.lifecycle() == at.procon.eventhub.dto.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,
                    java.time.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() == at.procon.eventhub.dto.EventType.CARD_INSERTED) {
                startedAt = event.occurredAt();
                odometerBeginKm = toKilometers(event.odometerM());
            } else if (event.eventType() == at.procon.eventhub.dto.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")
            );
        }
    }
}
