package at.procon.eventhub.processing.service;

import at.procon.eventhub.dto.DriverRefDto;
import at.procon.eventhub.dto.EventHubEventDto;
import at.procon.eventhub.dto.VehicleRefDto;
import at.procon.eventhub.processing.dto.UnifiedRuntimeDerivedProjectionResultDto;
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
import at.procon.eventhub.processing.dto.UnifiedRuntimeTachographEsperScopeResultDto;
import at.procon.eventhub.processing.model.UnifiedDiscoveredVehicleRef;
import at.procon.eventhub.processing.model.RuntimeDriverVehicleEvidenceAttachmentResult;
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBundle;
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
import com.fasterxml.jackson.databind.JsonNode;
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.Set;
import org.springframework.stereotype.Service;

@Service
public class UnifiedRuntimeTachographEsperScopeProcessingService {

    private final UnifiedRuntimeEventAssemblyService eventAssemblyService;
    private final UnifiedRuntimeDerivedProjectionService derivedProjectionService;
    private final RuntimeDriverVehicleEvidenceAttachmentService vehicleEvidenceAttachmentService;

    public UnifiedRuntimeTachographEsperScopeProcessingService(
            UnifiedRuntimeEventAssemblyService eventAssemblyService,
            UnifiedRuntimeDerivedProjectionService derivedProjectionService,
            RuntimeDriverVehicleEvidenceAttachmentService vehicleEvidenceAttachmentService
    ) {
        this.eventAssemblyService = eventAssemblyService;
        this.derivedProjectionService = derivedProjectionService;
        this.vehicleEvidenceAttachmentService = vehicleEvidenceAttachmentService;
    }

    public UnifiedRuntimeTachographEsperScopeResultDto processScope(UnifiedRuntimeProcessingApiRequest apiRequest) {
        UnifiedRuntimeProcessingRequest request = apiRequest.toRuntimeRequest();
        UnifiedRuntimeEventBundle broadBundle = eventAssemblyService.assembleDriverScopedEvents(request);
        LinkedHashSet<String> selectedDriverKeys = selectedDriverKeys(request, broadBundle.mergedEvents());
        if (selectedDriverKeys.isEmpty()) {
            throw new IllegalArgumentException("No driver partitions could be resolved from the runtime event scope.");
        }

        Map<String, UnifiedRuntimeDerivedProjectionResultDto> driverResults = new LinkedHashMap<>();
        Map<String, List<String>> attachedVehicleEvidenceByEvent = new LinkedHashMap<>();
        List<String> warnings = new ArrayList<>();
        for (String driverKey : selectedDriverKeys) {
            UnifiedRuntimeEventBundle driverBundle = partitionForDriver(request, broadBundle, driverKey);
            for (EventHubEventDto attachedEvent : driverBundle.expandedVehicleEvents()) {
                attachedVehicleEvidenceByEvent
                        .computeIfAbsent(dedupKey(attachedEvent), ignored -> new ArrayList<>())
                        .add(driverKey);
            }
            driverBundle.notes().stream()
                    .filter(note -> note.startsWith("WARNING:"))
                    .map(note -> note.substring("WARNING:".length()).trim())
                    .forEach(warnings::add);
            if (driverBundle.mergedEvents().isEmpty()) {
                warnings.add("No events remained after partitioning runtime scope for driver " + driverKey + ".");
                continue;
            }
            UnifiedRuntimeProcessingRequest driverRequest = request.withDriverKey(driverKey);
            UnifiedRuntimeDerivedProjectionResultDto driverResult = derivedProjectionService.buildDriverDerivedProjection(
                    apiRequest,
                    driverRequest,
                    driverBundle,
                    driverKey
            );
            driverResults.put(driverKey, driverResult);
        }
        attachedVehicleEvidenceByEvent.forEach((eventKey, drivers) -> {
            if (drivers.size() > 1) {
                warnings.add("Vehicle-only event " + eventKey + " was attached to multiple driver partitions "
                        + drivers + "; check overlapping vehicle-usage intervals.");
            }
        });

        List<String> notes = new ArrayList<>(broadBundle.notes());
        notes.add("Runtime tachograph Esper scope processing used Java-side driver partitioning before calling the common event-input Esper pipeline.");
        notes.add("Selected driver partitions: " + selectedDriverKeys.size() + ".");
        if (!request.includeAllDrivers() && !request.driverKeys().isEmpty()) {
            notes.add("The broad runtime event set was filtered to the requested driverKeys.");
        }
        if (!request.vehicleKeys().isEmpty() || request.includeAllVehicles()) {
            notes.add("vehicleKeys/includeAllVehicles are accepted in the request model for source-neutral scopes; driver partitions are enriched only with vehicle evidence that overlaps reconstructed driver vehicle-usage intervals.");
        }
        notes.add("Vehicle-only evidence attachment is controlled by expandVehicleEvents/attachVehicleOnlyEvents and vehicleExpansionPaddingMinutes/vehicleEvidencePaddingMinutes.");

        return new UnifiedRuntimeTachographEsperScopeResultDto(
                request,
                broadBundle.mergedEvents().size(),
                driverResults.size(),
                broadBundle.discoveredVehicles().size(),
                broadBundle.discoveredVehicles(),
                driverResults,
                notes,
                warnings
        );
    }

    private LinkedHashSet<String> selectedDriverKeys(
            UnifiedRuntimeProcessingRequest request,
            List<EventHubEventDto> events
    ) {
        LinkedHashSet<String> allDrivers = discoverDriverKeys(events);
        if (request.includeAllDrivers()) {
            return allDrivers;
        }
        LinkedHashSet<String> requested = new LinkedHashSet<>(request.driverKeys());
        if (request.driverKey() != null) {
            requested.add(request.driverKey());
        }
        if (requested.isEmpty()) {
            return allDrivers;
        }
        LinkedHashSet<String> selected = new LinkedHashSet<>();
        for (String driverKey : allDrivers) {
            if (requested.contains(driverKey)) {
                selected.add(driverKey);
            }
        }
        for (String driverKey : requested) {
            selected.add(driverKey);
        }
        return selected;
    }

    private UnifiedRuntimeEventBundle partitionForDriver(
            UnifiedRuntimeProcessingRequest request,
            UnifiedRuntimeEventBundle broadBundle,
            String driverKey
    ) {
        List<EventHubEventDto> directDriverEvents = broadBundle.mergedEvents().stream()
                .filter(event -> Objects.equals(driverKey(event), driverKey))
                .toList();
        RuntimeDriverVehicleEvidenceAttachmentResult attachmentResult = vehicleEvidenceAttachmentService.attachVehicleEvidence(
                driverKey,
                directDriverEvents,
                broadBundle.mergedEvents(),
                request.expandVehicleEvents(),
                request.vehicleExpansionPaddingMinutes()
        );
        List<UnifiedDiscoveredVehicleRef> driverVehicles = discoverVehicles(attachmentResult.mergedEvents());
        List<String> notes = new ArrayList<>(broadBundle.notes());
        notes.add("Partitioned mixed runtime event scope for driver " + driverKey + ".");
        notes.add("Driver direct events: " + attachmentResult.directDriverEvents().size() + ".");
        notes.add("Vehicle-only evidence events attached to driver partition: " + attachmentResult.attachedVehicleEvidenceEvents().size() + ".");
        notes.add("Vehicle-usage intervals used for temporal evidence attachment: " + attachmentResult.vehicleUsageIntervalCount() + ".");
        notes.addAll(attachmentResult.notes());
        attachmentResult.warnings().forEach(warning -> notes.add("WARNING: " + warning));
        return new UnifiedRuntimeEventBundle(
                request.withDriverKey(driverKey),
                attachmentResult.directDriverEvents(),
                driverVehicles,
                attachmentResult.attachedVehicleEvidenceEvents(),
                attachmentResult.mergedEvents(),
                notes
        );
    }

    private LinkedHashSet<String> discoverDriverKeys(List<EventHubEventDto> events) {
        LinkedHashSet<String> result = new LinkedHashSet<>();
        for (EventHubEventDto event : sort(events)) {
            String driverKey = driverKey(event);
            if (driverKey != null) {
                result.add(driverKey);
            }
        }
        return result;
    }

    private List<UnifiedDiscoveredVehicleRef> discoverVehicles(List<EventHubEventDto> events) {
        List<UnifiedDiscoveredVehicleRef> result = new ArrayList<>();
        for (EventHubEventDto event : events) {
            UnifiedDiscoveredVehicleRef candidate = vehicleRef(event.vehicleRef());
            if (candidate == null || !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 boolean matchesAnyVehicle(VehicleRefDto vehicleRef, List<UnifiedDiscoveredVehicleRef> vehicles) {
        UnifiedDiscoveredVehicleRef candidate = vehicleRef(vehicleRef);
        if (candidate == null || !candidate.hasAnyReference()) {
            return false;
        }
        return vehicles.stream().anyMatch(vehicle -> vehicle.matches(candidate));
    }

    private UnifiedDiscoveredVehicleRef vehicleRef(VehicleRefDto vehicleRef) {
        if (vehicleRef == null || !vehicleRef.hasAnyReference()) {
            return null;
        }
        return 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()
        );
    }

    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) {
            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 String driverKey(EventHubEventDto event) {
        if (event == null) {
            return null;
        }
        String rawDriverKey = text(rawPayload(event), "driverKey");
        if (rawDriverKey != null) {
            return rawDriverKey;
        }
        DriverRefDto driverRef = event.driverRef();
        if (driverRef != null && driverRef.hasAnyReference()) {
            return driverRef.stableKey();
        }
        return null;
    }

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