package at.procon.eventhub.service;

import at.procon.eventhub.dto.EventHubEventDto;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Component;

@Component
public class EventAcquisitionRecordKeyService {

    /**
     * Source-record key is used only for acquisition idempotency of the same source record.
     * It does not merge or deduplicate equivalent events from different providers/sources.
     */
    public String buildSourceRecordKeyHash(EventHubEventDto event, int eventSourceId) {
        String sourceRecordKey = String.join("|",
                nullToEmpty(event.packageInfo() == null ? null : event.packageInfo().tenantKey()),
                String.valueOf(eventSourceId),
                nullToEmpty(event.externalSourceEventId())
        );
        return sha256Hex(sourceRecordKey);
    }

    /**
     * Event signature is a non-unique acquisition-time semantic key.
     *
     * It intentionally excludes EventSource and externalSourceEventId. Multiple source records
     * may have the same signature and still must be stored separately. The signature is only a
     * later query/projection hint for comparing sources, filling gaps, or building merged views.
     */
    public String buildEventSignatureHash(EventHubEventDto event) {
        String signature = String.join("|",
                nullToEmpty(event.packageInfo() == null ? null : event.packageInfo().tenantKey()),
                driverSignatureKey(event),
                vehicleSignatureKey(event),
                normalizeTime(event.occurredAt()),
                event.eventDomain() == null ? "" : event.eventDomain().name(),
                event.eventType() == null ? "" : event.eventType().name(),
                event.lifecycle() == null ? "" : event.lifecycle().name(),
                event.eventDetails() == null ? "" : nullToEmpty(event.eventDetails().type()),
                canonicalJson(event.eventDetails() == null ? null : event.eventDetails().attributes()),
                event.position() == null ? "" : nullToEmpty(event.position().latitude()) + ":" + nullToEmpty(event.position().longitude())
        );
        return sha256Hex(signature);
    }

    private String driverSignatureKey(EventHubEventDto event) {
        if (event.driverRef() == null) {
            return "";
        }
        if (event.driverRef().driverCard() != null && event.driverRef().driverCard().hasValue()) {
            return "CARD:" + event.driverRef().driverCard().stableKey();
        }
        return "SOURCE_DRIVER:" + nullToEmpty(event.driverRef().sourceEntityId());
    }

    private String vehicleSignatureKey(EventHubEventDto event) {
        if (event.vehicleRef() == null) {
            return "";
        }
        boolean hasVin = event.vehicleRef().vin() != null && !event.vehicleRef().vin().isBlank();
        boolean hasRegistration = event.vehicleRef().vehicleRegistration() != null
                && event.vehicleRef().vehicleRegistration().hasValue();

        if (hasVin) {
            return "VIN:" + event.vehicleRef().vin();
        }
        if (hasRegistration) {
            return "VRN:" + event.vehicleRef().vehicleRegistration().stableKey();
        }
        if (event.vehicleRef().sourceVehicleEntityId() != null) {
            return "SOURCE_VEHICLE:" + event.vehicleRef().sourceVehicleEntityId();
        }
        return "SOURCE_REGISTRATION:" + nullToEmpty(event.vehicleRef().sourceRegistrationEntityId());
    }

    private String normalizeTime(OffsetDateTime value) {
        return value == null ? "" : value.toInstant().toString();
    }

    private String nullToEmpty(Object value) {
        return value == null ? "" : String.valueOf(value);
    }

    private String canonicalJson(JsonNode node) {
        if (node == null || node.isNull()) {
            return "";
        }
        if (node.isObject()) {
            ObjectNode objectNode = (ObjectNode) node;
            List<Map.Entry<String, JsonNode>> fields = new ArrayList<>();
            Iterator<Map.Entry<String, JsonNode>> iterator = objectNode.fields();
            iterator.forEachRemaining(fields::add);
            fields.sort(Comparator.comparing(Map.Entry::getKey));
            StringBuilder result = new StringBuilder("{");
            boolean first = true;
            for (Map.Entry<String, JsonNode> field : fields) {
                if (!first) {
                    result.append(',');
                }
                first = false;
                result.append(escape(field.getKey())).append(':').append(canonicalJson(field.getValue()));
            }
            return result.append('}').toString();
        }
        if (node.isArray()) {
            ArrayNode arrayNode = (ArrayNode) node;
            StringBuilder result = new StringBuilder("[");
            for (int i = 0; i < arrayNode.size(); i++) {
                if (i > 0) {
                    result.append(',');
                }
                result.append(canonicalJson(arrayNode.get(i)));
            }
            return result.append(']').toString();
        }
        if (node.isTextual()) {
            return escape(node.asText());
        }
        return node.toString();
    }

    private String escape(String value) {
        return '"' + value.replace("\\", "\\\\").replace("\"", "\\\"") + '"';
    }

    private String sha256Hex(String value) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] hash = digest.digest(value.getBytes(StandardCharsets.UTF_8));
            StringBuilder result = new StringBuilder(hash.length * 2);
            for (byte b : hash) {
                result.append(String.format("%02x", b));
            }
            return result.toString();
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("SHA-256 is not available", e);
        }
    }
}
