package at.procon.eventhub.tachograph.service;

import at.procon.eventhub.dto.CardSlot;
import at.procon.eventhub.dto.CardStatus;
import at.procon.eventhub.dto.DriverCardRefDto;
import at.procon.eventhub.dto.DriverRefDto;
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.SourcePackageRefDto;
import at.procon.eventhub.dto.VehicleRefDto;
import at.procon.eventhub.dto.VehicleRegistrationRefDto;
import at.procon.eventhub.importing.extraction.ExtractionContext;
import at.procon.eventhub.importing.extraction.ExtractionRowMapper;
import at.procon.eventhub.service.EventDetailsFactory;
import at.procon.eventhub.tachograph.dto.TachographImportRequest;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;

abstract class AbstractTachographCardEventRowMapper implements ExtractionRowMapper<TachographImportRequest> {

    private final EventDetailsFactory detailsFactory;

    protected AbstractTachographCardEventRowMapper(EventDetailsFactory detailsFactory) {
        this.detailsFactory = detailsFactory;
    }

    @Override
    public EventHubEventDto map(ResultSet rs, int rowNum, ExtractionContext<TachographImportRequest> context) throws SQLException {
        OffsetDateTime occurredAt = offsetDateTime(rs, "occurred_at");
        SourcePackageRefDto sourcePackageRef = sourcePackageRef(rs);
        DriverRefDto driverRef = driverRef(rs);
        VehicleRefDto vehicleRef = vehicleRef(rs);
        DriverCardRefDto driverCard = driverRef == null ? null : driverRef.driverCard();
        EventType eventType = eventType(rs);
        EventLifecycle lifecycle = lifecycle(rs);
        CardStatus cardStatus = cardStatus(lifecycle);

        String externalSourceEventId = string(rs, "external_source_event_id");
        if (externalSourceEventId == null) {
            externalSourceEventId = defaultExternalSourceEventId(context, rowNum, occurredAt, sourcePackageRef, driverRef, vehicleRef, lifecycle);
        }

        return new EventHubEventDto(
                UUID.randomUUID(),
                externalSourceEventId,
                driverRef,
                vehicleRef,
                occurredAt,
                offsetDateTime(rs, "received_partner_at"),
                OffsetDateTime.now(),
                EventDomain.DRIVER_CARD,
                eventType,
                lifecycle,
                longValue(rs, "odometer_m"),
                null,
                detailsFactory.driverCard(cardSlot(rs), cardStatus, driverCard),
                sourcePackageRef != null && sourcePackageRef.hasAnyReference() ? sourcePackageRef : null,
                detailsFactory.payloadFromMap(payload(rs)),
                false,
                context.packageInfo()
        );
    }

    protected Map<String, Object> sourceSpecificPayload(ResultSet rs) throws SQLException {
        return Map.of();
    }

    private DriverRefDto driverRef(ResultSet rs) throws SQLException {
        DriverCardRefDto driverCard = null;
        String cardNumber = string(rs, "driver_card_number");
        if (cardNumber != null) {
            driverCard = new DriverCardRefDto(string(rs, "driver_card_nation"), cardNumber);
        }
        DriverRefDto driverRef = new DriverRefDto(string(rs, "driver_source_entity_id"), driverCard);
        return driverRef.hasAnyReference() ? driverRef : null;
    }

    private VehicleRefDto vehicleRef(ResultSet rs) throws SQLException {
        VehicleRegistrationRefDto registration = null;
        String registrationNumber = string(rs, "vehicle_registration_number");
        if (registrationNumber != null) {
            registration = new VehicleRegistrationRefDto(string(rs, "vehicle_registration_nation"), registrationNumber);
        }
        VehicleRefDto vehicleRef = new VehicleRefDto(
                string(rs, "vehicle_source_entity_id"),
                string(rs, "vehicle_vin"),
                string(rs, "vehicle_registration_source_entity_id"),
                registration
        );
        return vehicleRef.hasAnyReference() ? vehicleRef : null;
    }

    private SourcePackageRefDto sourcePackageRef(ResultSet rs) throws SQLException {
        return new SourcePackageRefDto(
                string(rs, "source_package_kind"),
                string(rs, "source_package_id"),
                string(rs, "source_package_entity_id"),
                offsetDateTime(rs, "source_package_period_from"),
                offsetDateTime(rs, "source_package_period_to"),
                offsetDateTime(rs, "source_package_imported_at")
        );
    }

    private Map<String, Object> payload(ResultSet rs) throws SQLException {
        Map<String, Object> raw = new LinkedHashMap<>();
        put(raw, "sourceRowId", string(rs, "source_row_id"));
        raw.putAll(sourceSpecificPayload(rs));
        return Map.of("raw", raw);
    }

    private String defaultExternalSourceEventId(
            ExtractionContext<TachographImportRequest> context,
            int rowNum,
            OffsetDateTime occurredAt,
            SourcePackageRefDto sourcePackageRef,
            DriverRefDto driverRef,
            VehicleRefDto vehicleRef,
            EventLifecycle lifecycle
    ) {
        String sourcePackageId = sourcePackageRef == null || sourcePackageRef.sourcePackageId() == null
                ? "NO_SOURCE_PACKAGE"
                : sourcePackageRef.sourcePackageId();
        String subject = driverRef != null && driverRef.hasAnyReference()
                ? driverRef.stableKey()
                : vehicleRef == null ? "NO_SUBJECT" : vehicleRef.stableKey();
        return "TACHOGRAPH:" + context.planItem().extractionCode()
                + ":" + sourcePackageId
                + ":" + lifecycle
                + ":" + occurredAt
                + ":" + subject
                + ":ROW-" + rowNum;
    }

    protected String string(ResultSet rs, String column) throws SQLException {
        String value = rs.getString(column);
        return value == null || value.isBlank() ? null : value.trim();
    }

    protected Object object(ResultSet rs, String column) throws SQLException {
        return rs.getObject(column);
    }

    protected OffsetDateTime offsetDateTime(ResultSet rs, String column) throws SQLException {
        Object value = rs.getObject(column);
        if (value == null) {
            return null;
        }
        if (value instanceof OffsetDateTime offsetDateTime) {
            return offsetDateTime.withOffsetSameInstant(ZoneOffset.UTC);
        }
        if (value instanceof Timestamp timestamp) {
            return timestamp.toLocalDateTime().atOffset(ZoneOffset.UTC);
        }
        if (value instanceof LocalDateTime localDateTime) {
            return localDateTime.atOffset(ZoneOffset.UTC);
        }
        String text = value.toString();
        try {
            return OffsetDateTime.parse(text).withOffsetSameInstant(ZoneOffset.UTC);
        } catch (RuntimeException ignored) {
            return LocalDateTime.parse(text).atOffset(ZoneOffset.UTC);
        }
    }

    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 CardSlot cardSlot(ResultSet rs) throws SQLException {
        Object value = object(rs, "card_slot");
        if (value instanceof Number number) {
            return switch (number.intValue()) {
                case 0 -> CardSlot.DRIVER;
                case 1 -> CardSlot.CO_DRIVER;
                default -> null;
            };
        }
        String text = value == null ? null : value.toString();
        Integer parsed = parseInteger(text);
        if (parsed != null) {
            return parsed == 0 ? CardSlot.DRIVER : parsed == 1 ? CardSlot.CO_DRIVER : null;
        }
        return parseEnum(CardSlot.class, text, null);
    }

    private EventType eventType(ResultSet rs) throws SQLException {
        return parseEnum(EventType.class, string(rs, "event_type"), EventType.UNKNOWN_EVENT);
    }

    private EventLifecycle lifecycle(ResultSet rs) throws SQLException {
        return parseEnum(EventLifecycle.class, string(rs, "lifecycle"), EventLifecycle.SNAPSHOT);
    }

    private CardStatus cardStatus(EventLifecycle lifecycle) {
        return lifecycle == EventLifecycle.INSERT ? CardStatus.INSERTED : lifecycle == EventLifecycle.WITHDRAW ? CardStatus.NOT_INSERTED : null;
    }

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

    private Integer parseInteger(String value) {
        if (value == null || value.isBlank()) {
            return null;
        }
        try {
            return Integer.parseInt(value.trim());
        } catch (NumberFormatException ignored) {
            return null;
        }
    }

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