package at.procon.eventhub.yellowfox.service;

import at.procon.eventhub.config.EventHubProperties;
import at.procon.eventhub.dto.EventSourceDto;
import at.procon.eventhub.importing.masterdata.MasterDataRefreshResult;
import at.procon.eventhub.importing.masterdata.SourceMasterEntityUpsert;
import at.procon.eventhub.importing.masterdata.SourceMasterRelationUpsert;
import at.procon.eventhub.persistence.DriverIdentityRepository;
import at.procon.eventhub.persistence.EventSourceRepository;
import at.procon.eventhub.persistence.SourceMasterDataRepository;
import at.procon.eventhub.persistence.VehicleIdentityRepository;
import at.procon.eventhub.yellowfox.dto.YellowFoxD8ImportRequest;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StreamUtils;

@Service
public class YellowFoxMasterDataRefreshService {

    private static final Logger log = LoggerFactory.getLogger(YellowFoxMasterDataRefreshService.class);

    private static final List<String> ENTITY_SQL_RESOURCES = List.of(
            "classpath:sql/yellowfox/master-data/fleets.sql",
            "classpath:sql/yellowfox/master-data/drivers.sql",
            "classpath:sql/yellowfox/master-data/driver-cards.sql",
            "classpath:sql/yellowfox/master-data/vehicles.sql",
            "classpath:sql/yellowfox/master-data/vehicle-registrations.sql",
            "classpath:sql/yellowfox/master-data/telematic-providers.sql"
    );

    private static final String RELATIONS_SQL_RESOURCE = "classpath:sql/yellowfox/master-data/relations.sql";
    private static final String MASTER_DATA_SOURCE_KIND = "MASTER_DATA";
    private static final String MASTER_DATA_SOURCE_KEY = "YELLOWFOX_MASTER_DATA";
    private static final int MASTER_DATA_UPSERT_BATCH_SIZE = 5000;

    private final ObjectProvider<NamedParameterJdbcTemplate> yellowFoxJdbcTemplateProvider;
    private final SourceMasterDataRepository sourceMasterDataRepository;
    private final EventSourceRepository eventSourceRepository;
    private final DriverIdentityRepository driverIdentityRepository;
    private final VehicleIdentityRepository vehicleIdentityRepository;
    private final ResourceLoader resourceLoader;
    private final EventHubProperties properties;

    public YellowFoxMasterDataRefreshService(
            @Qualifier("yellowFoxNamedParameterJdbcTemplate") ObjectProvider<NamedParameterJdbcTemplate> yellowFoxJdbcTemplateProvider,
            SourceMasterDataRepository sourceMasterDataRepository,
            EventSourceRepository eventSourceRepository,
            DriverIdentityRepository driverIdentityRepository,
            VehicleIdentityRepository vehicleIdentityRepository,
            ResourceLoader resourceLoader,
            EventHubProperties properties
    ) {
        this.yellowFoxJdbcTemplateProvider = yellowFoxJdbcTemplateProvider;
        this.sourceMasterDataRepository = sourceMasterDataRepository;
        this.eventSourceRepository = eventSourceRepository;
        this.driverIdentityRepository = driverIdentityRepository;
        this.vehicleIdentityRepository = vehicleIdentityRepository;
        this.resourceLoader = resourceLoader;
        this.properties = properties;
    }

    public MasterDataRefreshResult refreshIfRequested(YellowFoxD8ImportRequest request) {
        if (!request.refreshMasterDataFirst()) {
            return MasterDataRefreshResult.empty();
        }
        return refresh(request);
    }

    public MasterDataRefreshResult refresh(YellowFoxD8ImportRequest request) {
        NamedParameterJdbcTemplate yellowFoxJdbcTemplate = yellowFoxJdbcTemplateProvider.getIfAvailable();
        if (yellowFoxJdbcTemplate == null) {
            log.info("Skipping YellowFox master-data refresh for tenant={} because no YellowFox datasource is configured.",
                    request.tenantKey());
            return MasterDataRefreshResult.empty();
        }

        String tenantKey = request.tenantKey() == null || request.tenantKey().isBlank() ? "default" : request.tenantKey().trim();
        EventSourceDto masterDataSource = masterDataSourceFor(request.eventSource());
        int eventSourceId = eventSourceRepository.resolveSourceId(tenantKey, masterDataSource);

        log.info("Starting YellowFox source master-data refresh tenant={} source={} batchSize={}",
                tenantKey, masterDataSource.stableKey(), MASTER_DATA_UPSERT_BATCH_SIZE);

        int entities = 0;
        for (String sqlResource : ENTITY_SQL_RESOURCES) {
            entities += streamEntities(yellowFoxJdbcTemplate, tenantKey, eventSourceId, sqlResource, loadSql(sqlResource));
        }

        int relationCount = streamRelations(yellowFoxJdbcTemplate, tenantKey, eventSourceId, RELATIONS_SQL_RESOURCE, loadSql(RELATIONS_SQL_RESOURCE));
        boolean syncVehicleRegistrations = properties.getYellowFox().isSyncVehicleRegistrationsOnMasterDataUpdate();

        log.info("Reconciling YellowFox driver identities from source master data tenant={} source={}",
                tenantKey, masterDataSource.stableKey());
        int reconciledDrivers = driverIdentityRepository.reconcileFromMasterData(tenantKey, eventSourceId);

        log.info("Reconciling YellowFox vehicle identities from source master data tenant={} source={} syncVehicleRegistrations={}",
                tenantKey, masterDataSource.stableKey(), syncVehicleRegistrations);
        int reconciledVehicles = vehicleIdentityRepository.reconcileFromMasterData(
                tenantKey,
                eventSourceId,
                syncVehicleRegistrations
        );

        MasterDataRefreshResult result = new MasterDataRefreshResult(entities, relationCount);
        log.info("Refreshed YellowFox source master data tenant={} source={} entities={} relations={} reconciledDrivers={} reconciledVehicles={}",
                tenantKey, masterDataSource.stableKey(), result.entitiesUpserted(), result.relationsUpserted(), reconciledDrivers, reconciledVehicles);
        return result;
    }

    private int streamEntities(
            NamedParameterJdbcTemplate yellowFoxJdbcTemplate,
            String tenantKey,
            int eventSourceId,
            String sqlResource,
            String sql
    ) {
        String section = masterDataSection(sqlResource);
        List<SourceMasterEntityUpsert> buffer = new ArrayList<>(MASTER_DATA_UPSERT_BATCH_SIZE);
        Map<String, Integer> typeCounts = new LinkedHashMap<>();
        int[] count = {0};
        int[] chunk = {0};

        log.info("Reading YellowFox master-data entities tenant={} section={}", tenantKey, section);
        yellowFoxJdbcTemplate.query(sql, Map.of(), rs -> {
            SourceMasterEntityUpsert entity = entity(rs);
            buffer.add(entity);
            increment(typeCounts, entity.entityType());
            if (buffer.size() >= MASTER_DATA_UPSERT_BATCH_SIZE) {
                count[0] += flushEntities(tenantKey, eventSourceId, section, ++chunk[0], buffer, count[0], typeCounts);
                buffer.clear();
            }
        });
        if (!buffer.isEmpty()) {
            count[0] += flushEntities(tenantKey, eventSourceId, section, ++chunk[0], buffer, count[0], typeCounts);
        }
        log.info("Finished YellowFox master-data entities tenant={} section={} processed={} byType={}",
                tenantKey, section, count[0], typeCounts);
        return count[0];
    }

    private int streamRelations(
            NamedParameterJdbcTemplate yellowFoxJdbcTemplate,
            String tenantKey,
            int eventSourceId,
            String sqlResource,
            String sql
    ) {
        String section = masterDataSection(sqlResource);
        List<SourceMasterRelationUpsert> buffer = new ArrayList<>(MASTER_DATA_UPSERT_BATCH_SIZE);
        Map<String, Integer> typeCounts = new LinkedHashMap<>();
        int[] count = {0};
        int[] chunk = {0};

        log.info("Reading YellowFox master-data relations tenant={} section={}", tenantKey, section);
        yellowFoxJdbcTemplate.query(sql, Map.of(), rs -> {
            SourceMasterRelationUpsert relation = relation(rs);
            buffer.add(relation);
            increment(typeCounts, relation.relationType());
            if (buffer.size() >= MASTER_DATA_UPSERT_BATCH_SIZE) {
                count[0] += flushRelations(tenantKey, eventSourceId, section, ++chunk[0], buffer, count[0], typeCounts);
                buffer.clear();
            }
        });
        if (!buffer.isEmpty()) {
            count[0] += flushRelations(tenantKey, eventSourceId, section, ++chunk[0], buffer, count[0], typeCounts);
        }
        log.info("Finished YellowFox master-data relations tenant={} section={} processed={} byType={}",
                tenantKey, section, count[0], typeCounts);
        return count[0];
    }

    private int flushEntities(
            String tenantKey,
            int eventSourceId,
            String section,
            int chunk,
            List<SourceMasterEntityUpsert> buffer,
            int alreadyProcessed,
            Map<String, Integer> typeCounts
    ) {
        int upserted = sourceMasterDataRepository.upsertEntities(tenantKey, eventSourceId, buffer);
        log.info("YellowFox master-data entity progress tenant={} section={} chunk={} chunkSize={} processed={} byType={}",
                tenantKey, section, chunk, buffer.size(), alreadyProcessed + upserted, typeCounts);
        return upserted;
    }

    private int flushRelations(
            String tenantKey,
            int eventSourceId,
            String section,
            int chunk,
            List<SourceMasterRelationUpsert> buffer,
            int alreadyProcessed,
            Map<String, Integer> typeCounts
    ) {
        int upserted = sourceMasterDataRepository.upsertRelations(tenantKey, eventSourceId, buffer);
        log.info("YellowFox master-data relation progress tenant={} section={} chunk={} chunkSize={} processed={} byType={}",
                tenantKey, section, chunk, buffer.size(), alreadyProcessed + upserted, typeCounts);
        return upserted;
    }

    private void increment(Map<String, Integer> counts, String type) {
        String key = type == null || type.isBlank() ? "UNKNOWN" : type;
        counts.merge(key, 1, Integer::sum);
    }

    private String masterDataSection(String sqlResource) {
        int slash = sqlResource.lastIndexOf('/');
        String fileName = slash < 0 ? sqlResource : sqlResource.substring(slash + 1);
        return fileName.endsWith(".sql") ? fileName.substring(0, fileName.length() - 4) : fileName;
    }

    private EventSourceDto masterDataSourceFor(EventSourceDto source) {
        return new EventSourceDto(
                source.providerKey(),
                MASTER_DATA_SOURCE_KIND,
                MASTER_DATA_SOURCE_KEY,
                source.sourceInstanceKey(),
                source.tenantProviderSettingKey(),
                source.externalFleetKey()
        );
    }

    private SourceMasterEntityUpsert entity(ResultSet rs) throws SQLException {
        String entityType = string(rs, "entity_type");
        String sourceEntityId = string(rs, "source_entity_id");
        return new SourceMasterEntityUpsert(
                entityType,
                sourceEntityId,
                string(rs, "source_external_key"),
                string(rs, "display_name"),
                bool(rs, "active"),
                offsetDateTime(rs, "valid_from"),
                offsetDateTime(rs, "valid_to"),
                offsetDateTime(rs, "source_updated_at"),
                payload(rs)
        );
    }

    private SourceMasterRelationUpsert relation(ResultSet rs) throws SQLException {
        return new SourceMasterRelationUpsert(
                string(rs, "relation_type"),
                string(rs, "from_entity_type"),
                string(rs, "from_source_entity_id"),
                string(rs, "to_entity_type"),
                string(rs, "to_source_entity_id"),
                offsetDateTime(rs, "valid_from"),
                offsetDateTime(rs, "valid_to"),
                offsetDateTime(rs, "source_updated_at"),
                payload(rs)
        );
    }

    private Map<String, Object> payload(ResultSet rs) throws SQLException {
        ResultSetMetaData metaData = rs.getMetaData();
        Map<String, Object> payload = new LinkedHashMap<>();
        for (int i = 1; i <= metaData.getColumnCount(); i++) {
            String name = metaData.getColumnLabel(i);
            Object value = rs.getObject(i);
            if (value != null) {
                payload.put(name, value);
            }
        }
        return payload;
    }

    private String loadSql(String location) {
        Resource resource = resourceLoader.getResource(location);
        try (var inputStream = resource.getInputStream()) {
            return StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
        } catch (IOException e) {
            throw new IllegalStateException("Cannot load YellowFox master-data SQL resource " + location, e);
        }
    }

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

    private Boolean bool(ResultSet rs, String column) throws SQLException {
        Object value = rs.getObject(column);
        if (value == null) {
            return null;
        }
        if (value instanceof Boolean bool) {
            return bool;
        }
        if (value instanceof Number number) {
            return number.intValue() != 0;
        }
        return Boolean.parseBoolean(value.toString());
    }

    private 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);
        }
    }
}
