package at.procon.eventhub.tachographfilesession.service;

import com.espertech.esper.common.client.EPCompiled;
import com.espertech.esper.common.client.EventBean;
import com.espertech.esper.common.client.configuration.Configuration;
import com.espertech.esper.compiler.client.CompilerArguments;
import com.espertech.esper.compiler.client.EPCompileException;
import com.espertech.esper.compiler.client.EPCompilerProvider;
import com.espertech.esper.runtime.client.EPDeployException;
import com.espertech.esper.runtime.client.EPDeployment;
import com.espertech.esper.runtime.client.EPRuntime;
import com.espertech.esper.runtime.client.EPRuntimeProvider;
import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession;
import at.procon.eventhub.tachographfilesession.model.ResolvedActivityInterval;
import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline;
import at.procon.eventhub.tachographfilesession.model.ResolvedVehicleUsageInterval;
import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingDerivedProjectionBundle;
import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import org.springframework.util.StreamUtils;

@Component
public class DriverTimelineReusableProjectionBuilder {

    private static final AtomicLong RUNTIME_COUNTER = new AtomicLong();
    private static final String DRIVING_DERIVED_PROJECTION_BUNDLE_EPL_TEMPLATE =
            loadResource("esper/tachograph-driving-derived-projection-bundle.epl");

    private final DriverTimelineBuilder driverTimelineBuilder;

    public DriverTimelineReusableProjectionBuilder(DriverTimelineBuilder driverTimelineBuilder) {
        this.driverTimelineBuilder = driverTimelineBuilder;
    }

    public TachographEsperDrivingDerivedProjectionBundle buildEsperDrivingDerivedProjectionBundle(
            TachographFileSession session,
            DriverExtractionSession driverSession,
            int significantDrivingMinutes,
            int minimumRestPeriodMinutes
    ) {
        ResolvedDriverTimeline timeline = driverTimelineBuilder.build(session, driverSession);
        return buildEsperDrivingDerivedProjectionBundle(
                session.sessionId(),
                driverSession.driverKey(),
                timeline,
                significantDrivingMinutes,
                minimumRestPeriodMinutes
        );
    }

    public TachographEsperDrivingDerivedProjectionBundle buildEsperDrivingDerivedProjectionBundle(
            UUID sessionId,
            String driverKey,
            ResolvedDriverTimeline timeline,
            int significantDrivingMinutes,
            int minimumRestPeriodMinutes
    ) {
        if (timeline == null) {
            return emptyBundle();
        }
        return buildEsperDrivingDerivedProjectionBundle(
                sessionId,
                driverKey,
                timeline.activityIntervals(),
                timeline.vehicleUsageIntervals(),
                significantDrivingMinutes,
                minimumRestPeriodMinutes
        );
    }

    private TachographEsperDrivingDerivedProjectionBundle buildEsperDrivingDerivedProjectionBundle(
            UUID sessionId,
            String driverKey,
            List<ResolvedActivityInterval> activityIntervals,
            List<ResolvedVehicleUsageInterval> vehicleUsageIntervals,
            int significantDrivingMinutes,
            int minimumRestPeriodMinutes
    ) {
        if ((activityIntervals == null || activityIntervals.isEmpty())
                && (vehicleUsageIntervals == null || vehicleUsageIntervals.isEmpty())) {
            return emptyBundle();
        }

        List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionIntervals = new ArrayList<>();
        List<TachographEsperDrivingInterruptionIntervalEvent> dailyWeeklyRestCandidateIntervals = new ArrayList<>();
        List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionVehicleChangeIntervals = new ArrayList<>();
        List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals = new ArrayList<>();
        List<TachographEsperPotentialHomeOvernightStayIntervalEvent> potentialHomeOvernightStayIntervals = new ArrayList<>();

        executeWithRuntime(
                configuration -> {
                    configuration.getCommon().addEventType(
                            "TachographActivityIntervalInputEvent",
                            activityIntervalInputDefinition()
                    );
                    configuration.getCommon().addEventType(
                            "TachographVehicleUsageIntervalInputEvent",
                            vehicleUsageIntervalInputDefinition()
                    );
                },
                renderDrivingDerivedProjectionBundleEpl(significantDrivingMinutes, minimumRestPeriodMinutes),
                Map.of(
                        "drivingInterruptionIntervals", newData -> collectDrivingInterruptionIntervalEvents(newData, drivingInterruptionIntervals),
                        "dailyWeeklyRestCandidateIntervals", newData -> collectDrivingInterruptionIntervalEvents(newData, dailyWeeklyRestCandidateIntervals),
                        "drivingInterruptionVehicleChangeIntervals", newData -> collectDrivingInterruptionIntervalEvents(newData, drivingInterruptionVehicleChangeIntervals),
                        "vuCardAbsentIntervals", newData -> collectVuCardAbsentIntervalEvents(newData, vuCardAbsentIntervals),
                        "potentialHomeOvernightStayIntervals", newData -> collectPotentialHomeOvernightStayIntervalEvents(newData, potentialHomeOvernightStayIntervals)
                ),
                runtime -> {
                    if (vehicleUsageIntervals != null) {
                        for (ResolvedVehicleUsageInterval interval : vehicleUsageIntervals) {
                            runtime.getEventService().sendEventMap(
                                    toVehicleUsageIntervalInputMap(interval),
                                    "TachographVehicleUsageIntervalInputEvent"
                            );
                        }
                    }
                    if (activityIntervals != null) {
                        for (ResolvedActivityInterval interval : activityIntervals) {
                            runtime.getEventService().sendEventMap(
                                    toActivityIntervalInputMap(sessionId, driverKey, interval),
                                    "TachographActivityIntervalInputEvent"
                            );
                        }
                    }
                }
        );

        return new TachographEsperDrivingDerivedProjectionBundle(
                sortDrivingInterruptionIntervals(drivingInterruptionIntervals),
                sortDrivingInterruptionIntervals(dailyWeeklyRestCandidateIntervals),
                sortDrivingInterruptionIntervals(drivingInterruptionVehicleChangeIntervals),
                sortVuCardAbsentIntervals(vuCardAbsentIntervals),
                sortPotentialHomeOvernightStayIntervals(potentialHomeOvernightStayIntervals)
        );
    }

    private TachographEsperDrivingDerivedProjectionBundle emptyBundle() {
        return new TachographEsperDrivingDerivedProjectionBundle(
                List.of(),
                List.of(),
                List.of(),
                List.of(),
                List.of()
        );
    }

    private void executeWithRuntime(
            Consumer<Configuration> configurationSetup,
            String epl,
            Map<String, Consumer<EventBean[]>> listeners,
            Consumer<EPRuntime> sender
    ) {
        EPRuntime runtime = null;
        try {
            Configuration configuration = new Configuration();
            configurationSetup.accept(configuration);
            String runtimeUri = "eventhub-tachograph-reusable-projection-" + RUNTIME_COUNTER.incrementAndGet();
            runtime = EPRuntimeProvider.getRuntime(runtimeUri, configuration);

            CompilerArguments arguments = new CompilerArguments(configuration);
            EPCompiled compiled = EPCompilerProvider.getCompiler().compile(epl, arguments);
            EPDeployment deployment = runtime.getDeploymentService().deploy(compiled);
            for (Map.Entry<String, Consumer<EventBean[]>> entry : listeners.entrySet()) {
                runtime.getDeploymentService()
                        .getStatement(deployment.getDeploymentId(), entry.getKey())
                        .addListener((newData, oldData, statement, rt) -> entry.getValue().accept(newData));
            }

            sender.accept(runtime);
        } catch (EPCompileException | EPDeployException e) {
            throw new IllegalStateException("Cannot compile/deploy reusable tachograph projection EPL bundle", e);
        } finally {
            if (runtime != null) {
                runtime.destroy();
            }
        }
    }

    private Map<String, Object> activityIntervalInputDefinition() {
        Map<String, Object> definition = new LinkedHashMap<>();
        definition.put("sessionId", UUID.class);
        definition.put("driverKey", String.class);
        definition.put("intervalId", String.class);
        definition.put("activityType", String.class);
        definition.put("cardSlot", String.class);
        definition.put("cardStatus", String.class);
        definition.put("drivingStatus", String.class);
        definition.put("registrationKey", String.class);
        definition.put("vehicleKey", String.class);
        definition.put("sourceKind", String.class);
        definition.put("firstSourceIntervalId", String.class);
        definition.put("lastSourceIntervalId", String.class);
        definition.put("startedAt", OffsetDateTime.class);
        definition.put("endedAt", OffsetDateTime.class);
        definition.put("startedAtEpochSecond", long.class);
        definition.put("endedAtEpochSecond", long.class);
        definition.put("durationSeconds", long.class);
        definition.put("sourceIntervalIds", java.util.List.class);
        definition.put("synthetic", boolean.class);
        definition.put("clippedToRequestedPeriod", boolean.class);
        definition.put("level", String.class);
        return definition;
    }

    private Map<String, Object> vehicleUsageIntervalInputDefinition() {
        Map<String, Object> definition = new LinkedHashMap<>();
        definition.put("sessionId", UUID.class);
        definition.put("driverKey", String.class);
        definition.put("intervalId", String.class);
        definition.put("firstSourceIntervalId", String.class);
        definition.put("lastSourceIntervalId", String.class);
        definition.put("startedAt", OffsetDateTime.class);
        definition.put("endedAt", OffsetDateTime.class);
        definition.put("startedAtEpochSecond", long.class);
        definition.put("endedAtEpochSecond", Long.class);
        definition.put("durationSeconds", long.class);
        definition.put("odometerBeginKm", Long.class);
        definition.put("odometerEndKm", Long.class);
        definition.put("registrationKey", String.class);
        definition.put("vehicleKey", String.class);
        definition.put("sourceKind", String.class);
        definition.put("sourceIntervalIds", java.util.List.class);
        return definition;
    }

    private Map<String, Object> toActivityIntervalInputMap(
            UUID sessionId,
            String driverKey,
            ResolvedActivityInterval interval
    ) {
        Map<String, Object> event = new LinkedHashMap<>();
        event.put("sessionId", sessionId);
        event.put("driverKey", driverKey);
        event.put("intervalId", interval.intervalId());
        event.put("activityType", interval.activityType());
        event.put("cardSlot", interval.slot());
        event.put("cardStatus", interval.cardStatus());
        event.put("drivingStatus", interval.drivingStatus());
        event.put("registrationKey", interval.registrationKey());
        event.put("vehicleKey", interval.vehicleKey());
        event.put("sourceKind", interval.sourceKind());
        event.put("firstSourceIntervalId", firstSourceIntervalId(interval));
        event.put("lastSourceIntervalId", lastSourceIntervalId(interval));
        event.put("startedAt", interval.from());
        event.put("endedAt", interval.to());
        event.put("startedAtEpochSecond", interval.from().toEpochSecond());
        event.put("endedAtEpochSecond", interval.to().toEpochSecond());
        event.put("durationSeconds", interval.durationSeconds());
        event.put("sourceIntervalIds", interval.sourceIntervalIds());
        event.put("synthetic", interval.synthetic());
        event.put("clippedToRequestedPeriod", interval.clippedToRequestedPeriod());
        event.put("level", interval.level());
        return event;
    }

    private Map<String, Object> toVehicleUsageIntervalInputMap(ResolvedVehicleUsageInterval interval) {
        Map<String, Object> event = new LinkedHashMap<>();
        event.put("sessionId", interval.sessionId());
        event.put("driverKey", interval.driverKey());
        event.put("intervalId", interval.intervalId());
        event.put("firstSourceIntervalId", firstSourceIntervalId(interval));
        event.put("lastSourceIntervalId", lastSourceIntervalId(interval));
        event.put("startedAt", interval.from());
        event.put("endedAt", interval.to());
        event.put("startedAtEpochSecond", interval.from().toEpochSecond());
        event.put("endedAtEpochSecond", interval.to() == null ? null : interval.to().toEpochSecond());
        event.put("durationSeconds", interval.durationSeconds());
        event.put("odometerBeginKm", interval.odometerBeginKm());
        event.put("odometerEndKm", interval.odometerEndKm());
        event.put("registrationKey", interval.registrationKey());
        event.put("vehicleKey", interval.vehicleKey());
        event.put("sourceKind", interval.sourceKind());
        event.put("sourceIntervalIds", interval.sourceIntervalIds());
        return event;
    }

    private String firstSourceIntervalId(ResolvedActivityInterval interval) {
        return interval.sourceIntervalIds().isEmpty() ? interval.intervalId() : interval.sourceIntervalIds().get(0);
    }

    private String lastSourceIntervalId(ResolvedActivityInterval interval) {
        return interval.sourceIntervalIds().isEmpty()
                ? interval.intervalId()
                : interval.sourceIntervalIds().get(interval.sourceIntervalIds().size() - 1);
    }

    private String firstSourceIntervalId(ResolvedVehicleUsageInterval interval) {
        return interval.sourceIntervalIds().isEmpty() ? interval.intervalId() : interval.sourceIntervalIds().get(0);
    }

    private String lastSourceIntervalId(ResolvedVehicleUsageInterval interval) {
        return interval.sourceIntervalIds().isEmpty()
                ? interval.intervalId()
                : interval.sourceIntervalIds().get(interval.sourceIntervalIds().size() - 1);
    }

    private void collectDrivingInterruptionIntervalEvents(
            EventBean[] newData,
            List<TachographEsperDrivingInterruptionIntervalEvent> target
    ) {
        if (newData == null) {
            return;
        }
        for (EventBean event : newData) {
            long startedAtEpochSecond = (Long) event.get("startedAtEpochSecond");
            long endedAtEpochSecond = (Long) event.get("endedAtEpochSecond");
            target.add(new TachographEsperDrivingInterruptionIntervalEvent(
                    (UUID) event.get("sessionId"),
                    (String) event.get("driverKey"),
                    OffsetDateTime.ofInstant(Instant.ofEpochSecond(startedAtEpochSecond), ZoneOffset.UTC),
                    OffsetDateTime.ofInstant(Instant.ofEpochSecond(endedAtEpochSecond), ZoneOffset.UTC),
                    (Long) event.get("durationSeconds"),
                    (String) event.get("previousDrivingSourceIntervalId"),
                    (String) event.get("nextDrivingSourceIntervalId"),
                    (String) event.get("previousRegistrationKey"),
                    (String) event.get("nextRegistrationKey"),
                    (String) event.get("previousVehicleKey"),
                    (String) event.get("nextVehicleKey")
            ));
        }
    }

    private void collectVuCardAbsentIntervalEvents(
            EventBean[] newData,
            List<TachographEsperVuCardAbsentIntervalEvent> target
    ) {
        if (newData == null) {
            return;
        }
        for (EventBean event : newData) {
            long startedAtEpochSecond = (Long) event.get("startedAtEpochSecond");
            long endedAtEpochSecond = (Long) event.get("endedAtEpochSecond");
            target.add(new TachographEsperVuCardAbsentIntervalEvent(
                    (UUID) event.get("sessionId"),
                    (String) event.get("driverKey"),
                    OffsetDateTime.ofInstant(Instant.ofEpochSecond(startedAtEpochSecond), ZoneOffset.UTC),
                    OffsetDateTime.ofInstant(Instant.ofEpochSecond(endedAtEpochSecond), ZoneOffset.UTC),
                    (Long) event.get("durationSeconds"),
                    (String) event.get("previousUsageIntervalId"),
                    (String) event.get("nextUsageIntervalId"),
                    (String) event.get("previousRegistrationKey"),
                    (String) event.get("nextRegistrationKey"),
                    (String) event.get("previousVehicleKey"),
                    (String) event.get("nextVehicleKey")
            ));
        }
    }

    private void collectPotentialHomeOvernightStayIntervalEvents(
            EventBean[] newData,
            List<TachographEsperPotentialHomeOvernightStayIntervalEvent> target
    ) {
        if (newData == null) {
            return;
        }
        for (EventBean event : newData) {
            long startedAtEpochSecond = (Long) event.get("startedAtEpochSecond");
            long endedAtEpochSecond = (Long) event.get("endedAtEpochSecond");
            target.add(new TachographEsperPotentialHomeOvernightStayIntervalEvent(
                    (UUID) event.get("sessionId"),
                    (String) event.get("driverKey"),
                    OffsetDateTime.ofInstant(Instant.ofEpochSecond(startedAtEpochSecond), ZoneOffset.UTC),
                    OffsetDateTime.ofInstant(Instant.ofEpochSecond(endedAtEpochSecond), ZoneOffset.UTC),
                    (Long) event.get("durationSeconds"),
                    (Long) event.get("unknownDurationSeconds"),
                    (Double) event.get("unknownCoveragePercent"),
                    (String) event.get("previousDrivingSourceIntervalId"),
                    (String) event.get("nextDrivingSourceIntervalId"),
                    (String) event.get("previousRegistrationKey"),
                    (String) event.get("nextRegistrationKey"),
                    (String) event.get("previousVehicleKey"),
                    (String) event.get("nextVehicleKey")
            ));
        }
    }

    private List<TachographEsperDrivingInterruptionIntervalEvent> sortDrivingInterruptionIntervals(
            List<TachographEsperDrivingInterruptionIntervalEvent> intervals
    ) {
        return intervals.stream()
                .sorted(Comparator.comparing(TachographEsperDrivingInterruptionIntervalEvent::startedAt)
                        .thenComparing(TachographEsperDrivingInterruptionIntervalEvent::endedAt))
                .toList();
    }

    private List<TachographEsperVuCardAbsentIntervalEvent> sortVuCardAbsentIntervals(
            List<TachographEsperVuCardAbsentIntervalEvent> intervals
    ) {
        return intervals.stream()
                .sorted(Comparator.comparing(TachographEsperVuCardAbsentIntervalEvent::startedAt)
                        .thenComparing(TachographEsperVuCardAbsentIntervalEvent::endedAt))
                .toList();
    }

    private List<TachographEsperPotentialHomeOvernightStayIntervalEvent> sortPotentialHomeOvernightStayIntervals(
            List<TachographEsperPotentialHomeOvernightStayIntervalEvent> intervals
    ) {
        return intervals.stream()
                .sorted(Comparator.comparing(TachographEsperPotentialHomeOvernightStayIntervalEvent::startedAt)
                        .thenComparing(TachographEsperPotentialHomeOvernightStayIntervalEvent::endedAt))
                .toList();
    }

    private String renderDrivingDerivedProjectionBundleEpl(int significantDrivingMinutes, int minimumRestPeriodMinutes) {
        return DRIVING_DERIVED_PROJECTION_BUNDLE_EPL_TEMPLATE
                .replace(
                        "${SIGNIFICANT_DRIVING_THRESHOLD_SECONDS}",
                        Long.toString(Math.max(1, significantDrivingMinutes) * 60L)
                )
                .replace(
                        "${MINIMUM_REST_PERIOD_THRESHOLD_SECONDS}",
                        Long.toString(Math.max(1, minimumRestPeriodMinutes) * 60L)
                );
    }

    private static String loadResource(String path) {
        try {
            ClassPathResource resource = new ClassPathResource(path);
            return StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8);
        } catch (IOException e) {
            throw new IllegalStateException("Cannot load EPL resource: " + path, e);
        }
    }
}
