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

import com.magistr.app.config.tenant.TenantConfig;
import org.springframework.stereotype.Component;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.UnaryOperator;
import java.util.regex.Pattern;

/**
 * Потокобезопасный реестр готовности обязательных tenant-БД.
 *
 * <p>Каждое изменение публикуется единым неизменяемым снимком. Результат фоновой
 * проверки соединения привязан к непрозрачному fingerprint конфигурации, поэтому
 * запоздалый результат для старых реквизитов не изменит состояние новой конфигурации.</p>
 */
@Component
public class TenantReadinessRegistry {

    private static final String FINGERPRINT_ALGORITHM = "HmacSHA256";
    private static final Pattern DOMAIN_PATTERN = Pattern.compile(
            "[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?"
    );

    private final AtomicReference<RegistryState> state = new AtomicReference<>(RegistryState.empty());
    private final Clock clock;
    private final byte[] fingerprintKey;

    public TenantReadinessRegistry() {
        this(Clock.systemUTC(), randomFingerprintKey());
    }

    TenantReadinessRegistry(Clock clock, byte[] fingerprintKey) {
        this.clock = Objects.requireNonNull(clock, "Часы реестра готовности обязательны");
        if (fingerprintKey == null || fingerprintKey.length < 16) {
            throw new IllegalArgumentException("Ключ fingerprint должен содержать не менее 16 байт");
        }
        this.fingerprintKey = fingerprintKey.clone();
    }

    /**
     * Полностью заменяет список обязательных конфигураций.
     * Состояние неизменившегося tenant сохраняется, изменившегося — сбрасывается.
     */
    public void replaceDesired(List<TenantConfig> desired) {
        boolean missingDesired = desired == null;
        List<TenantConfig> safeDesired = desired == null ? List.of() : new ArrayList<>(desired);
        mutate(current -> {
            Map<String, TenantStatus> replacement = new LinkedHashMap<>();
            boolean invalidConfiguration = missingDesired;

            for (TenantConfig config : safeDesired) {
                NormalizedConfig normalized = normalize(config);
                if (normalized == null || replacement.containsKey(normalized.domain())) {
                    invalidConfiguration = true;
                    continue;
                }

                String fingerprint = fingerprint(normalized);
                TenantStatus previous = current.tenants().get(normalized.domain());
                if (previous != null && previous.fingerprint().equals(fingerprint)) {
                    replacement.put(normalized.domain(), previous);
                } else {
                    replacement.put(normalized.domain(), TenantStatus.pending(
                            normalized.domain(), fingerprint, normalized.h2Placeholder(), clock.instant()));
                }
            }

            return new RegistryState(replacement,
                    current.configurationFailure() || invalidConfiguration);
        });
    }

    /** Удаляет tenant из обязательного набора. */
    public void removeDesired(String domain) {
        String normalizedDomain = normalizeDomain(domain);
        mutate(current -> {
            if (!current.tenants().containsKey(normalizedDomain)) {
                return current;
            }
            Map<String, TenantStatus> updated = new LinkedHashMap<>(current.tenants());
            updated.remove(normalizedDomain);
            return new RegistryState(updated, current.configurationFailure());
        });
    }

    public void markMigrationStarted(TenantConfig config) {
        updateByConfig(config, MigrationState.IN_PROGRESS, ConnectivityState.UNKNOWN);
    }

    public void markMigrationSucceeded(TenantConfig config) {
        updateByConfig(config, MigrationState.SUCCEEDED, ConnectivityState.UNKNOWN);
    }

    public void markMigrationFailed(TenantConfig config) {
        updateByConfig(config, MigrationState.FAILED, ConnectivityState.UNKNOWN);
    }

    public void markMigrationStarted(String domain) {
        updateExisting(domain, MigrationState.IN_PROGRESS, ConnectivityState.UNKNOWN);
    }

    public void markMigrationSucceeded(String domain) {
        updateExisting(domain, MigrationState.SUCCEEDED, ConnectivityState.UNKNOWN);
    }

    public void markMigrationFailed(String domain) {
        updateExisting(domain, MigrationState.FAILED, ConnectivityState.UNKNOWN);
    }

    /**
     * Сохраняет результат проверки текущей конфигурации tenant.
     */
    public boolean markConnectivity(String domain, boolean connected) {
        String normalizedDomain = normalizeDomain(domain);
        TenantStatus current = state.get().tenants().get(normalizedDomain);
        if (current == null) {
            return false;
        }
        return markConnectivity(normalizedDomain, current.fingerprint(), connected, clock.instant());
    }

    /**
     * Сохраняет результат только если fingerprint всё ещё соответствует проверенной конфигурации.
     */
    public boolean markConnectivity(String domain,
                                    String expectedFingerprint,
                                    boolean connected,
                                    Instant checkedAt) {
        String normalizedDomain = normalizeDomain(domain);
        Objects.requireNonNull(expectedFingerprint, "Fingerprint проверяемой конфигурации обязателен");
        Objects.requireNonNull(checkedAt, "Время проверки соединения обязательно");

        AtomicReference<Boolean> applied = new AtomicReference<>(false);
        mutate(current -> {
            applied.set(false);
            TenantStatus previous = current.tenants().get(normalizedDomain);
            if (previous == null || !previous.fingerprint().equals(expectedFingerprint)
                    || previous.migrationState() != MigrationState.SUCCEEDED
                    || previous.h2Placeholder()) {
                return current;
            }

            if (previous.connectivityCheckedAt() != null
                    && checkedAt.isBefore(previous.connectivityCheckedAt())) {
                return current;
            }

            Map<String, TenantStatus> updated = new LinkedHashMap<>(current.tenants());
            updated.put(normalizedDomain, previous.withConnectivity(
                    connected ? ConnectivityState.AVAILABLE : ConnectivityState.UNAVAILABLE,
                    checkedAt,
                    clock.instant()));
            applied.set(true);
            return new RegistryState(updated, current.configurationFailure());
        });
        return applied.get();
    }

    /** Отмечает ошибку чтения или разбора общей конфигурации без сохранения её текста. */
    public void markConfigurationFailure() {
        mutate(current -> new RegistryState(current.tenants(), true));
    }

    public void clearConfigurationFailure() {
        mutate(current -> new RegistryState(current.tenants(), false));
    }

    /** Возвращает безопасные задания для фоновой проверки соединений. */
    public List<ConnectivityTarget> connectivityTargets() {
        List<ConnectivityTarget> targets = new ArrayList<>();
        state.get().tenants().values().stream()
                .filter(status -> status.migrationState() == MigrationState.SUCCEEDED)
                .filter(status -> !status.h2Placeholder())
                .forEach(status -> targets.add(new ConnectivityTarget(status.domain(), status.fingerprint())));
        return List.copyOf(targets);
    }

    /** Формирует агрегированную готовность на текущий момент. */
    public AggregateReadiness aggregate(Duration maxStaleness) {
        Objects.requireNonNull(maxStaleness, "Максимальный возраст проверки обязателен");
        if (maxStaleness.isNegative() || maxStaleness.isZero()) {
            throw new IllegalArgumentException("Максимальный возраст проверки должен быть положительным");
        }

        RegistryState snapshot = state.get();
        Instant assessedAt = clock.instant();
        Instant oldestAllowed = assessedAt.minus(maxStaleness);
        int ready = 0;
        int migrationFailures = 0;
        int migrationPending = 0;
        int staleConnectivity = 0;
        int unavailableConnectivity = 0;
        int unsupported = 0;

        for (TenantStatus status : snapshot.tenants().values()) {
            if (status.h2Placeholder()) {
                unsupported++;
                continue;
            }
            if (status.migrationState() == MigrationState.FAILED) {
                migrationFailures++;
                continue;
            }
            if (status.migrationState() != MigrationState.SUCCEEDED) {
                migrationPending++;
                continue;
            }
            if (status.connectivityCheckedAt() == null
                    || status.connectivityCheckedAt().isBefore(oldestAllowed)) {
                staleConnectivity++;
                continue;
            }
            if (status.connectivityState() != ConnectivityState.AVAILABLE) {
                unavailableConnectivity++;
                continue;
            }
            ready++;
        }

        int desiredCount = snapshot.tenants().size();
        boolean isReady = !snapshot.configurationFailure()
                && desiredCount > 0
                && unsupported == 0
                && ready == desiredCount;
        return new AggregateReadiness(
                isReady,
                isReady ? "ГОТОВ" : "НЕ ГОТОВ",
                desiredCount,
                ready,
                desiredCount - ready,
                migrationPending,
                migrationFailures,
                unavailableConnectivity,
                staleConnectivity,
                unsupported,
                snapshot.configurationFailure(),
                assessedAt
        );
    }

    /** Неизменяемый диагностический снимок для внутренних тестов и интеграции. */
    public Map<String, TenantStatus> snapshot() {
        return state.get().tenants();
    }

    private void updateByConfig(TenantConfig config,
                                MigrationState migrationState,
                                ConnectivityState connectivityState) {
        NormalizedConfig normalized = normalize(config);
        if (normalized == null) {
            markConfigurationFailure();
            return;
        }
        String fingerprint = fingerprint(normalized);
        mutate(current -> {
            TenantStatus previous = current.tenants().get(normalized.domain());
            TenantStatus base = previous != null && previous.fingerprint().equals(fingerprint)
                    ? previous
                    : TenantStatus.pending(normalized.domain(), fingerprint, normalized.h2Placeholder(), clock.instant());
            Map<String, TenantStatus> updated = new LinkedHashMap<>(current.tenants());
            updated.put(normalized.domain(), base.withMigration(migrationState, connectivityState, clock.instant()));
            return new RegistryState(updated, current.configurationFailure());
        });
    }

    private void updateExisting(String domain,
                                MigrationState migrationState,
                                ConnectivityState connectivityState) {
        String normalizedDomain = normalizeDomain(domain);
        mutate(current -> {
            TenantStatus previous = current.tenants().get(normalizedDomain);
            if (previous == null) {
                return current;
            }
            Map<String, TenantStatus> updated = new LinkedHashMap<>(current.tenants());
            updated.put(normalizedDomain, previous.withMigration(migrationState, connectivityState, clock.instant()));
            return new RegistryState(updated, current.configurationFailure());
        });
    }

    private void mutate(UnaryOperator<RegistryState> mutation) {
        RegistryState current;
        RegistryState updated;
        do {
            current = state.get();
            updated = Objects.requireNonNull(mutation.apply(current), "Новое состояние реестра обязательно");
            if (updated == current) {
                return;
            }
        } while (!state.compareAndSet(current, updated));
    }

    private NormalizedConfig normalize(TenantConfig config) {
        if (config == null || config.getDomain() == null || config.getDomain().isBlank()
                || config.getUrl() == null || config.getUrl().isBlank()) {
            return null;
        }
        String domain = config.getDomain().trim().toLowerCase(Locale.ROOT);
        String url = config.getUrl().trim();
        if (!DOMAIN_PATTERN.matcher(domain).matches()) {
            return null;
        }
        String name = config.getName() == null || config.getName().isBlank()
                ? domain
                : config.getName().trim();
        return new NormalizedConfig(
                name,
                domain,
                url,
                config.getUsername(),
                config.getPassword(),
                url.startsWith("jdbc:h2:")
        );
    }

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

    private String fingerprint(NormalizedConfig normalized) {
        try {
            Mac mac = Mac.getInstance(FINGERPRINT_ALGORITHM);
            mac.init(new SecretKeySpec(fingerprintKey, FINGERPRINT_ALGORITHM));
            updateFingerprint(mac, normalized.name());
            updateFingerprint(mac, normalized.domain());
            updateFingerprint(mac, normalized.url());
            updateFingerprint(mac, normalized.username());
            updateFingerprint(mac, normalized.password());
            return Base64.getUrlEncoder().withoutPadding().encodeToString(mac.doFinal());
        } catch (GeneralSecurityException cryptoFailure) {
            throw new IllegalStateException("Не удалось сформировать безопасный fingerprint конфигурации", cryptoFailure);
        }
    }

    private void updateFingerprint(Mac mac, String value) {
        byte[] bytes = value == null ? new byte[0] : value.getBytes(StandardCharsets.UTF_8);
        mac.update(ByteBuffer.allocate(Integer.BYTES).putInt(bytes.length).array());
        mac.update(bytes);
    }

    private static byte[] randomFingerprintKey() {
        byte[] key = new byte[32];
        new SecureRandom().nextBytes(key);
        return key;
    }

    public enum MigrationState {
        PENDING,
        IN_PROGRESS,
        SUCCEEDED,
        FAILED
    }

    public enum ConnectivityState {
        UNKNOWN,
        AVAILABLE,
        UNAVAILABLE
    }

    public record ConnectivityTarget(String domain, String fingerprint) {
        public ConnectivityTarget {
            Objects.requireNonNull(domain, "Домен проверки обязателен");
            Objects.requireNonNull(fingerprint, "Fingerprint проверки обязателен");
        }
    }

    public record TenantStatus(String domain,
                               String fingerprint,
                               MigrationState migrationState,
                               ConnectivityState connectivityState,
                               Instant connectivityCheckedAt,
                               boolean h2Placeholder,
                               Instant changedAt) {
        public TenantStatus {
            Objects.requireNonNull(domain, "Домен состояния обязателен");
            Objects.requireNonNull(fingerprint, "Fingerprint состояния обязателен");
            Objects.requireNonNull(migrationState, "Состояние миграции обязательно");
            Objects.requireNonNull(connectivityState, "Состояние соединения обязательно");
            Objects.requireNonNull(changedAt, "Время изменения состояния обязательно");
        }

        private static TenantStatus pending(String domain,
                                            String fingerprint,
                                            boolean h2Placeholder,
                                            Instant changedAt) {
            return new TenantStatus(domain, fingerprint, MigrationState.PENDING,
                    ConnectivityState.UNKNOWN, null, h2Placeholder, changedAt);
        }

        private TenantStatus withMigration(MigrationState newMigrationState,
                                           ConnectivityState newConnectivityState,
                                           Instant now) {
            return new TenantStatus(domain, fingerprint, newMigrationState,
                    newConnectivityState, null, h2Placeholder, now);
        }

        private TenantStatus withConnectivity(ConnectivityState newConnectivityState,
                                              Instant checkedAt,
                                              Instant now) {
            return new TenantStatus(domain, fingerprint, migrationState,
                    newConnectivityState, checkedAt, h2Placeholder, now);
        }
    }

    public record AggregateReadiness(boolean ready,
                                     String status,
                                     int desiredTenants,
                                     int readyTenants,
                                     int unavailableTenants,
                                     int pendingMigrations,
                                     int failedMigrations,
                                     int unavailableConnections,
                                     int staleConnections,
                                     int unsupportedTenants,
                                     boolean configurationFailure,
                                     Instant assessedAt) {
        public AggregateReadiness {
            Objects.requireNonNull(status, "Текст готовности обязателен");
            Objects.requireNonNull(assessedAt, "Время оценки готовности обязательно");
        }
    }

    private record NormalizedConfig(String name,
                                    String domain,
                                    String url,
                                    String username,
                                    String password,
                                    boolean h2Placeholder) {
    }

    private record RegistryState(Map<String, TenantStatus> tenants, boolean configurationFailure) {
        private RegistryState {
            tenants = Collections.unmodifiableMap(new LinkedHashMap<>(tenants));
        }

        private static RegistryState empty() {
            return new RegistryState(Map.of(), false);
        }
    }
}
