package at.procon.eventhub.tachograph.service;

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 AbstractTachographSpecificConditionRowMapper implements ExtractionRowMapper<TachographImportRequest> {

    private final EventDetailsFactory detailsFactory;

    protected AbstractTachographSpecificConditionRowMapper(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);
        EventDomain eventDomain = eventDomain(rs);
        EventType eventType = eventType(rs);
        EventLifecycle lifecycle = lifecycle(rs);

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

        return new EventHubEventDto(
                UUID.randomUUID(),
                externalSourceEventId,
                driverRef,
                vehicleRef,
                occurredAt,
                offsetDateTime(rs, "received_partner_at"),
                OffsetDateTime.now(),
                eventDomain,
                eventType,
                lifecycle,
                null,
                null,
                detailsFactory.specificCondition(),
                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,
            EventType eventType,
            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
                + ":" + eventType
                + ":" + 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 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 EventDomain eventDomain(ResultSet rs) throws SQLException {
        return parseEnum(EventDomain.class, string(rs, "event_domain"), EventDomain.SPECIFIC_CONDITION);
    }

    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 <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(' ', '_');
        try {
            return Enum.valueOf(type, normalized);
        } catch (IllegalArgumentException ignored) {
            return fallback;
        }
    }

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