package com.magistr.app.config.tenant.health;

import com.magistr.app.config.tenant.TenantRoutingDataSource;
import jakarta.annotation.PreDestroy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Фоновая проверка доступности tenant-БД с ограниченным числом параллельных задач.
 */
@Component
public class TenantDatabaseHealthMonitor {

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

    private final TenantReadinessRegistry registry;
    private final TenantRoutingDataSource routingDataSource;
    private final int parallelism;
    private final Duration checkTimeout;
    private final Duration maxStaleness;
    private final Clock clock;
    private final ExecutorService executor;
    private final AtomicBoolean refreshInProgress = new AtomicBoolean(false);

    @Autowired
    public TenantDatabaseHealthMonitor(
            TenantReadinessRegistry registry,
            TenantRoutingDataSource routingDataSource,
            @Value("${app.health.tenant-database.parallelism:4}") int parallelism,
            @Value("${app.health.tenant-database.check-timeout-ms:6000}") long checkTimeoutMillis,
            @Value("${app.health.tenant-database.max-staleness-ms:30000}") long maxStalenessMillis) {
        this(registry, routingDataSource, parallelism,
                Duration.ofMillis(checkTimeoutMillis),
                Duration.ofMillis(maxStalenessMillis),
                Clock.systemUTC());
    }

    public TenantDatabaseHealthMonitor(TenantReadinessRegistry registry,
                                       TenantRoutingDataSource routingDataSource,
                                       int parallelism,
                                       Duration checkTimeout,
                                       Duration maxStaleness,
                                       Clock clock) {
        this.registry = Objects.requireNonNull(registry, "Реестр готовности обязателен");
        this.routingDataSource = Objects.requireNonNull(routingDataSource, "Маршрутизатор tenant-БД обязателен");
        if (parallelism < 1 || parallelism > 32) {
            throw new IllegalArgumentException("Параллелизм проверки должен быть от 1 до 32");
        }
        this.parallelism = parallelism;
        this.checkTimeout = requirePositive(checkTimeout, "Таймаут проверки должен быть положительным");
        this.maxStaleness = requirePositive(maxStaleness, "Максимальный возраст проверки должен быть положительным");
        this.clock = Objects.requireNonNull(clock, "Часы монитора обязательны");
        this.executor = Executors.newFixedThreadPool(parallelism, new HealthThreadFactory());
    }

    @Scheduled(
            fixedDelayString = "${app.health.tenant-database.interval-ms:10000}",
            initialDelayString = "${app.health.tenant-database.initial-delay-ms:1000}")
    public void refreshScheduled() {
        refreshNow();
    }

    /**
     * Выполняет немедленный проход. Одновременно может выполняться только один проход.
     */
    public RefreshSummary refreshNow() {
        if (!refreshInProgress.compareAndSet(false, true)) {
            return new RefreshSummary(0, 0, 0, true);
        }

        try {
            List<TenantReadinessRegistry.ConnectivityTarget> targets = registry.connectivityTargets();
            int available = 0;
            int unavailable = 0;

            for (int offset = 0; offset < targets.size(); offset += parallelism) {
                int end = Math.min(offset + parallelism, targets.size());
                List<TenantReadinessRegistry.ConnectivityTarget> batch = targets.subList(offset, end);
                List<Callable<CheckResult>> checks = new ArrayList<>(batch.size());

                for (TenantReadinessRegistry.ConnectivityTarget target : batch) {
                    checks.add(() -> check(target));
                }

                List<Future<CheckResult>> futures;
                try {
                    futures = executor.invokeAll(checks, checkTimeout.toNanos(), TimeUnit.NANOSECONDS);
                } catch (InterruptedException interrupted) {
                    Thread.currentThread().interrupt();
                    Instant interruptedAt = clock.instant();
                    log.warn("Пакетная проверка tenant-БД была прервана");
                    for (int remaining = offset; remaining < targets.size(); remaining++) {
                        TenantReadinessRegistry.ConnectivityTarget target = targets.get(remaining);
                        if (registry.markConnectivity(
                                target.domain(), target.fingerprint(), false, interruptedAt)) {
                            unavailable++;
                        }
                    }
                    return new RefreshSummary(targets.size(), available, unavailable, false);
                }

                for (int index = 0; index < batch.size(); index++) {
                    TenantReadinessRegistry.ConnectivityTarget target = batch.get(index);
                    CheckResult result = completedResult(target, futures.get(index));
                    boolean applied = registry.markConnectivity(
                            target.domain(), target.fingerprint(), result.connected(), result.checkedAt());
                    if (applied && result.connected()) {
                        available++;
                    } else if (applied) {
                        unavailable++;
                    }
                }
            }

            return new RefreshSummary(targets.size(), available, unavailable, false);
        } finally {
            refreshInProgress.set(false);
        }
    }

    public Duration maxStaleness() {
        return maxStaleness;
    }

    public int parallelism() {
        return parallelism;
    }

    @PreDestroy
    public void close() {
        executor.shutdownNow();
    }

    private CheckResult check(TenantReadinessRegistry.ConnectivityTarget target) {
        try {
            boolean connected = routingDataSource.testConnection(target.domain());
            return new CheckResult(connected, clock.instant());
        } catch (RuntimeException checkFailure) {
            log.warn("Проверка tenant-БД '{}' завершилась ошибкой: errorType={}",
                    target.domain(), checkFailure.getClass().getSimpleName());
            log.debug("Технические детали фоновой проверки tenant-БД", checkFailure);
            return new CheckResult(false, clock.instant());
        }
    }

    private CheckResult completedResult(TenantReadinessRegistry.ConnectivityTarget target,
                                        Future<CheckResult> future) {
        if (future.isCancelled()) {
            log.warn("Проверка tenant-БД '{}' превысила допустимое время", target.domain());
            return new CheckResult(false, clock.instant());
        }

        try {
            return future.get();
        } catch (InterruptedException interrupted) {
            Thread.currentThread().interrupt();
            future.cancel(true);
            log.warn("Проверка tenant-БД '{}' была прервана", target.domain());
            return new CheckResult(false, clock.instant());
        } catch (ExecutionException failure) {
            log.warn("Не удалось проверить tenant-БД '{}': errorType={}",
                    target.domain(), failure.getCause() == null
                            ? failure.getClass().getSimpleName()
                            : failure.getCause().getClass().getSimpleName());
            return new CheckResult(false, clock.instant());
        }
    }

    private Duration requirePositive(Duration value, String message) {
        Objects.requireNonNull(value, message);
        if (value.isNegative() || value.isZero()) {
            throw new IllegalArgumentException(message);
        }
        return value;
    }

    public record RefreshSummary(int requested,
                                 int available,
                                 int unavailable,
                                 boolean skipped) {
    }

    private record CheckResult(boolean connected, Instant checkedAt) {
        private CheckResult {
            Objects.requireNonNull(checkedAt, "Время завершения проверки tenant-БД обязательно");
        }
    }

    private static final class HealthThreadFactory implements ThreadFactory {
        private final AtomicInteger sequence = new AtomicInteger();

        @Override
        public Thread newThread(Runnable task) {
            Thread thread = new Thread(task,
                    "проверка-готовности-tenant-бд-" + sequence.incrementAndGet());
            thread.setDaemon(true);
            return thread;
        }
    }
}
