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

import at.procon.eventhub.dto.DriverRefDto;
import at.procon.eventhub.dto.EventDetailsDto;
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.dto.GeoPointDto;
import at.procon.eventhub.dto.VehicleRefDto;
import at.procon.eventhub.processing.support.TachographRuntimeIdentityResolver;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Service;

@Service
public class RuntimeSupportEvidenceNormalizer {

    private static final LinkedHashSet<EventDomain> DIRECT_TACHOGRAPH_SUPPORT_DOMAINS = new LinkedHashSet<>(List.of(
            EventDomain.POSITION,
            EventDomain.PLACE,
            EventDomain.BORDER_CROSSING,
            EventDomain.LOAD_UNLOAD
    ));

    private final ObjectMapper objectMapper;

    public RuntimeSupportEvidenceNormalizer(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    public RuntimeSupportEvidenceNormalizationResult normalizeForTachographDriver(
            String driverKey,
            List<EventHubEventDto> events
    ) {
        List<EventHubEventDto> safeEvents = events == null ? List.of() : List.copyOf(events);
        List<EventHubEventDto> normalizedEvents = new ArrayList<>(safeEvents.size());
        int normalizedSupportEvidence = 0;
        int unchanged = 0;
        for (EventHubEventDto event : safeEvents) {
            EventHubEventDto normalized = normalizeOneForTachographDriver(driverKey, event);
            normalizedEvents.add(normalized);
            if (normalized != event) {
                normalizedSupportEvidence++;
            } else {
                unchanged++;
            }
        }
        List<String> notes = new ArrayList<>();
        notes.add("Runtime support evidence normalization inspected " + safeEvents.size() + " event(s).");
        notes.add("Runtime support evidence normalization adapted " + normalizedSupportEvidence
                + " support/vehicle event(s) for the tachograph Esper profile.");
        return new RuntimeSupportEvidenceNormalizationResult(
                normalizedEvents,
                safeEvents.size(),
                normalizedSupportEvidence,
                unchanged,
                notes
        );
    }

    public RuntimeSupportEvidenceEvent toSupportEvidenceEvent(String fallbackDriverKey, EventHubEventDto event) {
        if (event == null || isDriverActivityOrCardUsage(event)) {
            return null;
        }
        JsonNode raw = rawPayload(event);
        GeoPointDto position = event.position();
        BigDecimal latitude = position == null ? decimal(raw, "latitude") : position.latitude();
        BigDecimal longitude = position == null ? decimal(raw, "longitude") : position.longitude();
        Long odometerKm = firstNonNull(longValue(raw, "odometerKm"), toKilometers(event.odometerM()));
        return new RuntimeSupportEvidenceEvent(
                firstNonBlank(text(raw, "supportEventId"), text(raw, "sourceRowId"), event.externalSourceEventId()),
                sourceFamily(event),
                firstNonBlank(text(raw, "sourceKind"), sourceKind(event)),
                event.eventDomain() == null ? null : event.eventDomain().name(),
                event.eventType() == null ? null : event.eventType().name(),
                event.lifecycle() == null ? null : event.lifecycle().name(),
                firstNonBlank(TachographRuntimeIdentityResolver.driverKey(event), fallbackDriverKey),
                TachographRuntimeIdentityResolver.vehicleKey(event),
                TachographRuntimeIdentityResolver.registrationKey(event),
                event.occurredAt(),
                event.occurredAt() == null ? null : event.occurredAt().toEpochSecond(),
                latitude,
                longitude,
                firstNonBlank(text(raw, "country"), detailText(event, "country")),
                firstNonBlank(text(raw, "region"), detailText(event, "region")),
                firstNonBlank(text(raw, "countryFrom"), detailText(event, "countryFrom")),
                firstNonBlank(text(raw, "countryTo"), detailText(event, "countryTo")),
                firstNonBlank(text(raw, "operation"), detailText(event, "operation")),
                odometerKm,
                decimal(raw, "avgSpeedKmh"),
                decimal(raw, "maxSpeedKmh"),
                rawAttributes(event, raw)
        );
    }

    private EventHubEventDto normalizeOneForTachographDriver(String fallbackDriverKey, EventHubEventDto event) {
        if (event == null || isDriverActivityOrCardUsage(event)) {
            return event;
        }
        RuntimeSupportEvidenceEvent support = toSupportEvidenceEvent(fallbackDriverKey, event);
        if (support == null || !hasGeoOrOdometerEvidence(support)) {
            return event;
        }
        EventDomain normalizedDomain = normalizedDomain(event);
        EventType normalizedType = normalizedType(normalizedDomain, event.eventType());
        EventLifecycle normalizedLifecycle = normalizedLifecycle(normalizedDomain, event.lifecycle());
        JsonNode payload = normalizedPayload(fallbackDriverKey, event, support, normalizedDomain);
        EventDetailsDto details = normalizedDetails(event, support, normalizedDomain);
        DriverRefDto driverRef = event.driverRef();
        if ((driverRef == null || !driverRef.hasAnyReference()) && support.driverKey() != null) {
            driverRef = new DriverRefDto(support.driverKey(), null);
        }
        return new EventHubEventDto(
                event.eventId(),
                event.externalSourceEventId(),
                driverRef,
                event.vehicleRef(),
                event.occurredAt(),
                event.receivedPartnerAt(),
                event.receivedHubAt(),
                normalizedDomain,
                normalizedType,
                normalizedLifecycle,
                event.odometerM(),
                normalizedPosition(event, support),
                details,
                event.sourcePackageRef(),
                payload,
                event.manualEntry(),
                event.packageInfo()
        );
    }

    private GeoPointDto normalizedPosition(EventHubEventDto original, RuntimeSupportEvidenceEvent support) {
        if (original != null && original.position() != null) {
            return original.position();
        }
        if (support == null || support.latitude() == null || support.longitude() == null) {
            return null;
        }
        return new GeoPointDto(support.latitude(), support.longitude());
    }

    private boolean isDriverActivityOrCardUsage(EventHubEventDto event) {
        if (event == null) {
            return true;
        }
        if (event.eventDomain() == EventDomain.DRIVER_ACTIVITY) {
            return true;
        }
        return event.eventDomain() == EventDomain.DRIVER_CARD
                && (event.eventType() == EventType.CARD_INSERTED || event.eventType() == EventType.CARD_WITHDRAWN);
    }

    private boolean hasGeoOrOdometerEvidence(RuntimeSupportEvidenceEvent support) {
        return support != null
                && ((support.latitude() != null && support.longitude() != null) || support.odometerKm() != null);
    }

    private EventDomain normalizedDomain(EventHubEventDto event) {
        if (event.eventDomain() != null && DIRECT_TACHOGRAPH_SUPPORT_DOMAINS.contains(event.eventDomain())) {
            return event.eventDomain();
        }
        return EventDomain.POSITION;
    }

    private EventType normalizedType(EventDomain normalizedDomain, EventType originalType) {
        if (normalizedDomain == EventDomain.POSITION) {
            return EventType.POSITION_RECORDED;
        }
        if (normalizedDomain == EventDomain.PLACE) {
            return EventType.WORKING_DAY_PLACE_RECORDED;
        }
        if (normalizedDomain == EventDomain.BORDER_CROSSING) {
            return originalType == null ? EventType.BORDER_INBOUND : originalType;
        }
        if (normalizedDomain == EventDomain.LOAD_UNLOAD) {
            return originalType == null ? EventType.LOAD_UNLOAD : originalType;
        }
        return originalType == null ? EventType.UNKNOWN_EVENT : originalType;
    }

    private EventLifecycle normalizedLifecycle(EventDomain normalizedDomain, EventLifecycle originalLifecycle) {
        if (normalizedDomain == EventDomain.POSITION || normalizedDomain == EventDomain.PLACE) {
            return EventLifecycle.SNAPSHOT;
        }
        return originalLifecycle == null ? EventLifecycle.SNAPSHOT : originalLifecycle;
    }

    private EventDetailsDto normalizedDetails(
            EventHubEventDto original,
            RuntimeSupportEvidenceEvent support,
            EventDomain normalizedDomain
    ) {
        Map<String, Object> attributes = new LinkedHashMap<>();
        put(attributes, "normalizedSupportEvidence", true);
        put(attributes, "normalizedForProfile", "tachograph-driver-esper-v1");
        put(attributes, "originalEventDomain", support.eventDomain());
        put(attributes, "originalEventType", support.eventType());
        put(attributes, "originalLifecycle", support.lifecycle());
        put(attributes, "sourceFamily", support.sourceFamily());
        put(attributes, "sourceKind", support.sourceKind());
        put(attributes, "country", support.countryCode());
        put(attributes, "region", support.regionCode());
        put(attributes, "countryFrom", support.countryFrom());
        put(attributes, "countryTo", support.countryTo());
        put(attributes, "operation", support.operation());
        if (original.eventDetails() != null && original.eventDetails().attributes() != null) {
            attributes.put("originalAttributes", original.eventDetails().attributes());
        }
        String type = normalizedDomain == EventDomain.POSITION ? "POSITION" : normalizedDomain.name();
        return new EventDetailsDto(type, objectMapper.valueToTree(attributes));
    }

    private JsonNode normalizedPayload(
            String fallbackDriverKey,
            EventHubEventDto original,
            RuntimeSupportEvidenceEvent support,
            EventDomain normalizedDomain
    ) {
        ObjectNode root = objectMapper.createObjectNode();
        ObjectNode raw = root.putObject("raw");
        JsonNode originalRaw = rawPayload(original);
        if (originalRaw != null && originalRaw.isObject()) {
            originalRaw.fields().forEachRemaining(entry -> raw.set(entry.getKey(), entry.getValue()));
        }
        put(raw, "normalizedSupportEvidence", true);
        put(raw, "normalizedForProfile", "tachograph-driver-esper-v1");
        put(raw, "supportEventId", support.eventId());
        put(raw, "supportEventDomain", support.eventDomain());
        put(raw, "supportEventType", support.eventType());
        put(raw, "supportEventLifecycle", support.lifecycle());
        put(raw, "originalEventDomain", support.eventDomain());
        put(raw, "originalEventType", support.eventType());
        put(raw, "originalLifecycle", support.lifecycle());
        put(raw, "normalizedEventDomain", normalizedDomain.name());
        put(raw, "driverKey", firstNonBlank(support.driverKey(), fallbackDriverKey));
        put(raw, "vehicleKey", support.vehicleKey());
        put(raw, "registrationKey", support.registrationKey());
        put(raw, "sourceFamily", support.sourceFamily());
        put(raw, "sourceKind", support.sourceKind());
        put(raw, "country", support.countryCode());
        put(raw, "region", support.regionCode());
        put(raw, "countryFrom", support.countryFrom());
        put(raw, "countryTo", support.countryTo());
        put(raw, "operation", support.operation());
        put(raw, "odometerKm", support.odometerKm());
        put(raw, "avgSpeedKmh", support.speedKmh());
        put(raw, "maxSpeedKmh", support.maxSpeedKmh());
        put(raw, "latitude", support.latitude());
        put(raw, "longitude", support.longitude());
        put(raw, "rawRecordPath", firstNonBlank(text(originalRaw, "rawRecordPath"), original.externalSourceEventId()));
        return root;
    }

    private Map<String, Object> rawAttributes(EventHubEventDto event, JsonNode raw) {
        Map<String, Object> result = new LinkedHashMap<>();
        put(result, "externalSourceEventId", event == null ? null : event.externalSourceEventId());
        put(result, "eventDomain", event == null || event.eventDomain() == null ? null : event.eventDomain().name());
        put(result, "eventType", event == null || event.eventType() == null ? null : event.eventType().name());
        put(result, "lifecycle", event == null || event.lifecycle() == null ? null : event.lifecycle().name());
        if (raw != null) {
            result.put("raw", raw);
        }
        return result;
    }

    private String sourceFamily(EventHubEventDto event) {
        if (event == null || event.packageInfo() == null) {
            return null;
        }
        return event.packageInfo().eventFamily();
    }

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

    private JsonNode rawPayload(EventHubEventDto event) {
        return TachographRuntimeIdentityResolver.rawPayload(event);
    }

    private String detailText(EventHubEventDto event, String field) {
        if (event == null || event.eventDetails() == null || event.eventDetails().attributes() == null) {
            return null;
        }
        JsonNode value = event.eventDetails().attributes().get(field);
        return value == null || value.isNull() ? null : value.asText(null);
    }

    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 Long longValue(JsonNode node, String field) {
        if (node == null || field == null) {
            return null;
        }
        JsonNode value = node.get(field);
        if (value == null || value.isNull()) {
            return null;
        }
        try {
            return value.isNumber() ? value.asLong() : Long.parseLong(value.asText());
        } catch (NumberFormatException ex) {
            return null;
        }
    }

    private BigDecimal decimal(JsonNode node, String field) {
        if (node == null || field == null) {
            return null;
        }
        JsonNode value = node.get(field);
        if (value == null || value.isNull()) {
            return null;
        }
        if (value.isNumber()) {
            return value.decimalValue();
        }
        String text = value.asText(null);
        if (text == null || text.isBlank()) {
            return null;
        }
        try {
            return new BigDecimal(text.trim());
        } catch (NumberFormatException ex) {
            return null;
        }
    }

    private Long toKilometers(Long meters) {
        return meters == null ? null : meters / 1_000L;
    }

    @SafeVarargs
    private final <T> T firstNonNull(T... values) {
        if (values == null) {
            return null;
        }
        for (T value : values) {
            if (value != null) {
                return value;
            }
        }
        return null;
    }

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

    private void put(ObjectNode node, String field, Object value) {
        if (node != null && field != null && value != null) {
            node.set(field, objectMapper.valueToTree(value));
        }
    }

    private void put(Map<String, Object> target, String field, Object value) {
        if (target != null && field != null && value != null) {
            target.put(field, value);
        }
    }
}
