package com.magistr.app.config.tenant;

import com.zaxxer.hikari.HikariDataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

/**
 * DataSource, который переключается между БД разных тенантов.
 * Runtime-состояние публикуется одним неизменяемым snapshot, поэтому запрос
 * никогда не видит конфигурацию и DataSource из разных версий.
 */
public class TenantRoutingDataSource extends AbstractRoutingDataSource {

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

    private final Object lifecycleMonitor = new Object();
    private volatile Map<String, TenantState> tenantStates = Map.of();

    public TenantRoutingDataSource() {
        // Inherited lifecycle Spring требует initial target map. Runtime routing
        // выполняется переопределённым determineTargetDataSource().
        setTargetDataSources(Map.of());
        setLenientFallback(false);
    }

    @Override
    protected Object determineCurrentLookupKey() {
        return determineLookupKey(tenantStates);
    }

    @Override
    protected DataSource determineTargetDataSource() {
        Map<String, TenantState> snapshot = tenantStates;
        String lookupKey = determineLookupKey(snapshot);
        TenantState state = snapshot.get(lookupKey);
        if (state == null || state.dataSource() == null) {
            throw new IllegalStateException("Подключение для выбранного тенанта не настроено");
        }
        return state.dataSource();
    }

    /**
     * Создаёт Hikari pool, но не включает его в маршрутизацию.
     */
    public HikariDataSource prepareTenantDataSource(TenantConfig config) {
        TenantConfig normalized = normalizeConfig(config);
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(normalized.getUrl());
        dataSource.setUsername(normalized.getUsername());
        dataSource.setPassword(normalized.getPassword());
        dataSource.setPoolName("tenant-" + normalized.getDomain() + "-candidate-"
                + Integer.toUnsignedString(System.identityHashCode(dataSource)));
        dataSource.setMaximumPoolSize(10);
        dataSource.setMinimumIdle(2);
        dataSource.setConnectionTimeout(10_000);
        dataSource.setValidationTimeout(5_000);
        dataSource.setIdleTimeout(300_000);
        dataSource.setMaxLifetime(600_000);
        dataSource.setInitializationFailTimeout(10_000);
        return dataSource;
    }

    /**
     * Атомарно публикует подготовленный DataSource и возвращает прежнее состояние.
     */
    public TenantState swapTenant(TenantConfig config, DataSource candidate) {
        Objects.requireNonNull(candidate, "Подготовленный DataSource обязателен");
        TenantConfig normalized = normalizeConfig(config);
        String domain = normalized.getDomain();

        synchronized (lifecycleMonitor) {
            Map<String, TenantState> current = tenantStates;
            TenantState previous = copyState(current.get(domain));
            Map<String, TenantState> updated = new LinkedHashMap<>(current);
            updated.put(domain, new TenantState(domain, normalized, candidate));
            tenantStates = immutableStates(updated);
            return previous;
        }
    }

    /**
     * Атомарно исключает тенанта из маршрутизации, но не закрывает возвращённый DataSource.
     */
    public TenantState removeTenantAtomically(String domain) {
        String normalizedDomain = normalizeDomain(domain);
        synchronized (lifecycleMonitor) {
            Map<String, TenantState> current = tenantStates;
            TenantState previous = current.get(normalizedDomain);
            if (previous == null) {
                return null;
            }
            Map<String, TenantState> updated = new LinkedHashMap<>(current);
            updated.remove(normalizedDomain);
            tenantStates = immutableStates(updated);
            return copyState(previous);
        }
    }

    public Optional<TenantState> snapshotTenant(String domain) {
        if (domain == null || domain.isBlank()) {
            return Optional.empty();
        }
        return Optional.ofNullable(copyState(tenantStates.get(normalizeDomain(domain))));
    }

    public Map<String, TenantConfig> snapshotTenantConfigs() {
        Map<String, TenantState> snapshot = tenantStates;
        Map<String, TenantConfig> configs = new LinkedHashMap<>();
        snapshot.forEach((domain, state) -> configs.put(domain, copyConfig(state.config())));
        return Collections.unmodifiableMap(configs);
    }

    public Optional<DataSource> getTenantDataSource(String domain) {
        if (domain == null || domain.isBlank()) {
            return Optional.empty();
        }
        TenantState state = tenantStates.get(normalizeDomain(domain));
        return state == null ? Optional.empty() : Optional.of(state.dataSource());
    }

    /**
     * Проверяет подключение к БД для зарегистрированного тенанта.
     */
    public boolean testConnection(String domain) {
        Optional<DataSource> tenantDataSource = getTenantDataSource(domain);
        if (tenantDataSource.isEmpty()) {
            return false;
        }

        try (Connection connection = tenantDataSource.get().getConnection()) {
            return connection.isValid(5);
        } catch (SQLException e) {
            log.warn("Проверка подключения тенанта '{}' завершилась ошибкой: типОшибки={}",
                    normalizeDomain(domain), e.getClass().getSimpleName());
            log.debug("Технические детали проверки подключения тенанта", e);
            return false;
        }
    }

    /**
     * Тестирует подключение по произвольным параметрам без регистрации тенанта.
     */
    public String testExternalConnection(String url, String username, String password) {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(url);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        dataSource.setMaximumPoolSize(1);
        dataSource.setConnectionTimeout(5_000);

        try (Connection connection = dataSource.getConnection()) {
            if (connection.isValid(5)) {
                return "OK";
            }
            return "Подключение не валидно";
        } catch (Exception e) {
            log.warn("Проверка внешнего подключения завершилась ошибкой: типОшибки={}",
                    e.getClass().getSimpleName());
            log.debug("Технические детали проверки внешнего подключения", e);
            return "Не удалось подключиться к базе данных";
        } finally {
            dataSource.close();
        }
    }

    /**
     * Возвращает неизменяемый снимок конфигураций для совместимости существующих consumers.
     */
    public Map<String, TenantConfig> getTenantConfigs() {
        return snapshotTenantConfigs();
    }

    public boolean hasTenant(String domain) {
        return domain != null && !domain.isBlank() && tenantStates.containsKey(normalizeDomain(domain));
    }

    public boolean isInitialized() {
        return !tenantStates.isEmpty();
    }

    private String determineLookupKey(Map<String, TenantState> snapshot) {
        String tenant = TenantContext.getCurrentTenant();
        if (tenant != null) {
            return tenant;
        }
        return snapshot.isEmpty() ? "default" : snapshot.keySet().iterator().next();
    }

    private Map<String, TenantState> immutableStates(Map<String, TenantState> source) {
        Map<String, TenantState> copy = new LinkedHashMap<>();
        source.forEach((domain, state) -> copy.put(domain, new TenantState(
                domain,
                copyConfig(state.config()),
                state.dataSource()
        )));
        return Collections.unmodifiableMap(copy);
    }

    private TenantState copyState(TenantState state) {
        if (state == null) {
            return null;
        }
        return new TenantState(state.domain(), copyConfig(state.config()), state.dataSource());
    }

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

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

    private String normalizeDomain(String domain) {
        if (domain == null || domain.isBlank()) {
            throw new IllegalArgumentException("Домен не может быть пустым");
        }
        return domain.trim().toLowerCase(Locale.ROOT);
    }

    public record TenantState(String domain, TenantConfig config, DataSource dataSource) {
    }
}
