package com.magistr.app.service;

import com.zaxxer.hikari.HikariDataSource;
import com.zaxxer.hikari.HikariPoolMXBean;
import jakarta.annotation.PreDestroy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import javax.sql.DataSource;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Отложенно закрывает пулы, исключённые из актуального снимка тенантов.
 * Это даёт уже начатым запросам ограниченное время на завершение.
 */
@Service
public class RetiredTenantPoolService {

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

    private final long graceTimeoutNanos;
    private final long pollIntervalMillis;
    private final ScheduledExecutorService scheduler;
    private final Map<HikariDataSource, RetiredPool> retiredPools = new ConcurrentHashMap<>();
    private final AtomicBoolean shuttingDown = new AtomicBoolean(false);

    public RetiredTenantPoolService(
            @Value("${app.tenants.pool-retirement.grace-timeout:30s}") Duration graceTimeout,
            @Value("${app.tenants.pool-retirement.poll-interval:250ms}") Duration pollInterval) {
        if (graceTimeout.isNegative()) {
            throw new IllegalArgumentException("Период ожидания закрытия пула не может быть отрицательным");
        }
        if (pollInterval.isZero() || pollInterval.isNegative()) {
            throw new IllegalArgumentException("Интервал проверки закрытия пула должен быть положительным");
        }

        this.graceTimeoutNanos = graceTimeout.toNanos();
        this.pollIntervalMillis = Math.max(1L, pollInterval.toMillis());
        this.scheduler = Executors.newSingleThreadScheduledExecutor(task -> {
            Thread thread = new Thread(task, "retired-tenant-pool-drain");
            thread.setDaemon(true);
            return thread;
        });
        this.scheduler.scheduleWithFixedDelay(
                this::pollRetiredPools,
                this.pollIntervalMillis,
                this.pollIntervalMillis,
                TimeUnit.MILLISECONDS
        );
    }

    /**
     * Передаёт выведенный из эксплуатации Hikari-пул на асинхронное закрытие.
     *
     * @param dataSource источник данных, ранее удалённый из активного снимка маршрутизации
     */
    public void retire(DataSource dataSource) {
        if (!(dataSource instanceof HikariDataSource hikariDataSource)) {
            log.warn("Источник данных нельзя передать на отложенное закрытие: поддерживаются только Hikari-пулы");
            return;
        }
        if (hikariDataSource.isClosed()) {
            return;
        }
        if (shuttingDown.get()) {
            closePool(hikariDataSource, "остановка приложения");
            return;
        }

        long deadlineNanos = System.nanoTime() + graceTimeoutNanos;
        RetiredPool previous = retiredPools.putIfAbsent(
                hikariDataSource,
                new RetiredPool(deadlineNanos)
        );
        if (previous == null) {
            log.info("Пул подключений передан на отложенное закрытие: pool={}",
                    hikariDataSource.getPoolName());
        }
    }

    private void pollRetiredPools() {
        if (shuttingDown.get()) {
            return;
        }

        long nowNanos = System.nanoTime();
        retiredPools.forEach((pool, retiredPool) -> pollPool(pool, retiredPool, nowNanos));
    }

    private void pollPool(HikariDataSource pool, RetiredPool retiredPool, long nowNanos) {
        if (pool.isClosed()) {
            retiredPools.remove(pool, retiredPool);
            return;
        }

        boolean graceExpired = nowNanos - retiredPool.deadlineNanos() >= 0;
        boolean drained = hasNoActiveConnections(pool);
        if ((drained || graceExpired) && retiredPools.remove(pool, retiredPool)) {
            closePool(pool, drained ? "активные подключения завершены" : "истёк период ожидания");
        }
    }

    private boolean hasNoActiveConnections(HikariDataSource pool) {
        try {
            HikariPoolMXBean poolMxBean = pool.getHikariPoolMXBean();
            return poolMxBean != null && poolMxBean.getActiveConnections() == 0;
        } catch (RuntimeException exception) {
            log.warn("Не удалось проверить активные подключения выведенного пула: pool={}, errorType={}",
                    pool.getPoolName(), exception.getClass().getSimpleName());
            return false;
        }
    }

    private void closePool(HikariDataSource pool, String reason) {
        try {
            pool.close();
            log.info("Выведенный пул подключений закрыт: pool={}, reason={}", pool.getPoolName(), reason);
        } catch (RuntimeException exception) {
            log.error("Не удалось закрыть выведенный пул подключений: pool={}, errorType={}",
                    pool.getPoolName(), exception.getClass().getSimpleName());
        }
    }

    @PreDestroy
    public void shutdown() {
        if (!shuttingDown.compareAndSet(false, true)) {
            return;
        }

        scheduler.shutdownNow();
        retiredPools.forEach((pool, retiredPool) -> {
            if (retiredPools.remove(pool, retiredPool)) {
                closePool(pool, "остановка приложения");
            }
        });
    }

    private record RetiredPool(long deadlineNanos) {
    }
}
