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

import at.procon.eventhub.processing.dto.UnifiedRuntimeDerivedProjectionResultDto;
import at.procon.eventhub.processing.dto.UnifiedRuntimeDriverWorkingTimeScopeResultDto;
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventPartitioningApiRequest;
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingPartitionResultDto;
import at.procon.eventhub.processing.eventprocessing.module.DriverWorkingTimeModuleKeys;
import at.procon.eventhub.processing.eventprocessing.module.RuntimeProcessingModuleContext;
import at.procon.eventhub.processing.eventprocessing.module.RuntimeProcessingModuleResult;
import at.procon.eventhub.processing.eventprocessing.module.RuntimeProcessingModuleStatus;
import at.procon.eventhub.processing.eventprocessing.module.RuntimeProcessingPipelineExecutor;
import at.procon.eventhub.processing.eventprocessing.module.RuntimeProcessingModuleRegistry;
import at.procon.eventhub.processing.eventprocessing.module.DriverActivityIntervalsModule;
import at.procon.eventhub.processing.eventprocessing.module.DriverVehicleUsageIntervalsModule;
import at.procon.eventhub.processing.eventprocessing.module.DriverVehicleUsageMergeModule;
import at.procon.eventhub.processing.eventprocessing.module.VehicleEvidenceAttachmentModule;
import at.procon.eventhub.processing.eventprocessing.module.SupportEvidenceNormalizationModule;
import at.procon.eventhub.processing.eventprocessing.module.DriverWorkingTimeDerivedProjectionsModule;
import at.procon.eventhub.processing.service.RuntimeDriverWorkingTimeScopeProcessingService;
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class DriverWorkingTimeRuntimeProcessingPlan implements RuntimeProcessingPlan {

    public static final String PLAN_KEY = "driver-working-time-v1";
    public static final String LEGACY_PROFILE_ALIAS = "tachograph-driver-esper-v1";

    private final RuntimeProcessingPipelineExecutor pipelineExecutor;
    private final boolean includeRuntimeEventAssemblyModule;

    @Autowired
    public DriverWorkingTimeRuntimeProcessingPlan(RuntimeProcessingPipelineExecutor pipelineExecutor) {
        this.pipelineExecutor = pipelineExecutor;
        this.includeRuntimeEventAssemblyModule = true;
    }

    /**
     * Compatibility constructor for older unit tests and legacy adapters that instantiated the
     * driver-working-time plan directly from the old scope service. This constructor builds a
     * small local module registry around the existing scope-processing adapter. It intentionally
     * omits the runtime-event-assembly module because the scope-processing adapter performs that
     * assembly internally.
     */
    public DriverWorkingTimeRuntimeProcessingPlan(
            RuntimeDriverWorkingTimeScopeProcessingService scopeProcessingService
    ) {
        this(new RuntimeProcessingPipelineExecutor(new RuntimeProcessingModuleRegistry(List.of(
                new DriverActivityIntervalsModule(),
                new DriverVehicleUsageIntervalsModule(),
                new DriverVehicleUsageMergeModule(),
                new VehicleEvidenceAttachmentModule(),
                new SupportEvidenceNormalizationModule(),
                new DriverWorkingTimeDerivedProjectionsModule(scopeProcessingService)
        ))), false);
    }

    private DriverWorkingTimeRuntimeProcessingPlan(
            RuntimeProcessingPipelineExecutor pipelineExecutor,
            boolean includeRuntimeEventAssemblyModule
    ) {
        this.pipelineExecutor = pipelineExecutor;
        this.includeRuntimeEventAssemblyModule = includeRuntimeEventAssemblyModule;
    }

    @Override
    public String processingPlanKey() {
        return PLAN_KEY;
    }

    @Override
    public Set<String> aliases() {
        return Set.of(LEGACY_PROFILE_ALIAS);
    }

    @Override
    public RuntimeEventPartitioningStrategy defaultPartitioningStrategy() {
        return RuntimeEventPartitioningStrategy.DRIVER;
    }

    @Override
    public String displayName() {
        return "Driver working-time processing";
    }

    @Override
    public String description() {
        return "Loads canonical EventHub runtime events from selected sources, partitions them by driver, "
                + "attaches vehicle-only evidence by vehicle/time, normalizes mixed-source support evidence, "
                + "then executes source-neutral driver working-time processing modules for driving intervals, "
                + "interruptions, rest candidates, overnight candidates, trips, vehicle usage, and card-absence coverage.";
    }

    @Override
    public List<RuntimeEventPartitioningStrategy> supportedPartitioningStrategies() {
        return List.of(RuntimeEventPartitioningStrategy.DRIVER);
    }

    @Override
    public List<RuntimeProcessingModuleDescriptorDto> modules() {
        List<RuntimeProcessingModuleDescriptorDto> descriptors = new java.util.ArrayList<>();
        if (includeRuntimeEventAssemblyModule) {
            descriptors.add(new RuntimeProcessingModuleDescriptorDto(
                        DriverWorkingTimeModuleKeys.RUNTIME_EVENT_ASSEMBLY,
                        "Runtime event assembly",
                        "Loads and merges canonical runtime events from the selected source scope before plan-specific processing.",
                        "JAVA",
                        Set.of("UnifiedRuntimeEventBundle")
                ));
        }
        descriptors.addAll(List.of(
                new RuntimeProcessingModuleDescriptorDto(
                        DriverWorkingTimeModuleKeys.EVENT_TO_ACTIVITY_INTERVALS,
                        "Event to activity intervals",
                        "Pairs canonical DRIVER_ACTIVITY START/END point events into source-neutral activity intervals using a first-class EPL module.",
                        "ESPER",
                        Set.of("DriverActivityIntervalEvent")
                ),
                new RuntimeProcessingModuleDescriptorDto(
                        DriverWorkingTimeModuleKeys.EVENT_TO_VEHICLE_USAGE_INTERVALS,
                        "Event to vehicle usage intervals",
                        "Pairs canonical driver/card INSERT/WITHDRAW point events into source-neutral vehicle-usage intervals using a first-class EPL module.",
                        "ESPER",
                        Set.of("DriverVehicleUsageIntervalEvent")
                ),
                new RuntimeProcessingModuleDescriptorDto(
                        DriverWorkingTimeModuleKeys.VEHICLE_USAGE_MERGE,
                        "Vehicle usage merge",
                        "Merges adjacent same-driver/same-vehicle usage intervals, including 23:59:59 to 00:00:00 continuations. Currently delegated to the derived projections adapter.",
                        "JAVA/ESPER",
                        Set.of("DriverVehicleUsageIntervalEvent")
                ),
                new RuntimeProcessingModuleDescriptorDto(
                        DriverWorkingTimeModuleKeys.VEHICLE_EVIDENCE_ATTACHMENT,
                        "Vehicle evidence attachment",
                        "Attaches vehicle-only events to driver partitions when they overlap driver vehicle-usage intervals.",
                        "JAVA",
                        Set.of("RuntimeDriverPartitionDebugDto")
                ),
                new RuntimeProcessingModuleDescriptorDto(
                        DriverWorkingTimeModuleKeys.SUPPORT_EVIDENCE_NORMALIZATION,
                        "Support evidence normalization",
                        "Normalizes attached mixed-source support evidence for driver working-time processing.",
                        "JAVA",
                        Set.of("RuntimeSupportEvidenceNormalizationDebugDto")
                ),
                new RuntimeProcessingModuleDescriptorDto(
                        DriverWorkingTimeModuleKeys.DRIVING_DERIVED_PROJECTIONS,
                        "Driving-derived projections",
                        "Runs the shared driver working-time derived projection module for driving interruptions, rest candidates, trips, and overnight candidates.",
                        "ESPER+JAVA",
                        Set.of("DriverWorkingTimeProcessingResultDto")
                )
        ));
        return List.copyOf(descriptors);
    }

    @Override
    public Set<String> optionalParameters() {
        return Set.of(
                "significantDrivingMinutes",
                "minimumRestPeriodMinutes",
                "attachVehicleOnlyEvents",
                "vehicleEvidencePaddingMinutes",
                "includePartitionDebug"
        );
    }

    @Override
    public RuntimeProcessingExecutionResultDto execute(RuntimeProcessingExecutionApiRequest request) {
        boolean includePartitionDebug = booleanParameter(
                request.parameters(),
                "includePartitionDebug",
                request.partitioning() != null && request.partitioning().includeDebugOrDefault()
        );
        UnifiedRuntimeProcessingApiRequest scopeRequest = applyExecutionRequest(
                request.sourceSelection(),
                request.partitioning(),
                request.parameters()
        );

        Map<String, Object> attributes = new LinkedHashMap<>();
        attributes.put("runtimeScopeApiRequest", scopeRequest);
        attributes.put("includePartitionDebug", includePartitionDebug);
        RuntimeProcessingModuleContext initialContext = new RuntimeProcessingModuleContext(
                request,
                List.of(),
                attributes,
                Map.of()
        );

        List<String> executedModules = requestedOrDefaultModules(request.modules());
        Map<String, RuntimeProcessingModuleResult> moduleResults = pipelineExecutor.execute(
                request,
                executedModules,
                initialContext
        );
        UnifiedRuntimeDriverWorkingTimeScopeResultDto workingTimeResult = extractWorkingTimeResult(moduleResults);

        Map<String, RuntimeEventProcessingPartitionResultDto> partitionResults = new LinkedHashMap<>();
        workingTimeResult.driverResults().forEach((driverKey, driverResult) -> {
            Map<String, Object> metadata = new LinkedHashMap<>();
            metadata.put("projectionResultType", driverResult.projection() == null ? "NONE" : "DriverWorkingTimeProcessingResultDto");
            metadata.put("driverSeedEventCount", driverResult.driverSeedEventCount());
            metadata.put("expandedVehicleEventCount", driverResult.expandedVehicleEventCount());
            metadata.put("mergedEventCount", driverResult.mergedEventCount());
            if (driverResult.supportEvidenceNormalization() != null) {
                metadata.put("supportEvidenceNormalization", driverResult.supportEvidenceNormalization());
            }
            if (driverResult.partitionDebug() != null) {
                metadata.put("partitionDebug", driverResult.partitionDebug());
            }
            partitionResults.put(
                    driverKey,
                    new RuntimeEventProcessingPartitionResultDto(
                            "DRIVER",
                            driverKey,
                            "UnifiedRuntimeDerivedProjectionResultDto",
                            driverResult,
                            metadata,
                            partitionModuleResults(driverResult)
                    )
            );
        });

        return new RuntimeProcessingExecutionResultDto(
                processingPlanKey(),
                executedModules,
                RuntimeEventPartitioningStrategy.DRIVER,
                workingTimeResult.request(),
                workingTimeResult.inputEventCount(),
                workingTimeResult.selectedDriverCount(),
                workingTimeResult.discoveredVehicleCount(),
                workingTimeResult.discoveredVehicles(),
                sanitizeExecutionModuleResults(moduleResults),
                partitionResults,
                workingTimeResult.notes(),
                workingTimeResult.warnings()
        );
    }

    private UnifiedRuntimeDriverWorkingTimeScopeResultDto extractWorkingTimeResult(
            Map<String, RuntimeProcessingModuleResult> moduleResults
    ) {
        RuntimeProcessingModuleResult finalResult = moduleResults.get(DriverWorkingTimeModuleKeys.DRIVING_DERIVED_PROJECTIONS);
        if (finalResult == null) {
            throw new IllegalArgumentException("Processing plan " + PLAN_KEY
                    + " requires module " + DriverWorkingTimeModuleKeys.DRIVING_DERIVED_PROJECTIONS + ".");
        }
        if (finalResult.status() == RuntimeProcessingModuleStatus.FAILED) {
            throw new IllegalStateException("Processing module " + finalResult.moduleKey() + " failed: " + finalResult.warnings());
        }
        Object output = finalResult.output();
        if (output instanceof UnifiedRuntimeDriverWorkingTimeScopeResultDto result) {
            return result;
        }
        throw new IllegalStateException("Processing module " + DriverWorkingTimeModuleKeys.DRIVING_DERIVED_PROJECTIONS
                + " did not return UnifiedRuntimeDriverWorkingTimeScopeResultDto. Actual: "
                + (output == null ? "null" : output.getClass().getName()));
    }

    private Map<String, RuntimeProcessingModuleResult> sanitizeExecutionModuleResults(
            Map<String, RuntimeProcessingModuleResult> moduleResults
    ) {
        LinkedHashMap<String, RuntimeProcessingModuleResult> sanitized = new LinkedHashMap<>();
        for (Map.Entry<String, RuntimeProcessingModuleResult> entry : moduleResults.entrySet()) {
            RuntimeProcessingModuleResult result = entry.getValue();
            sanitized.put(entry.getKey(), new RuntimeProcessingModuleResult(
                    result.moduleKey(),
                    result.status(),
                    null,
                    result.metadata(),
                    result.warnings()
            ));
        }
        return sanitized;
    }

    private Map<String, RuntimeProcessingModuleResult> partitionModuleResults(
            UnifiedRuntimeDerivedProjectionResultDto driverResult
    ) {
        LinkedHashMap<String, RuntimeProcessingModuleResult> results = new LinkedHashMap<>();
        Map<String, Object> attachmentMetadata = new LinkedHashMap<>();
        attachmentMetadata.put("driverSeedEventCount", driverResult.driverSeedEventCount());
        attachmentMetadata.put("expandedVehicleEventCount", driverResult.expandedVehicleEventCount());
        attachmentMetadata.put("mergedEventCount", driverResult.mergedEventCount());
        results.put(
                DriverWorkingTimeModuleKeys.VEHICLE_EVIDENCE_ATTACHMENT,
                new RuntimeProcessingModuleResult(
                        DriverWorkingTimeModuleKeys.VEHICLE_EVIDENCE_ATTACHMENT,
                        RuntimeProcessingModuleStatus.SUCCESS,
                        driverResult.partitionDebug(),
                        attachmentMetadata,
                        List.of()
                )
        );
        Map<String, Object> normalizationMetadata = new LinkedHashMap<>();
        if (driverResult.supportEvidenceNormalization() != null) {
            normalizationMetadata.put("inputEventCount", driverResult.supportEvidenceNormalization().inputEventCount());
            normalizationMetadata.put("normalizedSupportEvidenceEventCount", driverResult.supportEvidenceNormalization().normalizedSupportEvidenceEventCount());
            normalizationMetadata.put("unchangedEventCount", driverResult.supportEvidenceNormalization().unchangedEventCount());
        }
        results.put(
                DriverWorkingTimeModuleKeys.SUPPORT_EVIDENCE_NORMALIZATION,
                new RuntimeProcessingModuleResult(
                        DriverWorkingTimeModuleKeys.SUPPORT_EVIDENCE_NORMALIZATION,
                        RuntimeProcessingModuleStatus.SUCCESS,
                        driverResult.supportEvidenceNormalization(),
                        normalizationMetadata,
                        List.of()
                )
        );
        results.put(
                DriverWorkingTimeModuleKeys.DRIVING_DERIVED_PROJECTIONS,
                new RuntimeProcessingModuleResult(
                        DriverWorkingTimeModuleKeys.DRIVING_DERIVED_PROJECTIONS,
                        RuntimeProcessingModuleStatus.SUCCESS,
                        driverResult.projection(),
                        Map.of(
                                "resultType", driverResult.projection() == null ? "NONE" : "DriverWorkingTimeProcessingResultDto",
                                "noteCount", driverResult.notes().size()
                        ),
                        List.of()
                )
        );
        return results;
    }

    public UnifiedRuntimeProcessingApiRequest applyExecutionRequest(
            UnifiedRuntimeProcessingApiRequest sourceSelection,
            RuntimeEventPartitioningApiRequest partitioning,
            Map<String, Object> parameters
    ) {
        if (sourceSelection == null) {
            throw new IllegalArgumentException("sourceSelection must not be null for processing plan " + PLAN_KEY + ".");
        }
        boolean includeAllDrivers = sourceSelection.includeAllDrivers() != null && sourceSelection.includeAllDrivers();
        java.util.Set<String> driverKeys = sourceSelection.driverKeys();
        boolean includeAllVehicles = sourceSelection.includeAllVehicles() != null && sourceSelection.includeAllVehicles();
        java.util.Set<String> vehicleKeys = sourceSelection.vehicleKeys();

        if (partitioning != null) {
            RuntimeEventPartitioningStrategy strategy = partitioning.strategy() == null
                    ? RuntimeEventPartitioningStrategy.DRIVER
                    : partitioning.strategy();
            if (strategy != RuntimeEventPartitioningStrategy.DRIVER
                    && strategy != RuntimeEventPartitioningStrategy.CUSTOM_PROFILE) {
                throw new IllegalArgumentException("Processing plan " + PLAN_KEY
                        + " currently supports DRIVER partitioning only. Requested: " + strategy);
            }
            includeAllDrivers = includeAllDrivers || partitioning.allPartitions() || partitioning.allDrivers();
            if (!partitioning.driverKeys().isEmpty()) {
                driverKeys = partitioning.driverKeys();
            } else if (!partitioning.partitionKeys().isEmpty()) {
                driverKeys = partitioning.partitionKeys();
            }
            includeAllVehicles = includeAllVehicles || partitioning.allVehicles();
            if (!partitioning.vehicleKeys().isEmpty()) {
                vehicleKeys = partitioning.vehicleKeys();
            }
        }

        Integer significantDrivingMinutes = integerParameter(parameters, "significantDrivingMinutes", sourceSelection.significantDrivingMinutes());
        Integer minimumRestPeriodMinutes = integerParameter(parameters, "minimumRestPeriodMinutes", sourceSelection.minimumRestPeriodMinutes());
        boolean attachVehicleOnlyEvents = booleanParameter(parameters, "attachVehicleOnlyEvents",
                partitioning == null ? sourceSelection.expandVehicleEvents() == null || sourceSelection.expandVehicleEvents() : partitioning.attachVehicleEvidenceOrDefault());
        Integer vehicleEvidencePaddingMinutes = nonNegativeIntegerParameter(parameters, "vehicleEvidencePaddingMinutes",
                partitioning == null
                        ? sourceSelection.vehicleExpansionPaddingMinutes()
                        : partitioning.vehicleEvidencePaddingMinutesOrDefault(sourceSelection.vehicleExpansionPaddingMinutes() == null ? 0 : sourceSelection.vehicleExpansionPaddingMinutes()));

        return new UnifiedRuntimeProcessingApiRequest(
                sourceSelection.sessionId(),
                sourceSelection.sessionIds(),
                sourceSelection.compositeSessionId(),
                sourceSelection.tenantKey(),
                sourceSelection.sourceFamilies(),
                sourceSelection.eventBackend(),
                sourceSelection.tachographSourceKinds(),
                sourceSelection.driverKey(),
                driverKeys,
                includeAllDrivers,
                vehicleKeys,
                includeAllVehicles,
                sourceSelection.driverSourceEntityId(),
                sourceSelection.driverCardNation(),
                sourceSelection.driverCardNumber(),
                sourceSelection.occurredFrom(),
                sourceSelection.occurredTo(),
                attachVehicleOnlyEvents,
                vehicleEvidencePaddingMinutes,
                significantDrivingMinutes,
                minimumRestPeriodMinutes
        );
    }

    private List<String> requestedOrDefaultModules(List<String> requestedModules) {
        if (requestedModules != null && !requestedModules.isEmpty()) {
            LinkedHashMap<String, String> requested = new LinkedHashMap<>();
            for (String module : requestedModules) {
                if (module != null && !module.isBlank()) {
                    requested.put(module.trim(), module.trim());
                }
            }
            requested.putIfAbsent(DriverWorkingTimeModuleKeys.DRIVING_DERIVED_PROJECTIONS,
                    DriverWorkingTimeModuleKeys.DRIVING_DERIVED_PROJECTIONS);
            return List.copyOf(requested.values());
        }
        return modules().stream()
                .map(RuntimeProcessingModuleDescriptorDto::moduleKey)
                .toList();
    }

    private boolean booleanParameter(Map<String, Object> parameters, String key, boolean fallback) {
        if (parameters == null || !parameters.containsKey(key)) {
            return fallback;
        }
        Object value = parameters.get(key);
        if (value == null) {
            return fallback;
        }
        if (value instanceof Boolean booleanValue) {
            return booleanValue;
        }
        String text = value.toString();
        if (text == null || text.isBlank()) {
            return fallback;
        }
        return Boolean.parseBoolean(text.trim());
    }

    private Integer nonNegativeIntegerParameter(Map<String, Object> parameters, String key, Integer fallback) {
        if (parameters == null || !parameters.containsKey(key)) {
            return fallback;
        }
        Object value = parameters.get(key);
        if (value == null) {
            return fallback;
        }
        if (value instanceof Number number) {
            return Math.max(0, number.intValue());
        }
        String text = value.toString();
        if (text == null || text.isBlank()) {
            return fallback;
        }
        try {
            return Math.max(0, Integer.parseInt(text.trim()));
        } catch (NumberFormatException ex) {
            throw new IllegalArgumentException("Parameter '" + key + "' must be an integer.", ex);
        }
    }

    private Integer integerParameter(Map<String, Object> parameters, String key, Integer fallback) {
        if (parameters == null || !parameters.containsKey(key)) {
            return fallback;
        }
        Object value = parameters.get(key);
        if (value == null) {
            return fallback;
        }
        if (value instanceof Number number) {
            return Math.max(1, number.intValue());
        }
        String text = value.toString();
        if (text == null || text.isBlank()) {
            return fallback;
        }
        try {
            return Math.max(1, Integer.parseInt(text.trim()));
        } catch (NumberFormatException ex) {
            throw new IllegalArgumentException("Parameter '" + key + "' must be an integer.", ex);
        }
    }
}
