package at.procon.eventhub.processing.service;

import at.procon.eventhub.dto.EventHubEventDto;
import at.procon.eventhub.dto.VehicleRefDto;
import at.procon.eventhub.processing.model.UnifiedDiscoveredVehicleRef;
import at.procon.eventhub.processing.model.UnifiedEventSourceFamily;
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBackend;
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBundle;
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Objects;
import org.springframework.stereotype.Service;

@Service
public class UnifiedRuntimeEventAssemblyService {

    private final List<RuntimeDriverEventLoader> driverEventLoaders;
    private final List<RuntimeVehicleEventLoader> vehicleEventLoaders;

    public UnifiedRuntimeEventAssemblyService(
            List<RuntimeDriverEventLoader> driverEventLoaders,
            List<RuntimeVehicleEventLoader> vehicleEventLoaders
    ) {
        this.driverEventLoaders = List.copyOf(driverEventLoaders);
        this.vehicleEventLoaders = List.copyOf(vehicleEventLoaders);
    }

    public UnifiedRuntimeEventBundle assembleDriverScopedEvents(UnifiedRuntimeProcessingRequest request) {
        List<EventHubEventDto> driverSeedEvents = loadDriverSeedEvents(request);
        List<UnifiedDiscoveredVehicleRef> discoveredVehicles = discoverVehicles(driverSeedEvents);
        List<EventHubEventDto> expandedVehicleEvents = request.expandVehicleEvents()
                ? loadExpandedVehicleEvents(request, discoveredVehicles)
                : List.of();
        List<EventHubEventDto> mergedEvents = deduplicateAndSort(driverSeedEvents, expandedVehicleEvents);

        List<String> notes = new ArrayList<>();
        notes.add(request.eventBackend() == UnifiedRuntimeEventBackend.EVENTHUB_DB
                ? "Driver seed events were loaded from the local EventHub event store."
                : "Driver seed events were loaded directly from the selected runtime sources.");
        if (request.expandVehicleEvents()) {
            notes.add("Vehicle expansion loaded additional events for vehicles discovered in the driver seed set.");
            notes.add("Vehicle expansion padding minutes: " + request.vehicleExpansionPaddingMinutes() + ".");
        } else {
            notes.add("Vehicle expansion was disabled for this runtime request.");
        }
        return new UnifiedRuntimeEventBundle(
                request,
                driverSeedEvents,
                discoveredVehicles,
                expandedVehicleEvents,
                mergedEvents,
                notes
        );
    }

    private List<EventHubEventDto> loadDriverSeedEvents(UnifiedRuntimeProcessingRequest request) {
        List<EventHubEventDto> result = new ArrayList<>();
        for (UnifiedEventSourceFamily sourceFamily : request.sourceFamilies()) {
            RuntimeDriverEventLoader loader = driverLoader(request, sourceFamily);
            if (loader instanceof EventHubRuntimeEventLoader eventHubLoader) {
                result.addAll(eventHubLoader.loadDriverEvents(request, sourceFamily));
            } else {
                result.addAll(loader.loadDriverEvents(request));
            }
        }
        return deduplicateAndSort(result, List.of());
    }

    private List<EventHubEventDto> loadExpandedVehicleEvents(
            UnifiedRuntimeProcessingRequest request,
            List<UnifiedDiscoveredVehicleRef> discoveredVehicles
    ) {
        List<EventHubEventDto> result = new ArrayList<>();
        for (UnifiedDiscoveredVehicleRef vehicleRef : discoveredVehicles) {
            for (UnifiedEventSourceFamily sourceFamily : request.sourceFamilies()) {
                RuntimeVehicleEventLoader loader = vehicleLoader(request, sourceFamily);
                if (loader instanceof EventHubRuntimeEventLoader eventHubLoader) {
                    result.addAll(eventHubLoader.loadVehicleEvents(request, sourceFamily, vehicleRef));
                } else {
                    result.addAll(loader.loadVehicleEvents(request, vehicleRef));
                }
            }
        }
        return deduplicateAndSort(result, List.of());
    }

    private List<UnifiedDiscoveredVehicleRef> discoverVehicles(List<EventHubEventDto> events) {
        List<UnifiedDiscoveredVehicleRef> result = new ArrayList<>();
        for (EventHubEventDto event : events) {
            VehicleRefDto vehicleRef = event.vehicleRef();
            if (vehicleRef == null || !vehicleRef.hasAnyReference()) {
                continue;
            }
            UnifiedDiscoveredVehicleRef candidate = new UnifiedDiscoveredVehicleRef(
                    vehicleRef.sourceVehicleEntityId(),
                    vehicleRef.vin(),
                    vehicleRef.vehicleRegistration() == null
                            ? null
                            : vehicleRef.vehicleRegistration().nationNumericCode() == null
                            ? vehicleRef.vehicleRegistration().nation()
                            : vehicleRef.vehicleRegistration().nationNumericCode().toString(),
                    vehicleRef.vehicleRegistration() == null ? null : vehicleRef.vehicleRegistration().number()
            );
            if (!candidate.hasAnyReference()) {
                continue;
            }
            boolean merged = false;
            for (int i = 0; i < result.size(); i++) {
                UnifiedDiscoveredVehicleRef existing = result.get(i);
                if (existing.matches(candidate)) {
                    result.set(i, existing.merge(candidate));
                    merged = true;
                    break;
                }
            }
            if (!merged) {
                result.add(candidate);
            }
        }
        result.sort(Comparator.comparing(UnifiedDiscoveredVehicleRef::stableKey));
        return List.copyOf(result);
    }

    private List<EventHubEventDto> deduplicateAndSort(
            List<EventHubEventDto> left,
            List<EventHubEventDto> right
    ) {
        LinkedHashMap<String, EventHubEventDto> byKey = new LinkedHashMap<>();
        appendDeduplicated(byKey, left);
        appendDeduplicated(byKey, right);
        return byKey.values().stream()
                .sorted(Comparator.comparing(EventHubEventDto::occurredAt, Comparator.nullsLast(Comparator.naturalOrder()))
                        .thenComparing(event -> event.eventDomain().name())
                        .thenComparing(event -> event.eventType().name())
                        .thenComparing(event -> event.lifecycle().name())
                        .thenComparing(EventHubEventDto::externalSourceEventId))
                .toList();
    }

    private void appendDeduplicated(LinkedHashMap<String, EventHubEventDto> byKey, List<EventHubEventDto> events) {
        for (EventHubEventDto event : 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 RuntimeDriverEventLoader driverLoader(UnifiedRuntimeProcessingRequest request, UnifiedEventSourceFamily sourceFamily) {
        return driverEventLoaders.stream()
                .filter(loader -> loader.supports(request, sourceFamily))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("No runtime driver event loader is registered for source family " + sourceFamily + "."));
    }

    private RuntimeVehicleEventLoader vehicleLoader(UnifiedRuntimeProcessingRequest request, UnifiedEventSourceFamily sourceFamily) {
        return vehicleEventLoaders.stream()
                .filter(loader -> loader.supports(request, sourceFamily))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("No runtime vehicle event loader is registered for source family " + sourceFamily + "."));
    }
}
