package com.magistr.app.service;

import com.magistr.app.config.tenant.TenantConfig;
import com.magistr.app.config.tenant.TenantConfigPersistenceException;
import com.magistr.app.config.tenant.TenantConfigStore;
import com.magistr.app.config.tenant.TenantRoutingDataSource;
import com.magistr.app.config.tenant.TenantSecretCompensationResult;
import com.magistr.app.config.tenant.TenantSecretUpdateReceipt;
import com.magistr.app.config.tenant.health.TenantReadinessRegistry;
import com.zaxxer.hikari.HikariDataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import javax.sql.DataSource;
import java.sql.Connection;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.regex.Pattern;
import java.util.function.Supplier;

@Service
public class TenantLifecycleService {

    private static final Logger log = LoggerFactory.getLogger(TenantLifecycleService.class);
    private static final Pattern DOMAIN_PATTERN = Pattern.compile(
            "[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?"
    );

    private final TenantRoutingDataSource routingDataSource;
    private final TenantConfigStore tenantConfigStore;
    private final TenantDatabaseMigrationService migrationService;
    private final RetiredTenantPoolService retiredPoolService;
    private final TenantReadinessRegistry readinessRegistry;

    public TenantLifecycleService(TenantRoutingDataSource routingDataSource,
                                  TenantConfigStore tenantConfigStore,
                                  TenantDatabaseMigrationService migrationService,
                                  RetiredTenantPoolService retiredPoolService,
                                  TenantReadinessRegistry readinessRegistry) {
        this.routingDataSource = routingDataSource;
        this.tenantConfigStore = tenantConfigStore;
        this.migrationService = migrationService;
        this.retiredPoolService = retiredPoolService;
        this.readinessRegistry = readinessRegistry;
    }

    /**
     * Выполняет составную lifecycle-операцию под общим reentrant monitor.
     * Используется для сериализации внешних read/compare/apply последовательностей
     * с add/update/remove, чтобы watcher не применял устаревший снимок.
     */
    public synchronized <T> T executeSerialized(Supplier<T> action) {
        return action.get();
    }

    /**
     * Выполняет составную lifecycle-операцию без возвращаемого значения.
     */
    public synchronized void executeSerialized(Runnable action) {
        action.run();
    }

    /**
     * Добавляет или обновляет tenant без разрыва действующего подключения.
     */
    public synchronized TenantConfig addOrUpdateTenant(TenantConfig requestedConfig) {
        return addOrUpdateTenantWithReceipt(requestedConfig).tenant();
    }

    /**
     * Добавляет или обновляет tenant и возвращает подтверждённый результат персистенции.
     */
    public synchronized TenantLifecycleMutationResult addOrUpdateTenantWithReceipt(TenantConfig requestedConfig) {
        TenantConfig config = validateAndNormalize(requestedConfig);
        Map<String, TenantConfig> oldConfigs = routingDataSource.snapshotTenantConfigs();
        Map<String, TenantConfig> desiredConfigs = new LinkedHashMap<>(oldConfigs);
        desiredConfigs.put(config.getDomain(), copyConfig(config));

        HikariDataSource candidate = null;
        boolean activated = false;
        try {
            candidate = prepareCandidate(config);
            verifyConnection(candidate);
            migrateCandidate(candidate);
            TenantSecretUpdateReceipt receipt = persistUpsert(config);

            TenantRoutingDataSource.TenantState previous;
            try {
                previous = routingDataSource.swapTenant(config, candidate);
            } catch (RuntimeException activationFailure) {
                throw activationFailureAfterCompensation(receipt, activationFailure);
            }

            activated = true;
            publishSuccessfulMutation(receipt, sortedConfigs(desiredConfigs), config);
            retireDataSource(previous == null ? null : previous.dataSource());
            log.info("Lifecycle тенанта '{}' успешно применён", config.getDomain());
            return new TenantLifecycleMutationResult(config, receipt);
        } finally {
            if (!activated) {
                closeCandidateDataSource(candidate);
            }
        }
    }

    /**
     * Сначала сохраняет desired-конфигурацию, затем локально отключает tenant.
     */
    public synchronized TenantConfig removeTenant(String requestedDomain) {
        return removeTenantWithReceipt(requestedDomain).tenant();
    }

    /**
     * Удаляет tenant и возвращает подтверждённый результат персистенции.
     */
    public synchronized TenantLifecycleMutationResult removeTenantWithReceipt(String requestedDomain) {
        String domain = validateAndNormalizeDomain(requestedDomain);
        Map<String, TenantConfig> oldConfigs = routingDataSource.snapshotTenantConfigs();
        TenantConfig existing = oldConfigs.get(domain);
        if (existing == null) {
            throw new NoSuchElementException("Тенант '" + domain + "' не найден");
        }

        Map<String, TenantConfig> desiredConfigs = new LinkedHashMap<>(oldConfigs);
        desiredConfigs.remove(domain);
        TenantSecretUpdateReceipt receipt = persistRemove(domain);

        TenantRoutingDataSource.TenantState removed;
        try {
            removed = routingDataSource.removeTenantAtomically(domain);
            if (removed == null) {
                throw new IllegalStateException("Локальное состояние тенанта отсутствует");
            }
        } catch (RuntimeException removalFailure) {
            throw removalFailureAfterCompensation(receipt, removalFailure);
        }

        publishSuccessfulMutation(receipt, sortedConfigs(desiredConfigs), null);
        retireDataSource(removed.dataSource());
        log.info("Lifecycle тенанта '{}' удалён", domain);
        return new TenantLifecycleMutationResult(existing, receipt);
    }

    /**
     * Синхронизирует уже персистированный список из mounted Secret. Метод не пишет
     * Secret повторно и использует тот же monitor, что ручные API add/update/remove.
     * Для нового или изменённого домена подключение сначала полностью подготавливается,
     * проверяется и мигрируется, после чего атомарно заменяет прежний runtime-снимок.
     */
    public synchronized void synchronizeFromPersistedConfig(List<TenantConfig> requestedConfigs) {
        if (requestedConfigs == null) {
            throw new IllegalArgumentException("Список конфигураций тенантов обязателен");
        }
        List<TenantConfig> configs = requestedConfigs.stream()
                .map(this::validateAndNormalize)
                .sorted(Comparator.comparing(TenantConfig::getDomain))
                .toList();

        Map<String, TenantConfig> desiredConfigs = new LinkedHashMap<>();
        for (TenantConfig config : configs) {
            if (desiredConfigs.put(config.getDomain(), config) != null) {
                throw new IllegalArgumentException("Конфигурация содержит повторяющиеся домены тенантов");
            }
        }

        Map<String, TenantConfig> currentConfigs = routingDataSource.snapshotTenantConfigs();
        readinessRegistry.replaceDesired(configs);

        for (TenantConfig config : configs) {
            TenantConfig currentConfig = currentConfigs.get(config.getDomain());
            if (currentConfig == null) {
                log.info("Активируем нового тенанта '{}' из persisted-конфигурации", config.getDomain());
                activatePersistedConfig(config);
            } else if (!sameConfig(currentConfig, config)) {
                log.info("Заменяем подключение тенанта '{}' из persisted-конфигурации", config.getDomain());
                activatePersistedConfig(config);
            } else {
                recoverPersistedReadinessIfNeeded(config);
            }
        }

        for (String existingDomain : new ArrayList<>(currentConfigs.keySet())) {
            if (!desiredConfigs.containsKey(existingDomain)) {
                log.info("Отключаем тенанта '{}' — его нет в persisted-конфигурации", existingDomain);
                TenantRoutingDataSource.TenantState removed =
                        routingDataSource.removeTenantAtomically(existingDomain);
                retireDataSource(removed == null ? null : removed.dataSource());
            }
        }

        readinessRegistry.clearConfigurationFailure();
    }

    private void activatePersistedConfig(TenantConfig config) {
        HikariDataSource candidate = null;
        boolean activated = false;
        TenantRoutingDataSource.TenantState previous;
        readinessRegistry.markMigrationStarted(config);
        try {
            candidate = prepareCandidate(config);
            verifyConnection(candidate);
            migrateCandidate(candidate);
            previous = routingDataSource.swapTenant(config, candidate);
            activated = true;
        } catch (RuntimeException activationFailure) {
            readinessRegistry.markMigrationFailed(config);
            throw activationFailure;
        } finally {
            if (!activated) {
                closeCandidateDataSource(candidate);
            }
        }

        // После swap новый pool уже является рабочим. Ошибка публикации readiness не должна
        // ошибочно помечать успешно мигрированный pool как FAILED: следующий retry восстановит
        // состояние по активному подключению без повторной замены DataSource.
        retireDataSource(previous == null ? null : previous.dataSource());
        readinessRegistry.markMigrationSucceeded(config);
        readinessRegistry.markConnectivity(config.getDomain(), true);
    }

    private void recoverPersistedReadinessIfNeeded(TenantConfig config) {
        TenantReadinessRegistry.TenantStatus status =
                readinessRegistry.snapshot().get(config.getDomain());
        if (status == null
                || status.migrationState() == TenantReadinessRegistry.MigrationState.SUCCEEDED) {
            return;
        }
        if (!routingDataSource.testConnection(config.getDomain())) {
            throw new TenantLifecycleException(
                    "Активное подключение тенанта не прошло проверку после предыдущей синхронизации"
            );
        }
        readinessRegistry.markMigrationSucceeded(config);
        readinessRegistry.markConnectivity(config.getDomain(), true);
    }

    private HikariDataSource prepareCandidate(TenantConfig config) {
        try {
            return routingDataSource.prepareTenantDataSource(config);
        } catch (RuntimeException preparationFailure) {
            logTechnicalFailure("Не удалось создать временный pool тенанта", preparationFailure);
            throw new TenantLifecycleException("Не удалось подготовить подключение к базе данных тенанта",
                    preparationFailure);
        }
    }

    private void verifyConnection(HikariDataSource candidate) {
        try (Connection connection = candidate.getConnection()) {
            if (!connection.isValid(5)) {
                throw new TenantLifecycleException("База данных тенанта не подтвердила готовность подключения");
            }
        } catch (TenantLifecycleException e) {
            throw e;
        } catch (Exception connectionFailure) {
            logTechnicalFailure("Не удалось проверить временное подключение тенанта", connectionFailure);
            throw new TenantLifecycleException("Не удалось подключиться к базе данных тенанта", connectionFailure);
        }
    }

    private void migrateCandidate(DataSource candidate) {
        try {
            migrationService.migrate(candidate);
        } catch (RuntimeException migrationFailure) {
            logTechnicalFailure("Миграции временной tenant-БД завершились ошибкой", migrationFailure);
            throw new TenantLifecycleException("Не удалось выполнить миграции базы данных тенанта", migrationFailure);
        }
    }

    private TenantSecretUpdateReceipt persistUpsert(TenantConfig config) {
        try {
            return requireReceipt(tenantConfigStore.upsertTenant(copyConfig(config)));
        } catch (TenantConfigPersistenceException persistenceFailure) {
            logTechnicalFailure("Персистенция tenant Secret завершилась ошибкой", persistenceFailure);
            throw new TenantLifecycleException(persistenceFailure.getMessage(), persistenceFailure);
        } catch (RuntimeException persistenceFailure) {
            logTechnicalFailure("Персистенция tenant Secret завершилась ошибкой", persistenceFailure);
            throw new TenantLifecycleException("Не удалось сохранить конфигурацию тенантов", persistenceFailure);
        }
    }

    private TenantSecretUpdateReceipt persistRemove(String domain) {
        try {
            return requireReceipt(tenantConfigStore.removeTenant(domain));
        } catch (TenantConfigPersistenceException persistenceFailure) {
            logTechnicalFailure("Персистенция tenant Secret завершилась ошибкой", persistenceFailure);
            throw new TenantLifecycleException(persistenceFailure.getMessage(), persistenceFailure);
        } catch (RuntimeException persistenceFailure) {
            logTechnicalFailure("Персистенция tenant Secret завершилась ошибкой", persistenceFailure);
            throw new TenantLifecycleException("Не удалось сохранить конфигурацию тенантов", persistenceFailure);
        }
    }

    private TenantSecretUpdateReceipt requireReceipt(TenantSecretUpdateReceipt receipt) {
        if (receipt == null) {
            throw new TenantConfigPersistenceException("Хранилище конфигурации тенантов не подтвердило операцию");
        }
        return receipt;
    }

    private TenantLifecycleException activationFailureAfterCompensation(TenantSecretUpdateReceipt receipt,
                                                                         RuntimeException activationFailure) {
        TenantSecretCompensationResult compensation = compensate(receipt);
        logTechnicalFailure("Активация подготовленного tenant pool завершилась ошибкой", activationFailure);
        String message = compensationMessage(
                "Не удалось активировать подключение тенанта",
                compensation
        );
        return new TenantLifecycleException(message, activationFailure);
    }

    private TenantLifecycleException removalFailureAfterCompensation(TenantSecretUpdateReceipt receipt,
                                                                      RuntimeException removalFailure) {
        TenantSecretCompensationResult compensation = compensate(receipt);
        logTechnicalFailure("Локальное удаление тенанта завершилось ошибкой", removalFailure);
        String message = compensationMessage("Не удалось отключить тенанта", compensation);
        return new TenantLifecycleException(message, removalFailure);
    }

    private TenantSecretCompensationResult compensate(TenantSecretUpdateReceipt receipt) {
        try {
            TenantSecretCompensationResult result = tenantConfigStore.compensate(receipt);
            if (result == null) {
                readinessRegistry.markConfigurationFailure();
                log.error("Хранилище конфигурации не вернуло результат компенсации tenant Secret");
                return null;
            }
            if (result == TenantSecretCompensationResult.RESTORED && receipt.persisted()) {
                readinessRegistry.clearConfigurationFailure();
                readinessRegistry.replaceDesired(receipt.previousTenants());
            }
            if (result == TenantSecretCompensationResult.NOT_REQUIRED && receipt.persisted()) {
                readinessRegistry.replaceDesired(receipt.committedTenants());
            }
            if (result == TenantSecretCompensationResult.SKIPPED_CONCURRENT_CHANGE
                    || (result == TenantSecretCompensationResult.NOT_REQUIRED && receipt.persisted())) {
                readinessRegistry.markConfigurationFailure();
            }
            return result;
        } catch (RuntimeException compensationFailure) {
            readinessRegistry.markConfigurationFailure();
            logTechnicalFailure("Компенсирующее восстановление tenant Secret завершилось ошибкой",
                    compensationFailure);
            return null;
        }
    }

    private String compensationMessage(String prefix, TenantSecretCompensationResult result) {
        if (result == TenantSecretCompensationResult.RESTORED) {
            return prefix + "; сохранённая конфигурация восстановлена";
        }
        if (result == TenantSecretCompensationResult.NOT_REQUIRED) {
            return prefix + "; tenant Secret не требовал восстановления";
        }
        if (result == TenantSecretCompensationResult.SKIPPED_CONCURRENT_CHANGE) {
            return prefix + "; tenant Secret уже изменён другим экземпляром, автоматический откат пропущен";
        }
        return prefix + "; требуется проверить tenant Secret";
    }

    private void publishSuccessfulMutation(TenantSecretUpdateReceipt receipt,
                                           List<TenantConfig> localDesired,
                                           TenantConfig migratedConfig) {
        try {
            List<TenantConfig> canonicalDesired = receipt.persisted()
                    ? receipt.committedTenants()
                    : localDesired.stream()
                            .filter(config -> !isH2Placeholder(config))
                            .toList();
            if (receipt.persisted()) {
                readinessRegistry.clearConfigurationFailure();
            }
            readinessRegistry.replaceDesired(canonicalDesired);
            if (migratedConfig != null) {
                readinessRegistry.markMigrationSucceeded(migratedConfig);
                readinessRegistry.markConnectivity(migratedConfig.getDomain(), true);
            }
        } catch (RuntimeException healthFailure) {
            readinessRegistry.markConfigurationFailure();
            logTechnicalFailure("Не удалось обновить состояние readiness после tenant lifecycle", healthFailure);
        }
    }

    private List<TenantConfig> sortedConfigs(Map<String, TenantConfig> configs) {
        List<TenantConfig> sorted = new ArrayList<>();
        configs.values().stream()
                .map(this::copyConfig)
                .sorted(Comparator.comparing(TenantConfig::getDomain))
                .forEach(sorted::add);
        return sorted;
    }

    private boolean isH2Placeholder(TenantConfig config) {
        return config != null
                && "H2 Placeholder".equals(config.getName())
                && config.getUrl() != null
                && config.getUrl().trim().startsWith("jdbc:h2:mem:placeholder");
    }

    private TenantConfig validateAndNormalize(TenantConfig config) {
        if (config == null) {
            throw new IllegalArgumentException("Конфигурация тенанта обязательна");
        }
        String domain = validateAndNormalizeDomain(config.getDomain());
        if (config.getUrl() == null || config.getUrl().isBlank()) {
            throw new IllegalArgumentException("URL базы данных не может быть пустым");
        }
        String url = config.getUrl().trim();
        if (!url.startsWith("jdbc:")) {
            throw new IllegalArgumentException("URL базы данных должен начинаться с jdbc:");
        }
        String name = config.getName() == null || config.getName().isBlank()
                ? domain
                : config.getName().trim();
        return new TenantConfig(name, domain, url, config.getUsername(), config.getPassword());
    }

    private String validateAndNormalizeDomain(String domain) {
        if (domain == null || domain.isBlank()) {
            throw new IllegalArgumentException("Домен не может быть пустым");
        }
        String normalized = domain.trim().toLowerCase(Locale.ROOT);
        if (!DOMAIN_PATTERN.matcher(normalized).matches()) {
            throw new IllegalArgumentException("Домен содержит недопустимые символы");
        }
        return normalized;
    }

    private TenantConfig copyConfig(TenantConfig config) {
        return new TenantConfig(
                config.getName(),
                config.getDomain(),
                config.getUrl(),
                config.getUsername(),
                config.getPassword()
        );
    }

    private boolean sameConfig(TenantConfig first, TenantConfig second) {
        TenantConfig normalizedFirst = validateAndNormalize(first);
        TenantConfig normalizedSecond = validateAndNormalize(second);
        return Objects.equals(normalizedFirst.getName(), normalizedSecond.getName())
                && Objects.equals(normalizedFirst.getDomain(), normalizedSecond.getDomain())
                && Objects.equals(normalizedFirst.getUrl(), normalizedSecond.getUrl())
                && Objects.equals(normalizedFirst.getUsername(), normalizedSecond.getUsername())
                && Objects.equals(normalizedFirst.getPassword(), normalizedSecond.getPassword());
    }

    private void retireDataSource(DataSource dataSource) {
        if (dataSource != null) {
            try {
                retiredPoolService.retire(dataSource);
            } catch (RuntimeException retirementFailure) {
                logTechnicalFailure("Не удалось передать прежний tenant pool на отложенное закрытие",
                        retirementFailure);
            }
        }
    }

    private void closeCandidateDataSource(DataSource dataSource) {
        if (dataSource instanceof HikariDataSource hikariDataSource) {
            try {
                hikariDataSource.close();
            } catch (RuntimeException closeFailure) {
                logTechnicalFailure("Не удалось штатно закрыть Hikari pool", closeFailure);
            }
        }
    }

    private void logTechnicalFailure(String message, Throwable failure) {
        log.error("{}: errorType={}", message, failure.getClass().getSimpleName());
        log.debug("Технические детали tenant lifecycle", failure);
    }
}
