package at.procon.eventhub.persistence;

import at.procon.eventhub.dto.DriverCardRefDto;
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.EventHubPackageRequest;
import at.procon.eventhub.dto.EventLifecycle;
import at.procon.eventhub.dto.EventSourceDto;
import at.procon.eventhub.dto.EventType;
import at.procon.eventhub.dto.GeoPointDto;
import at.procon.eventhub.dto.ImportScopeDto;
import at.procon.eventhub.dto.ImportScopeType;
import at.procon.eventhub.dto.SourceGroupRefDto;
import at.procon.eventhub.dto.SourceGroupType;
import at.procon.eventhub.dto.SourcePackageRefDto;
import at.procon.eventhub.dto.VehicleRefDto;
import at.procon.eventhub.dto.VehicleRegistrationRefDto;
import at.procon.eventhub.processing.model.UnifiedDriverEventsRequest;
import at.procon.eventhub.processing.model.UnifiedVehicleEventsRequest;
import at.procon.eventhub.reference.TachographNationRegistry;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

@Repository
public class EventHubEventReadRepository {

    private final JdbcTemplate jdbcTemplate;
    private final ObjectMapper objectMapper;

    public EventHubEventReadRepository(JdbcTemplate jdbcTemplate, ObjectMapper objectMapper) {
        this.jdbcTemplate = jdbcTemplate;
        this.objectMapper = objectMapper;
    }

    public List<EventHubEventDto> findEvents(
            UnifiedDriverEventsRequest request,
            String providerKey,
            List<String> sourceKinds
    ) {
        return findEvents(
                request.tenantKey(),
                request.occurredFrom(),
                request.occurredTo(),
                request.driverSourceEntityId(),
                request.driverCardNation(),
                request.driverCardNumber(),
                request.vehicleSourceEntityId(),
                request.vin(),
                request.registrationNation(),
                request.registrationNumber(),
                providerKey,
                sourceKinds
        );
    }

    public List<EventHubEventDto> findEventsByVehicle(
            UnifiedVehicleEventsRequest request,
            String providerKey,
            List<String> sourceKinds
    ) {
        return findEvents(
                request.tenantKey(),
                request.occurredFrom(),
                request.occurredTo(),
                null,
                null,
                null,
                request.vehicleSourceEntityId(),
                request.vin(),
                request.registrationNation(),
                request.registrationNumber(),
                providerKey,
                sourceKinds
        );
    }

    private List<EventHubEventDto> findEvents(
            String tenantKey,
            OffsetDateTime occurredFrom,
            OffsetDateTime occurredTo,
            String driverSourceEntityId,
            String driverCardNation,
            String driverCardNumber,
            String vehicleSourceEntityId,
            String vin,
            String registrationNation,
            String registrationNumber,
            String providerKey,
            List<String> sourceKinds
    ) {
        StringBuilder sql = new StringBuilder(
                """
                select
                    event.id,
                    event.external_source_event_id,
                    event.occurred_at,
                    event.received_partner_at,
                    event.received_hub_at,
                    event.event_domain,
                    event.event_type,
                    event.lifecycle,
                    event.odometer_m,
                    ST_Y(event.position::geometry) as latitude,
                    ST_X(event.position::geometry) as longitude,
                    event.payload,
                    event.manual_entry,
                    source.provider_key,
                    source.source_kind,
                    source.source_key,
                    source.source_instance_key,
                    source.tenant_provider_setting_key,
                    source.external_fleet_key,
                    package.id as data_package_id,
                    package.tenant_key,
                    package.event_family,
                    package.business_date,
                    package.external_package_id,
                    package.source_group_type,
                    package.source_group_entity_id,
                    package.source_group_code,
                    package.source_group_name,
                    package.import_scope_type,
                    package.root_source_org_entity_id,
                    package.root_source_org_code,
                    package.root_source_org_name,
                    package.include_children,
                    package.occurred_from as package_occurred_from,
                    package.occurred_to as package_occurred_to,
                    package.source_package_kind as package_source_package_kind,
                    package.source_package_period_from,
                    package.source_package_period_to,
                    package.source_package_imported_at,
                    event.source_package_id,
                    event.source_package_entity_id,
                    detail.detail_type,
                    detail.attributes,
                    driver.source_driver_entity_id,
                    driver_card.nation as driver_card_nation,
                    driver_card.nation_numeric_code as driver_card_nation_numeric_code,
                    driver_card.card_number as driver_card_number,
                    vehicle.source_vehicle_entity_id,
                    vehicle.vin,
                    registration.source_registration_entity_id,
                    registration.nation as vehicle_registration_nation,
                    registration.nation_numeric_code as vehicle_registration_nation_numeric_code,
                    registration.registration_number as vehicle_registration_number,
                    event.driver_id,
                    event.vehicle_id,
                    event.vehicle_registration_id
                from eventhub.event event
                join eventhub.event_source source on source.id = event.event_source_id
                join eventhub.data_package package on package.id = event.data_package_id
                left join lateral (
                    select detail_type, attributes
                    from eventhub.event_detail detail
                    where detail.event_occurred_at = event.occurred_at
                      and detail.event_id = event.id
                    order by detail_type
                    limit 1
                ) detail on true
                left join eventhub.driver driver on driver.id = event.driver_id
                left join eventhub.driver_card driver_card on driver_card.id = event.driver_card_id
                left join eventhub.vehicle vehicle on vehicle.id = event.vehicle_id
                left join eventhub.vehicle_registration registration on registration.id = event.vehicle_registration_id
                where package.tenant_key = ?
                  and source.provider_key = ?
                """
        );

        List<Object> params = new ArrayList<>();
        params.add(tenantKey);
        params.add(providerKey);

        if (sourceKinds != null && !sourceKinds.isEmpty()) {
            sql.append(" and source.source_kind in (");
            for (int i = 0; i < sourceKinds.size(); i++) {
                if (i > 0) {
                    sql.append(", ");
                }
                sql.append("?");
                params.add(sourceKinds.get(i));
            }
            sql.append(")");
        }
        if (occurredFrom != null) {
            sql.append(" and event.occurred_at >= ?");
            params.add(occurredFrom);
        }
        if (occurredTo != null) {
            sql.append(" and event.occurred_at <= ?");
            params.add(occurredTo);
        }
        if (driverSourceEntityId != null) {
            sql.append(" and driver.source_driver_entity_id = ?");
            params.add(driverSourceEntityId);
        }
        if (driverCardNumber != null) {
            sql.append(" and driver_card.card_number = ?");
            params.add(driverCardNumber);
            if (driverCardNation != null) {
                TachographNationRegistry.NationResolution driverCardNationResolution =
                        TachographNationRegistry.resolve(driverCardNation, null);
                if (driverCardNationResolution.numericCode() != null) {
                    sql.append(" and driver_card.nation_numeric_code = ?");
                    params.add(driverCardNationResolution.numericCode());
                } else {
                    sql.append(" and driver_card.nation = ?");
                    params.add(driverCardNationResolution.legacyNation());
                }
            }
        }
        if (vehicleSourceEntityId != null) {
            sql.append(" and vehicle.source_vehicle_entity_id = ?");
            params.add(vehicleSourceEntityId);
        }
        if (vin != null) {
            sql.append(" and vehicle.vin = ?");
            params.add(vin);
        }
        if (registrationNumber != null) {
            sql.append(" and registration.registration_number = ?");
            params.add(registrationNumber);
            if (registrationNation != null) {
                TachographNationRegistry.NationResolution registrationNationResolution =
                        TachographNationRegistry.resolve(registrationNation, null);
                if (registrationNationResolution.numericCode() != null) {
                    sql.append(" and registration.nation_numeric_code = ?");
                    params.add(registrationNationResolution.numericCode());
                } else {
                    sql.append(" and registration.nation = ?");
                    params.add(registrationNationResolution.legacyNation());
                }
            }
        }

        sql.append(" order by event.occurred_at, event.event_domain, event.event_type, event.lifecycle, event.id");

        return jdbcTemplate.query(
                sql.toString(),
                (rs, rowNum) -> mapEvent(rs),
                params.toArray()
        );
    }

    private EventHubEventDto mapEvent(ResultSet rs) throws SQLException {
        DriverRefDto driverRef = driverRef(rs);
        VehicleRefDto vehicleRef = vehicleRef(rs);
        if ((driverRef == null || !driverRef.hasAnyReference()) && (vehicleRef == null || !vehicleRef.hasAnyReference())) {
            throw new IllegalStateException("Loaded event does not have any driver or vehicle reference.");
        }

        return new EventHubEventDto(
                uuid(rs, "id"),
                rs.getString("external_source_event_id"),
                driverRef,
                vehicleRef,
                rs.getObject("occurred_at", OffsetDateTime.class),
                rs.getObject("received_partner_at", OffsetDateTime.class),
                rs.getObject("received_hub_at", OffsetDateTime.class),
                enumValue(EventDomain.class, rs.getString("event_domain"), EventDomain.TELEMATICS_DATA),
                enumValue(EventType.class, rs.getString("event_type"), EventType.UNKNOWN_EVENT),
                enumValue(EventLifecycle.class, rs.getString("lifecycle"), EventLifecycle.SNAPSHOT),
                longValue(rs, "odometer_m"),
                point(rs),
                eventDetails(rs),
                sourcePackageRef(rs),
                json(rs.getString("payload")),
                rs.getBoolean("manual_entry"),
                packageInfo(rs)
        );
    }

    private DriverRefDto driverRef(ResultSet rs) throws SQLException {
        String sourceEntityId = firstNonBlank(
                rs.getString("source_driver_entity_id"),
                syntheticId("EVENTHUB_DRIVER", uuid(rs, "driver_id"))
        );
        String cardNumber = rs.getString("driver_card_number");
        DriverCardRefDto driverCard = cardNumber == null
                ? null
                : new DriverCardRefDto(
                        rs.getString("driver_card_nation"),
                        integerValue(rs, "driver_card_nation_numeric_code"),
                        cardNumber
                );
        DriverRefDto driverRef = new DriverRefDto(sourceEntityId, driverCard);
        return driverRef.hasAnyReference() ? driverRef : null;
    }

    private VehicleRefDto vehicleRef(ResultSet rs) throws SQLException {
        VehicleRegistrationRefDto registration = null;
        String registrationNumber = rs.getString("vehicle_registration_number");
        if (registrationNumber != null) {
            registration = new VehicleRegistrationRefDto(
                    rs.getString("vehicle_registration_nation"),
                    integerValue(rs, "vehicle_registration_nation_numeric_code"),
                    registrationNumber
            );
        }
        VehicleRefDto vehicleRef = new VehicleRefDto(
                firstNonBlank(
                        rs.getString("source_vehicle_entity_id"),
                        syntheticId("EVENTHUB_VEHICLE", uuid(rs, "vehicle_id"))
                ),
                rs.getString("vin"),
                firstNonBlank(
                        rs.getString("source_registration_entity_id"),
                        syntheticId("EVENTHUB_VEHICLE_REGISTRATION", uuid(rs, "vehicle_registration_id"))
                ),
                registration
        );
        return vehicleRef.hasAnyReference() ? vehicleRef : null;
    }

    private EventDetailsDto eventDetails(ResultSet rs) throws SQLException {
        String detailType = rs.getString("detail_type");
        if (detailType == null || detailType.isBlank()) {
            return null;
        }
        return new EventDetailsDto(detailType, json(rs.getString("attributes")));
    }

    private SourcePackageRefDto sourcePackageRef(ResultSet rs) throws SQLException {
        String packageKind = rs.getString("package_source_package_kind");
        String sourcePackageId = firstNonBlank(rs.getString("source_package_id"), null);
        String sourceEntityId = firstNonBlank(rs.getString("source_package_entity_id"), null);
        OffsetDateTime periodFrom = rs.getObject("source_package_period_from", OffsetDateTime.class);
        OffsetDateTime periodTo = rs.getObject("source_package_period_to", OffsetDateTime.class);
        OffsetDateTime importedAt = rs.getObject("source_package_imported_at", OffsetDateTime.class);
        SourcePackageRefDto ref = new SourcePackageRefDto(
                packageKind,
                sourcePackageId,
                sourceEntityId,
                periodFrom,
                periodTo,
                importedAt
        );
        return ref.hasAnyReference() ? ref : null;
    }

    private EventHubPackageRequest packageInfo(ResultSet rs) throws SQLException {
        EventSourceDto eventSource = new EventSourceDto(
                rs.getString("provider_key"),
                rs.getString("source_kind"),
                rs.getString("source_key"),
                rs.getString("source_instance_key"),
                rs.getString("tenant_provider_setting_key"),
                rs.getString("external_fleet_key")
        );
        SourceGroupRefDto sourceGroup = sourceGroup(rs);
        ImportScopeDto importScope = importScope(rs);
        String eventFamily = firstNonBlank(rs.getString("event_family"), rs.getString("event_domain"));
        LocalDate businessDate = rs.getObject("business_date", LocalDate.class);
        String externalPackageId = firstNonBlank(rs.getString("external_package_id"), rs.getString("data_package_id"));
        return new EventHubPackageRequest(
                rs.getString("tenant_key"),
                eventSource,
                sourceGroup,
                importScope,
                eventFamily,
                businessDate,
                externalPackageId
        );
    }

    private SourceGroupRefDto sourceGroup(ResultSet rs) throws SQLException {
        String groupType = rs.getString("source_group_type");
        String sourceEntityId = rs.getString("source_group_entity_id");
        String code = rs.getString("source_group_code");
        String name = rs.getString("source_group_name");
        if (groupType == null && sourceEntityId == null && code == null && name == null) {
            return null;
        }
        return new SourceGroupRefDto(
                enumValue(SourceGroupType.class, groupType, null),
                sourceEntityId,
                code,
                name
        );
    }

    private ImportScopeDto importScope(ResultSet rs) throws SQLException {
        SourceGroupRefDto rootOrganisation = null;
        String rootEntityId = rs.getString("root_source_org_entity_id");
        String rootCode = rs.getString("root_source_org_code");
        String rootName = rs.getString("root_source_org_name");
        if (rootEntityId != null || rootCode != null || rootName != null) {
            rootOrganisation = new SourceGroupRefDto(SourceGroupType.ORGANISATION, rootEntityId, rootCode, rootName);
        }
        return new ImportScopeDto(
                enumValue(ImportScopeType.class, rs.getString("import_scope_type"), ImportScopeType.TENANT_ALL),
                rootOrganisation,
                rs.getBoolean("include_children"),
                rs.getObject("package_occurred_from", OffsetDateTime.class),
                rs.getObject("package_occurred_to", OffsetDateTime.class)
        );
    }

    private GeoPointDto point(ResultSet rs) throws SQLException {
        BigDecimal latitude = rs.getBigDecimal("latitude");
        BigDecimal longitude = rs.getBigDecimal("longitude");
        return latitude == null || longitude == null ? null : new GeoPointDto(latitude, longitude);
    }

    private UUID uuid(ResultSet rs, String column) throws SQLException {
        return (UUID) rs.getObject(column);
    }

    private Long longValue(ResultSet rs, String column) throws SQLException {
        Object value = rs.getObject(column);
        if (value == null) {
            return null;
        }
        if (value instanceof Number number) {
            return number.longValue();
        }
        return Long.parseLong(value.toString());
    }

    private Integer integerValue(ResultSet rs, String column) throws SQLException {
        Object value = rs.getObject(column);
        if (value == null) {
            return null;
        }
        if (value instanceof Number number) {
            return number.intValue();
        }
        return Integer.parseInt(value.toString());
    }

    static JsonNode parseJsonColumn(ObjectMapper objectMapper, String value) {
        try {
            JsonNode node = value == null || value.isBlank()
                    ? objectMapper.createObjectNode()
                    : objectMapper.readTree(value);
            while (node != null
                    && node.isTextual()
                    && looksLikeEmbeddedJson(node.textValue())) {
                node = objectMapper.readTree(node.textValue());
            }
            return node;
        } catch (JsonProcessingException e) {
            throw new IllegalArgumentException("Failed to parse JSON column.", e);
        }
    }

    private JsonNode json(String value) {
        return parseJsonColumn(objectMapper, value);
    }

    private static boolean looksLikeEmbeddedJson(String value) {
        if (value == null) {
            return false;
        }
        String trimmed = value.trim();
        return trimmed.startsWith("{") || trimmed.startsWith("[");
    }

    private String syntheticId(String prefix, UUID id) {
        return id == null ? null : prefix + ":" + id;
    }

    private String firstNonBlank(String first, String second) {
        if (first != null && !first.isBlank()) {
            return first;
        }
        return second != null && !second.isBlank() ? second : null;
    }

    private <T extends Enum<T>> T enumValue(Class<T> type, String value, T fallback) {
        if (value == null || value.isBlank()) {
            return fallback;
        }
        String normalized = value.trim().toUpperCase(Locale.ROOT).replace('-', '_').replace(' ', '_');
        try {
            return Enum.valueOf(type, normalized);
        } catch (IllegalArgumentException ignored) {
            return fallback;
        }
    }
}
