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

import at.procon.eventhub.dto.EventDomain;
import at.procon.eventhub.dto.EventHubEventDto;
import at.procon.eventhub.dto.EventLifecycle;
import at.procon.eventhub.dto.EventType;
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.model.UnifiedRuntimeEventBundle;
import at.procon.eventhub.processing.support.TachographRuntimeIdentityResolver;
import com.fasterxml.jackson.databind.JsonNode;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;

public final class DriverWorkingTimeEplEventMapper {

    public static final String DRIVER_ACTIVITY_POINT_EVENT = "DriverActivityPointEvent";
    public static final String DRIVER_ACTIVITY_INTERVAL_EVENT = "DriverActivityIntervalEvent";
    public static final String DRIVER_VEHICLE_USAGE_POINT_EVENT = "DriverVehicleUsagePointEvent";
    public static final String DRIVER_VEHICLE_USAGE_INTERVAL_EVENT = "DriverVehicleUsageIntervalEvent";

    private DriverWorkingTimeEplEventMapper() {
    }

    public static List<EventHubEventDto> sourceEvents(RuntimeProcessingModuleContext context) {
        RuntimeProcessingModuleResult assemblyResult = context.previousResults().get(DriverWorkingTimeModuleKeys.RUNTIME_EVENT_ASSEMBLY);
        if (assemblyResult != null && assemblyResult.output() instanceof UnifiedRuntimeEventBundle bundle) {
            return safeList(bundle.mergedEvents());
        }
        return safeList(context.events());
    }

    public static List<Map<String, Object>> activityPointEvents(List<EventHubEventDto> sourceEvents) {
        return safeList(sourceEvents).stream()
                .map(DriverWorkingTimeEplEventMapper::toActivityPointEvent)
                .filter(Objects::nonNull)
                .sorted(pointEventComparator())
                .toList();
    }

    public static List<Map<String, Object>> vehicleUsagePointEvents(List<EventHubEventDto> sourceEvents) {
        return safeList(sourceEvents).stream()
                .map(DriverWorkingTimeEplEventMapper::toVehicleUsagePointEvent)
                .filter(Objects::nonNull)
                .sorted(pointEventComparator())
                .toList();
    }

    public static Map<String, Object> activityPointEventDefinition() {
        Map<String, Object> definition = new LinkedHashMap<>();
        definition.put("sessionId", UUID.class);
        definition.put("driverKey", String.class);
        definition.put("eventId", String.class);
        definition.put("intervalId", String.class);
        definition.put("sourceRowId", String.class);
        definition.put("sourceRowIds", java.util.List.class);
        definition.put("activityType", String.class);
        definition.put("lifecycle", String.class);
        definition.put("occurredAt", OffsetDateTime.class);
        definition.put("occurredAtEpochSecond", long.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("synthetic", boolean.class);
        definition.put("clippedToRequestedPeriod", boolean.class);
        definition.put("level", String.class);
        return definition;
    }

    public static Map<String, Object> activityIntervalEventDefinition() {
        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;
    }

    public static Map<String, Object> vehicleUsagePointEventDefinition() {
        Map<String, Object> definition = new LinkedHashMap<>();
        definition.put("sessionId", UUID.class);
        definition.put("driverKey", String.class);
        definition.put("eventId", String.class);
        definition.put("intervalId", String.class);
        definition.put("sourceRowId", String.class);
        definition.put("sourceRowIds", java.util.List.class);
        definition.put("lifecycle", String.class);
        definition.put("occurredAt", OffsetDateTime.class);
        definition.put("occurredAtEpochSecond", long.class);
        definition.put("registrationKey", String.class);
        definition.put("vehicleKey", String.class);
        definition.put("sourceKind", String.class);
        definition.put("odometerKm", Long.class);
        return definition;
    }

    public static Map<String, Object> vehicleUsageIntervalEventDefinition() {
        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 static Map<String, Object> toActivityPointEvent(EventHubEventDto sourceEvent) {
        if (sourceEvent == null
                || sourceEvent.eventDomain() != EventDomain.DRIVER_ACTIVITY
                || (sourceEvent.lifecycle() != EventLifecycle.START && sourceEvent.lifecycle() != EventLifecycle.END)
                || sourceEvent.occurredAt() == null) {
            return null;
        }
        JsonNode raw = rawPayload(sourceEvent);
        JsonNode attributes = attributes(sourceEvent);
        String intervalId = firstNonBlank(text(raw, "intervalId"), text(raw, "sourceRowId"), sourceEvent.externalSourceEventId());
        String driverKey = TachographRuntimeIdentityResolver.driverKey(sourceEvent);
        if (driverKey == null || intervalId == null) {
            return null;
        }
        Map<String, Object> event = new LinkedHashMap<>();
        event.put("sessionId", sessionId(raw, sourceEvent));
        event.put("driverKey", driverKey);
        event.put("eventId", sourceEvent.externalSourceEventId());
        event.put("intervalId", intervalId);
        event.put("sourceRowId", firstNonBlank(text(raw, "sourceRowId"), intervalId));
        event.put("sourceRowIds", stringList(raw, "sourceRowIds", intervalId));
        event.put("activityType", firstNonBlank(text(raw, "activityType"), eventTypeAsActivity(sourceEvent.eventType())));
        event.put("lifecycle", sourceEvent.lifecycle().name());
        event.put("occurredAt", sourceEvent.occurredAt());
        event.put("occurredAtEpochSecond", sourceEvent.occurredAt().toEpochSecond());
        event.put("cardSlot", firstNonBlank(text(raw, "cardSlot"), text(attributes, "cardSlot")));
        event.put("cardStatus", firstNonBlank(text(raw, "cardStatus"), text(attributes, "cardStatus")));
        event.put("drivingStatus", firstNonBlank(text(raw, "drivingStatus"), text(attributes, "drivingStatus")));
        event.put("registrationKey", TachographRuntimeIdentityResolver.registrationKey(sourceEvent));
        event.put("vehicleKey", TachographRuntimeIdentityResolver.vehicleKey(sourceEvent));
        event.put("sourceKind", firstNonBlank(text(raw, "sourceKind"), sourceKind(sourceEvent)));
        event.put("synthetic", booleanValue(raw, "synthetic", false));
        event.put("clippedToRequestedPeriod", booleanValue(raw, "clippedToRequestedPeriod", false));
        event.put("level", firstNonBlank(text(raw, "level"), "RAW_EVENT"));
        return event;
    }

    private static Map<String, Object> toVehicleUsagePointEvent(EventHubEventDto sourceEvent) {
        if (sourceEvent == null
                || sourceEvent.eventDomain() != EventDomain.DRIVER_CARD
                || (sourceEvent.lifecycle() != EventLifecycle.INSERT && sourceEvent.lifecycle() != EventLifecycle.WITHDRAW)
                || sourceEvent.occurredAt() == null) {
            return null;
        }
        boolean supportedType = sourceEvent.eventType() == EventType.CARD_INSERTED
                || sourceEvent.eventType() == EventType.CARD_WITHDRAWN;
        if (!supportedType) {
            return null;
        }
        JsonNode raw = rawPayload(sourceEvent);
        String intervalId = firstNonBlank(text(raw, "intervalId"), text(raw, "sourceRowId"), sourceEvent.externalSourceEventId());
        String driverKey = TachographRuntimeIdentityResolver.driverKey(sourceEvent);
        if (driverKey == null || intervalId == null) {
            return null;
        }
        Map<String, Object> event = new LinkedHashMap<>();
        event.put("sessionId", sessionId(raw, sourceEvent));
        event.put("driverKey", driverKey);
        event.put("eventId", sourceEvent.externalSourceEventId());
        event.put("intervalId", intervalId);
        event.put("sourceRowId", firstNonBlank(text(raw, "sourceRowId"), intervalId));
        event.put("sourceRowIds", stringList(raw, "sourceRowIds", intervalId));
        event.put("lifecycle", sourceEvent.lifecycle().name());
        event.put("occurredAt", sourceEvent.occurredAt());
        event.put("occurredAtEpochSecond", sourceEvent.occurredAt().toEpochSecond());
        event.put("registrationKey", TachographRuntimeIdentityResolver.registrationKey(sourceEvent));
        event.put("vehicleKey", TachographRuntimeIdentityResolver.vehicleKey(sourceEvent));
        event.put("sourceKind", firstNonBlank(text(raw, "sourceKind"), sourceKind(sourceEvent)));
        event.put("odometerKm", odometerKm(sourceEvent, raw));
        return event;
    }

    private static Comparator<Map<String, Object>> pointEventComparator() {
        return Comparator
                .comparing((Map<String, Object> event) -> (Long) event.get("occurredAtEpochSecond"))
                .thenComparing(event -> lifecycleOrder(Objects.toString(event.get("lifecycle"), "")))
                .thenComparing(event -> Objects.toString(event.get("driverKey"), ""))
                .thenComparing(event -> Objects.toString(event.get("intervalId"), ""))
                .thenComparing(event -> Objects.toString(event.get("eventId"), ""));
    }

    private static int lifecycleOrder(String lifecycle) {
        return switch (lifecycle) {
            case "INSERT", "START" -> 0;
            case "WITHDRAW", "END" -> 1;
            default -> 2;
        };
    }

    private static 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 static JsonNode attributes(EventHubEventDto event) {
        return event.eventDetails() == null ? null : event.eventDetails().attributes();
    }

    private static 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;
    }

    private static boolean booleanValue(JsonNode node, String field, boolean fallback) {
        if (node == null || field == null || node.get(field) == null || node.get(field).isNull()) {
            return fallback;
        }
        return node.get(field).asBoolean(fallback);
    }

    private static Long longValue(JsonNode node, String field) {
        if (node == null || field == null || node.get(field) == null || node.get(field).isNull()) {
            return null;
        }
        JsonNode value = node.get(field);
        if (value.isNumber()) {
            return value.asLong();
        }
        try {
            return Long.parseLong(value.asText());
        } catch (NumberFormatException ignored) {
            return null;
        }
    }

    private static List<String> stringList(JsonNode node, String field, String fallback) {
        JsonNode value = node == null || field == null ? null : node.get(field);
        if (value == null || value.isNull()) {
            return fallback == null ? List.of() : List.of(fallback);
        }
        if (value.isArray()) {
            List<String> result = new ArrayList<>();
            value.forEach(item -> {
                if (item != null && !item.isNull()) {
                    String text = item.asText(null);
                    if (text != null && !text.isBlank()) {
                        result.add(text);
                    }
                }
            });
            return result.isEmpty() && fallback != null ? List.of(fallback) : List.copyOf(result);
        }
        String text = value.asText(null);
        return text == null || text.isBlank() ? (fallback == null ? List.of() : List.of(fallback)) : List.of(text);
    }

    private static String firstNonBlank(String... values) {
        if (values == null) {
            return null;
        }
        for (String value : values) {
            if (value != null && !value.isBlank()) {
                return value.trim();
            }
        }
        return null;
    }

    private static String eventTypeAsActivity(EventType eventType) {
        if (eventType == null) {
            return "UNKNOWN";
        }
        return switch (eventType) {
            case DRIVE -> "DRIVE";
            case WORK -> "WORK";
            case AVAILABILITY -> "AVAILABILITY";
            case BREAK_REST -> "BREAK_REST";
            default -> eventType.name();
        };
    }

    private static UUID sessionId(JsonNode raw, EventHubEventDto event) {
        String rawSessionId = firstNonBlank(
                text(raw, "sessionId"),
                event.sourcePackageRef() == null ? null : event.sourcePackageRef().sourcePackageId()
        );
        if (rawSessionId != null) {
            try {
                return UUID.fromString(rawSessionId);
            } catch (IllegalArgumentException ignored) {
                // DB-acquired source packages need not be UUID-based file sessions.
            }
        }
        return new UUID(0L, 0L);
    }

    private static String sourceKind(EventHubEventDto event) {
        return event.packageInfo() == null || event.packageInfo().eventSource() == null
                ? null
                : event.packageInfo().eventSource().sourceKind();
    }

    private static Long odometerKm(EventHubEventDto event, JsonNode raw) {
        Long explicit = longValue(raw, event.lifecycle() == EventLifecycle.WITHDRAW ? "odometerEndKm" : "odometerBeginKm");
        if (explicit != null) {
            return explicit;
        }
        explicit = longValue(raw, "odometerKm");
        if (explicit != null) {
            return explicit;
        }
        return event.odometerM() == null ? null : event.odometerM() / 1_000L;
    }

    private static <T> List<T> safeList(List<T> values) {
        return values == null ? List.of() : values;
    }
}
