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

import at.procon.eventhub.processing.dto.UnifiedRuntimeDerivedProjectionResultDto;
import at.procon.eventhub.processing.driverworkingtime.dto.DriverWorkingTimeProcessingResultDto;
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
import at.procon.eventhub.processing.eventprocessing.RuntimeEventProcessingService;
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventPartitioningApiRequest;
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingApiRequest;
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingPartitionResultDto;
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingResultDto;
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy;
import at.procon.eventhub.processing.eventprocessing.profile.TachographDriverEsperRuntimeEventProcessingProfile;
import at.procon.eventhub.processing.model.UnifiedEventSourceFamily;
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBackend;
import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto;
import at.procon.eventhub.tachographfilesession.dto.TachographEsperEventsProcessingRequest;
import at.procon.eventhub.tachographfilesession.model.TachographCompositeSession;
import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
import at.procon.eventhub.tachographfilesession.service.TachographCompositeSessionNotFoundException;
import at.procon.eventhub.tachographfilesession.service.TachographCompositeSessionRepository;
import at.procon.eventhub.tachographfilesession.service.TachographFileSessionNotFoundException;
import at.procon.eventhub.tachographfilesession.service.TachographFileSessionProcessingService;
import at.procon.eventhub.tachographfilesession.service.TachographFileSessionRepository;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import org.springframework.stereotype.Service;

@Service
public class RuntimeTachographParityValidationService {

    private final TachographFileSessionRepository fileSessionRepository;
    private final TachographCompositeSessionRepository compositeSessionRepository;
    private final TachographFileSessionProcessingService fileSessionProcessingService;
    private final RuntimeEventProcessingService runtimeEventProcessingService;

    public RuntimeTachographParityValidationService(
            TachographFileSessionRepository fileSessionRepository,
            TachographCompositeSessionRepository compositeSessionRepository,
            TachographFileSessionProcessingService fileSessionProcessingService,
            RuntimeEventProcessingService runtimeEventProcessingService
    ) {
        this.fileSessionRepository = fileSessionRepository;
        this.compositeSessionRepository = compositeSessionRepository;
        this.fileSessionProcessingService = fileSessionProcessingService;
        this.runtimeEventProcessingService = runtimeEventProcessingService;
    }

    public RuntimeTachographParityValidationResultDto validate(RuntimeTachographParityValidationApiRequest request) {
        RuntimeTachographParityValidationApiRequest effectiveRequest = request == null
                ? new RuntimeTachographParityValidationApiRequest(null, List.of(), null, null, Set.of(), true,
                null, null, true, 0, null, null, false)
                : request;
        List<UUID> sessionIds = resolveSessionIds(effectiveRequest);
        Set<String> driverKeys = resolveDriverKeys(effectiveRequest, sessionIds);
        if (driverKeys.isEmpty()) {
            throw new IllegalArgumentException("No driver keys could be resolved for tachograph parity validation.");
        }

        RuntimeEventProcessingResultDto runtimeResult = runtimeEventProcessingService.process(genericRuntimeRequest(
                effectiveRequest,
                sessionIds,
                driverKeys
        ));

        LinkedHashMap<String, RuntimeTachographDriverParityResultDto> driverResults = new LinkedHashMap<>();
        List<String> warnings = new ArrayList<>(runtimeResult.warnings());
        for (String driverKey : driverKeys) {
            RuntimeTachographDriverParityResultDto driverResult = compareDriver(
                    effectiveRequest,
                    sessionIds,
                    driverKey,
                    runtimeResult
            );
            driverResults.put(driverKey, driverResult);
            warnings.addAll(driverResult.warnings());
        }

        String status = driverResults.values().stream().allMatch(result -> "EQUAL".equals(result.status()))
                ? "EQUAL"
                : "DIFFERENT";
        List<String> notes = new ArrayList<>(runtimeResult.notes());
        notes.add("Validation compares the legacy tachograph file-session esper-events path with the generic runtime event-processing profile '"
                + TachographDriverEsperRuntimeEventProcessingProfile.PROFILE_KEY + "'.");
        if (sessionIds.size() > 1) {
            notes.add("For multiple sessions, file-session reference counts are summed per driver. Runtime processing may intentionally merge or deduplicate intervals across session boundaries, so category differences need domain review.");
        }

        return new RuntimeTachographParityValidationResultDto(
                TachographDriverEsperRuntimeEventProcessingProfile.PROFILE_KEY,
                status,
                sessionIds,
                driverResults.size(),
                driverResults,
                notes,
                warnings
        );
    }

    private RuntimeTachographDriverParityResultDto compareDriver(
            RuntimeTachographParityValidationApiRequest request,
            List<UUID> sessionIds,
            String driverKey,
            RuntimeEventProcessingResultDto runtimeResult
    ) {
        RuntimeProjectionCounts runtimeCounts = runtimeCounts(runtimeResult, driverKey);
        ReferenceProjectionCounts referenceCounts = referenceCounts(request, sessionIds, driverKey);
        List<RuntimeTachographParityCategoryComparisonDto> comparisons = comparisons(referenceCounts.counts(), runtimeCounts.counts());
        boolean runtimePresent = runtimeCounts.present();
        boolean referencePresent = referenceCounts.referenceSessionCount() > 0;
        boolean allEqual = comparisons.stream().allMatch(RuntimeTachographParityCategoryComparisonDto::equal);

        String status;
        if (!referencePresent) {
            status = "REFERENCE_MISSING";
        } else if (!runtimePresent) {
            status = "RUNTIME_MISSING";
        } else if (allEqual) {
            status = "EQUAL";
        } else {
            status = "DIFFERENT";
        }

        List<String> notes = new ArrayList<>();
        notes.add("Reference mode: " + referenceCounts.referenceMode() + ".");
        notes.add("Reference session count: " + referenceCounts.referenceSessionCount() + ".");
        if (sessionIds.size() > 1) {
            notes.add("Reference counts are summed from individual file-session endpoint results for this driver.");
        }
        List<String> warnings = new ArrayList<>();
        if (!referencePresent) {
            warnings.add("Driver " + driverKey + " was not found in any selected tachograph file session.");
        }
        if (!runtimePresent) {
            warnings.add("Generic runtime event-processing profile did not return a partition for driver " + driverKey + ".");
        }
        if (runtimePresent && referencePresent && !allEqual) {
            warnings.add("Runtime result differs from file-session reference result for driver " + driverKey + ".");
        }
        return new RuntimeTachographDriverParityResultDto(
                driverKey,
                status,
                referenceCounts.referenceMode(),
                referenceCounts.referenceSessionCount(),
                runtimePresent,
                comparisons,
                notes,
                warnings
        );
    }

    private RuntimeEventProcessingApiRequest genericRuntimeRequest(
            RuntimeTachographParityValidationApiRequest request,
            List<UUID> sessionIds,
            Set<String> driverKeys
    ) {
        UnifiedRuntimeProcessingApiRequest scope = new UnifiedRuntimeProcessingApiRequest(
                null,
                sessionIds,
                null,
                null,
                Set.of(UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION),
                UnifiedRuntimeEventBackend.SOURCE_DB,
                null,
                null,
                driverKeys,
                request.includeAllDriversOrDefault(),
                null,
                false,
                null,
                null,
                null,
                request.occurredFrom(),
                request.occurredTo(),
                request.expandVehicleEventsOrDefault(),
                request.vehicleExpansionPaddingMinutesOrDefault(),
                request.significantDrivingMinutes(),
                request.minimumRestPeriodMinutes()
        );
        RuntimeEventPartitioningApiRequest partitioning = new RuntimeEventPartitioningApiRequest(
                RuntimeEventPartitioningStrategy.DRIVER,
                null,
                request.includeAllDriversOrDefault(),
                driverKeys,
                request.includeAllDriversOrDefault(),
                null,
                false,
                request.expandVehicleEventsOrDefault(),
                request.vehicleExpansionPaddingMinutesOrDefault(),
                request.includeDebugOrDefault()
        );
        Map<String, Object> parameters = new LinkedHashMap<>();
        if (request.significantDrivingMinutes() != null) {
            parameters.put("significantDrivingMinutes", request.significantDrivingMinutes());
        }
        if (request.minimumRestPeriodMinutes() != null) {
            parameters.put("minimumRestPeriodMinutes", request.minimumRestPeriodMinutes());
        }
        parameters.put("attachVehicleOnlyEvents", request.expandVehicleEventsOrDefault());
        parameters.put("vehicleEvidencePaddingMinutes", request.vehicleExpansionPaddingMinutesOrDefault());
        parameters.put("includePartitionDebug", request.includeDebugOrDefault());
        return new RuntimeEventProcessingApiRequest(
                TachographDriverEsperRuntimeEventProcessingProfile.PROFILE_KEY,
                scope,
                partitioning,
                parameters
        );
    }

    private List<UUID> resolveSessionIds(RuntimeTachographParityValidationApiRequest request) {
        LinkedHashSet<UUID> result = new LinkedHashSet<>();
        if (request.sessionId() != null) {
            result.add(request.sessionId());
        }
        result.addAll(request.sessionIds());
        if (request.compositeSessionId() != null) {
            TachographCompositeSession compositeSession = compositeSessionRepository.find(request.compositeSessionId())
                    .orElseThrow(() -> new TachographCompositeSessionNotFoundException(request.compositeSessionId()));
            result.addAll(compositeSession.memberSessionIds());
        }
        if (result.isEmpty()) {
            throw new IllegalArgumentException("sessionId, sessionIds, or compositeSessionId is required for tachograph parity validation.");
        }
        result.forEach(this::requireFileSession);
        return List.copyOf(result);
    }

    private Set<String> resolveDriverKeys(RuntimeTachographParityValidationApiRequest request, List<UUID> sessionIds) {
        LinkedHashSet<String> result = new LinkedHashSet<>(request.requestedDriverKeys());
        if (request.includeAllDriversOrDefault() || result.isEmpty()) {
            for (UUID sessionId : sessionIds) {
                result.addAll(requireFileSession(sessionId).driversByKey().keySet());
            }
        }
        return Set.copyOf(result);
    }

    private TachographFileSession requireFileSession(UUID sessionId) {
        return fileSessionRepository.find(sessionId)
                .orElseThrow(() -> new TachographFileSessionNotFoundException(sessionId));
    }

    private ReferenceProjectionCounts referenceCounts(
            RuntimeTachographParityValidationApiRequest request,
            List<UUID> sessionIds,
            String driverKey
    ) {
        ProjectionCounts aggregate = ProjectionCounts.empty();
        int referenceSessionCount = 0;
        TachographEsperEventsProcessingRequest fileSessionRequest = new TachographEsperEventsProcessingRequest(
                request.occurredFrom(),
                request.occurredTo(),
                request.significantDrivingMinutes(),
                request.minimumRestPeriodMinutes()
        );
        for (UUID sessionId : sessionIds) {
            TachographFileSession session = requireFileSession(sessionId);
            if (!session.driversByKey().containsKey(driverKey)) {
                continue;
            }
            TachographEsperDriverProcessingResultDto projection = fileSessionProcessingService.getEsperDriverProcessingResults(
                    sessionId,
                    driverKey,
                    fileSessionRequest
            );
            aggregate = aggregate.plus(ProjectionCounts.from(projection));
            referenceSessionCount++;
        }
        String referenceMode = sessionIds.size() == 1 ? "SINGLE_FILE_SESSION" : "SUM_OF_FILE_SESSION_RESULTS";
        return new ReferenceProjectionCounts(aggregate, referenceMode, referenceSessionCount);
    }

    private RuntimeProjectionCounts runtimeCounts(RuntimeEventProcessingResultDto runtimeResult, String driverKey) {
        RuntimeEventProcessingPartitionResultDto partitionResult = runtimeResult.partitionResults().get(driverKey);
        if (partitionResult == null) {
            return new RuntimeProjectionCounts(ProjectionCounts.empty(), false);
        }
        Object result = partitionResult.result();
        if (result instanceof UnifiedRuntimeDerivedProjectionResultDto projectionResult) {
            return new RuntimeProjectionCounts(ProjectionCounts.from(projectionResult.projection()), projectionResult.projection() != null);
        }
        return new RuntimeProjectionCounts(ProjectionCounts.empty(), false);
    }

    private List<RuntimeTachographParityCategoryComparisonDto> comparisons(
            ProjectionCounts reference,
            ProjectionCounts runtime
    ) {
        return List.of(
                comparison("activityIntervals", reference.activityIntervalCount(), runtime.activityIntervalCount()),
                comparison("drivingIntervals", reference.drivingIntervalCount(), runtime.drivingIntervalCount()),
                comparison("drivingInterruptionIntervals", reference.drivingInterruptionIntervalCount(), runtime.drivingInterruptionIntervalCount()),
                comparison("drivingInterruptionVehicleChangeIntervals", reference.drivingInterruptionVehicleChangeIntervalCount(), runtime.drivingInterruptionVehicleChangeIntervalCount()),
                comparison("dailyWeeklyRestCandidateIntervals", reference.dailyWeeklyRestCandidateIntervalCount(), runtime.dailyWeeklyRestCandidateIntervalCount()),
                comparison("dailyWeeklyRestCandidateCoverageIntervals", reference.dailyWeeklyRestCandidateCoverageIntervalCount(), runtime.dailyWeeklyRestCandidateCoverageIntervalCount()),
                comparison("unclassifiedDailyWeeklyRestCandidateCoverageIntervals", reference.unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount(), runtime.unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount()),
                comparison("potentialHomeOvernightStayIntervals", reference.potentialHomeOvernightStayIntervalCount(), runtime.potentialHomeOvernightStayIntervalCount()),
                comparison("potentialInVehicleOvernightStayIntervals", reference.potentialInVehicleOvernightStayIntervalCount(), runtime.potentialInVehicleOvernightStayIntervalCount()),
                comparison("potentialInVehicleTripIntervals", reference.potentialInVehicleTripIntervalCount(), runtime.potentialInVehicleTripIntervalCount()),
                comparison("vehicleUsageIntervals", reference.vehicleUsageIntervalCount(), runtime.vehicleUsageIntervalCount()),
                comparison("vuCardAbsentIntervals", reference.vuCardAbsentIntervalCount(), runtime.vuCardAbsentIntervalCount()),
                comparison("supportGeoEvents", reference.supportGeoEventCount(), runtime.supportGeoEventCount())
        );
    }

    private RuntimeTachographParityCategoryComparisonDto comparison(String category, int fileSessionCount, int runtimeCount) {
        return new RuntimeTachographParityCategoryComparisonDto(
                category,
                fileSessionCount,
                runtimeCount,
                fileSessionCount == runtimeCount
        );
    }

    private record ReferenceProjectionCounts(
            ProjectionCounts counts,
            String referenceMode,
            int referenceSessionCount
    ) {
    }

    private record RuntimeProjectionCounts(
            ProjectionCounts counts,
            boolean present
    ) {
    }

    private record ProjectionCounts(
            int activityIntervalCount,
            int drivingIntervalCount,
            int drivingInterruptionIntervalCount,
            int drivingInterruptionVehicleChangeIntervalCount,
            int dailyWeeklyRestCandidateIntervalCount,
            int dailyWeeklyRestCandidateCoverageIntervalCount,
            int unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount,
            int potentialHomeOvernightStayIntervalCount,
            int potentialInVehicleOvernightStayIntervalCount,
            int potentialInVehicleTripIntervalCount,
            int vehicleUsageIntervalCount,
            int vuCardAbsentIntervalCount,
            int supportGeoEventCount
    ) {
        static ProjectionCounts empty() {
            return new ProjectionCounts(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
        }

        static ProjectionCounts from(TachographEsperDriverProcessingResultDto projection) {
            return projection == null ? empty() : from(projection.toDriverWorkingTime());
        }

        static ProjectionCounts from(DriverWorkingTimeProcessingResultDto projection) {
            if (projection == null) {
                return empty();
            }
            return new ProjectionCounts(
                    projection.activityIntervalCount(),
                    projection.drivingIntervalCount(),
                    projection.drivingInterruptionIntervalCount(),
                    projection.drivingInterruptionVehicleChangeIntervalCount(),
                    projection.dailyWeeklyRestCandidateIntervalCount(),
                    projection.dailyWeeklyRestCandidateCoverageIntervalCount(),
                    projection.unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount(),
                    projection.potentialHomeOvernightStayIntervalCount(),
                    projection.potentialInVehicleOvernightStayIntervalCount(),
                    projection.potentialInVehicleTripIntervalCount(),
                    projection.vehicleUsageIntervalCount(),
                    projection.vuCardAbsentIntervalCount(),
                    projection.supportGeoEventCount()
            );
        }

        ProjectionCounts plus(ProjectionCounts other) {
            return new ProjectionCounts(
                    activityIntervalCount + other.activityIntervalCount,
                    drivingIntervalCount + other.drivingIntervalCount,
                    drivingInterruptionIntervalCount + other.drivingInterruptionIntervalCount,
                    drivingInterruptionVehicleChangeIntervalCount + other.drivingInterruptionVehicleChangeIntervalCount,
                    dailyWeeklyRestCandidateIntervalCount + other.dailyWeeklyRestCandidateIntervalCount,
                    dailyWeeklyRestCandidateCoverageIntervalCount + other.dailyWeeklyRestCandidateCoverageIntervalCount,
                    unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount + other.unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount,
                    potentialHomeOvernightStayIntervalCount + other.potentialHomeOvernightStayIntervalCount,
                    potentialInVehicleOvernightStayIntervalCount + other.potentialInVehicleOvernightStayIntervalCount,
                    potentialInVehicleTripIntervalCount + other.potentialInVehicleTripIntervalCount,
                    vehicleUsageIntervalCount + other.vehicleUsageIntervalCount,
                    vuCardAbsentIntervalCount + other.vuCardAbsentIntervalCount,
                    supportGeoEventCount + other.supportGeoEventCount
            );
        }
    }
}
