package at.procon.eventhub.processing.eventprocessing.module;

import at.procon.eventhub.dto.DriverRefDto;
import at.procon.eventhub.dto.EventHubEventDto;
import at.procon.eventhub.dto.VehicleRefDto;
import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeDriverPartition;
import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeVehicleUsageInterval;
import at.procon.eventhub.processing.dto.RuntimeDriverPartitionDebugDto;
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingModuleDescriptorDto;
import at.procon.eventhub.processing.model.RuntimeDriverVehicleEvidenceAttachmentResult;
import at.procon.eventhub.processing.model.UnifiedDiscoveredVehicleRef;
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBundle;
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
import at.procon.eventhub.processing.service.RuntimeDriverVehicleEvidenceAttachmentService;
import at.procon.eventhub.processing.support.TachographRuntimeIdentityResolver;
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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class VehicleEvidenceAttachmentModule implements RuntimeProcessingModule {

    private final RuntimeDriverVehicleEvidenceAttachmentService vehicleEvidenceAttachmentService;

    @Autowired
    public VehicleEvidenceAttachmentModule(
            RuntimeDriverVehicleEvidenceAttachmentService vehicleEvidenceAttachmentService
    ) {
        this.vehicleEvidenceAttachmentService = vehicleEvidenceAttachmentService;
    }

    /** Compatibility constructor for legacy tests/local registries that still delegate the phase. */
    public VehicleEvidenceAttachmentModule() {
        this.vehicleEvidenceAttachmentService = null;
    }

    @Override
    public String moduleKey() {
        return DriverWorkingTimeModuleKeys.VEHICLE_EVIDENCE_ATTACHMENT;
    }

    @Override
    public RuntimeProcessingModuleDescriptorDto descriptor() {
        return new RuntimeProcessingModuleDescriptorDto(
                moduleKey(),
                "Vehicle evidence attachment",
                "Partitions the broad runtime scope by driver and attaches vehicle-only evidence using merged vehicle-usage intervals from the common pipeline.",
                "JAVA",
                Set.of("Map<String, DriverWorkingTimeDriverPartition>")
        );
    }

    @Override
    public RuntimeProcessingModuleResult execute(RuntimeProcessingModuleContext context) {
        if (vehicleEvidenceAttachmentService == null) {
            return delegatedPlaceholder();
        }
        UnifiedRuntimeEventBundle broadBundle = runtimeEventBundle(context);
        UnifiedRuntimeProcessingApiRequest scopeRequest = scopeRequest(context);
        boolean includePartitionDebug = booleanAttribute(context, "includePartitionDebug", false);
        List<DriverWorkingTimeVehicleUsageInterval> mergedVehicleUsageIntervals = mergedVehicleUsageIntervals(context);

        LinkedHashMap<String, DriverWorkingTimeDriverPartition> partitions = new LinkedHashMap<>();
        LinkedHashMap<String, List<String>> attachedVehicleEvidenceByEvent = new LinkedHashMap<>();
        List<String> warnings = new ArrayList<>();
        for (String driverKey : selectedDriverKeys(scopeRequest.toRuntimeRequest(), broadBundle.mergedEvents())) {
            List<EventHubEventDto> directDriverEvents = broadBundle.mergedEvents().stream()
                    .filter(event -> Objects.equals(driverKey(event), driverKey))
                    .toList();
            List<DriverWorkingTimeVehicleUsageInterval> driverVehicleUsageIntervals = mergedVehicleUsageIntervals.stream()
                    .filter(interval -> Objects.equals(driverKey, interval.driverKey()))
                    .toList();
            RuntimeDriverVehicleEvidenceAttachmentResult attachmentResult = vehicleEvidenceAttachmentService.attachVehicleEvidence(
                    driverKey,
                    directDriverEvents,
                    broadBundle.mergedEvents(),
                    driverVehicleUsageIntervals,
                    scopeRequest.expandVehicleEvents() == null || scopeRequest.expandVehicleEvents(),
                    scopeRequest.vehicleExpansionPaddingMinutes() == null ? 0 : scopeRequest.vehicleExpansionPaddingMinutes(),
                    includePartitionDebug
            );
            for (EventHubEventDto attachedEvent : attachmentResult.attachedVehicleEvidenceEvents()) {
                attachedVehicleEvidenceByEvent
                        .computeIfAbsent(dedupKey(attachedEvent), ignored -> new ArrayList<>())
                        .add(driverKey);
            }
            RuntimeDriverPartitionDebugDto partitionDebug = includePartitionDebug ? attachmentResult.toPartitionDebug() : null;
            partitions.put(driverKey, new DriverWorkingTimeDriverPartition(
                    driverKey,
                    attachmentResult.directDriverEvents(),
                    attachmentResult.attachedVehicleEvidenceEvents(),
                    attachmentResult.mergedEvents(),
                    discoverVehicles(attachmentResult.mergedEvents()),
                    driverVehicleUsageIntervals,
                    partitionDebug,
                    List.of(),
                    null,
                    attachmentResult.notes(),
                    attachmentResult.warnings()
            ));
            warnings.addAll(attachmentResult.warnings());
        }

        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.");
            }
        });

        Map<String, Object> metadata = new LinkedHashMap<>();
        metadata.put("selectedDriverCount", partitions.size());
        metadata.put("inputVehicleUsageIntervalCount", mergedVehicleUsageIntervals.size());
        metadata.put("attachedVehicleEvidenceEventCount", partitions.values().stream()
                .mapToInt(partition -> partition.attachedVehicleEvidenceEvents().size())
                .sum());
        return new RuntimeProcessingModuleResult(
                moduleKey(),
                RuntimeProcessingModuleStatus.SUCCESS,
                Map.copyOf(partitions),
                metadata,
                List.copyOf(warnings)
        );
    }

    private RuntimeProcessingModuleResult delegatedPlaceholder() {
        Map<String, Object> metadata = new LinkedHashMap<>();
        metadata.put("executionModel", "delegated");
        metadata.put("delegatedTo", DriverWorkingTimeModuleKeys.DRIVING_DERIVED_PROJECTIONS);
        metadata.put("note", "This logical module is executed inside the legacy derived projections adapter.");
        return new RuntimeProcessingModuleResult(
                moduleKey(),
                RuntimeProcessingModuleStatus.SUCCESS,
                Map.of(),
                metadata,
                List.of()
        );
    }

    private UnifiedRuntimeEventBundle runtimeEventBundle(RuntimeProcessingModuleContext context) {
        RuntimeProcessingModuleResult result = context.previousResults().get(DriverWorkingTimeModuleKeys.RUNTIME_EVENT_ASSEMBLY);
        if (result != null && result.output() instanceof UnifiedRuntimeEventBundle bundle) {
            return bundle;
        }
        throw new IllegalStateException("Module " + moduleKey()
                + " requires previous result " + DriverWorkingTimeModuleKeys.RUNTIME_EVENT_ASSEMBLY + ".");
    }

    private UnifiedRuntimeProcessingApiRequest scopeRequest(RuntimeProcessingModuleContext context) {
        Object value = context.attributes().get("runtimeScopeApiRequest");
        if (value instanceof UnifiedRuntimeProcessingApiRequest request) {
            return request;
        }
        return context.request().sourceSelection();
    }

    @SuppressWarnings("unchecked")
    private List<DriverWorkingTimeVehicleUsageInterval> mergedVehicleUsageIntervals(RuntimeProcessingModuleContext context) {
        RuntimeProcessingModuleResult result = context.previousResults().get(DriverWorkingTimeModuleKeys.VEHICLE_USAGE_MERGE);
        if (result == null || !(result.output() instanceof List<?> list)) {
            return List.of();
        }
        return (List<DriverWorkingTimeVehicleUsageInterval>) list;
    }

    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);
            }
        }
        selected.addAll(requested);
        return selected;
    }

    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 == null ? List.<EventHubEventDto>of() : 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 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> 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) {
        return TachographRuntimeIdentityResolver.driverKey(event);
    }

    private JsonNode rawPayload(EventHubEventDto event) {
        JsonNode payload = event == null ? null : 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();
    }

    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 boolean booleanAttribute(RuntimeProcessingModuleContext context, String key, boolean fallback) {
        Object value = context.attributes().get(key);
        if (value instanceof Boolean booleanValue) {
            return booleanValue;
        }
        return fallback;
    }
}
