package com.magistr.app.config.tenant;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
import java.util.*;
import java.util.stream.Collectors;

/**
 * Периодически перечитывает tenants.json (mounted ConfigMap).
 * Если ConfigMap был обновлён через K8s API, этот компонент
 * подхватит изменения и синхронизирует in-memory datasource'ы.
 *
 * Также отвечает за инициализацию БД (init.sql) для новых тенантов.
 */
@Component
public class TenantConfigWatcher {

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

    private final TenantRoutingDataSource routingDataSource;
    private final DataSource dataSource;
    private final ObjectMapper objectMapper = new ObjectMapper();

    @Value("${app.tenants.config-path:tenants.json}")
    private String tenantsConfigPath;

    // Хеш последнего прочитанного конфига — чтобы не перезагружать зря
    private String lastConfigHash = "";

    public TenantConfigWatcher(TenantRoutingDataSource routingDataSource, DataSource dataSource) {
        this.routingDataSource = routingDataSource;
        this.dataSource = dataSource;
    }

    /**
     * Каждые 30 секунд проверяет, изменился ли tenants.json.
     */
    @Scheduled(fixedDelay = 30_000, initialDelay = 30_000)
    public void watchForChanges() {
        try {
            File file = new File(tenantsConfigPath);
            if (!file.exists()) return;

            String content = new String(java.nio.file.Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8);
            String hash = configHash(content);

            if (hash.equals(lastConfigHash)) {
                return; // Ничего не изменилось
            }

            log.info("Обнаружено изменение tenants.json (хеш: {} -> {}), перечитываем конфиг", lastConfigHash, hash);
            lastConfigHash = hash;

            List<TenantConfig> newTenants = objectMapper.readValue(content, new TypeReference<>() {});
            syncTenants(newTenants);

        } catch (Exception e) {
            log.error("Ошибка при проверке конфига тенантов: {}", e.getMessage());
        }
    }

    /**
     * Обновляет хеш конфига (вызывается после ручного обновления ConfigMap с этого же пода).
     */
    public void refreshHash() {
        try {
            File file = new File(tenantsConfigPath);
            if (file.exists()) {
                String content = new String(java.nio.file.Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8);
                lastConfigHash = configHash(content);
            }
        } catch (Exception e) {
            log.warn("Не удалось обновить хеш конфига тенантов: {}", e.getMessage());
        }
    }

    static String configHash(String content) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            return HexFormat.of().formatHex(digest.digest(content.getBytes(StandardCharsets.UTF_8)));
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("SHA-256 недоступен в текущем окружении", e);
        }
    }

    /**
     * Синхронизирует in-memory тенантов с конфигом из файла.
     */
    private void syncTenants(List<TenantConfig> newTenants) {
        Map<String, TenantConfig> current = routingDataSource.getTenantConfigs();
        Set<String> newDomains = newTenants.stream()
                .map(t -> t.getDomain().toLowerCase())
                .collect(Collectors.toSet());

        // Добавить новые тенанты
        for (TenantConfig tenant : newTenants) {
            String domain = tenant.getDomain().toLowerCase();
            if (!current.containsKey(domain)) {
                log.info("Добавляем нового тенанта '{}' из обновлённого ConfigMap", domain);
                routingDataSource.addTenant(tenant);
                // Инициализируем БД для нового тенанта
                initDatabaseForTenant(tenant);
            }
        }

        // Удалить тенанты, которых больше нет в конфиге
        for (String existingDomain : new ArrayList<>(current.keySet())) {
            if (!newDomains.contains(existingDomain)) {
                log.info("Удаляем тенанта '{}' — его больше нет в ConfigMap", existingDomain);
                routingDataSource.removeTenant(existingDomain);
            }
        }
    }

    /**
     * Выполняет миграции Flyway для конкретного тенанта пи подключении.
     * Если БД уже существует, но история Flyway пуста — 
     * делает baseline (считает V1_init.sql уже выполненным).
     */
    public void initDatabaseForTenant(TenantConfig tenant) {
        String domain = tenant.getDomain();
        try {
            TenantContext.setCurrentTenant(domain);

            log.info("[{}] Запускаем миграции Flyway", domain);
            
            // Получаем DataSource конкретно для этого тенанта
            javax.sql.DataSource tenantDs = routingDataSource.getResolvedDataSources().get(domain);
            if (tenantDs == null) {
                // Если ещё не resolve'нулся (первый запуск), берём обёртку
                tenantDs = dataSource;
            }

            org.flywaydb.core.Flyway flyway = org.flywaydb.core.Flyway.configure()
                    .dataSource(tenantDs)
                    .baselineOnMigrate(true)
                    .baselineVersion("1")
                    .load();

            flyway.migrate();
            log.info("[{}] Миграции Flyway успешно выполнены", domain);

        } catch (Exception e) {
            log.error("[{}] Ошибка миграции Flyway: {}", domain, e.getMessage());
        } finally {
            TenantContext.clear();
        }
    }
}
