package com.magistr.app.config.tenant;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.magistr.app.config.tenant.health.TenantReadinessRegistry;
import com.magistr.app.service.TenantLifecycleException;
import com.magistr.app.service.TenantLifecycleService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HexFormat;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.function.LongSupplier;

/**
 * Периодически применяет конфигурацию тенантов из смонтированного tenants.json.
 * Все чтения и lifecycle-изменения сериализуются одним monitor'ом сервиса.
 */
@Component
public class TenantConfigWatcher {

    private static final Logger log = LoggerFactory.getLogger(TenantConfigWatcher.class);
    private static final String MISSING_FAILURE_KEY = "missing";
    private static final String READ_FAILURE_KEY = "read";

    static final long RETRY_BASE_DELAY_MILLIS = 30_000L;
    static final long RETRY_MAX_DELAY_MILLIS = 300_000L;

    private final TenantLifecycleService tenantLifecycleService;
    private final TenantReadinessRegistry readinessRegistry;
    private final ObjectMapper objectMapper = new ObjectMapper();

    @Value("${app.tenants.config-path:tenants.json}")
    private String tenantsConfigPath;

    @Value("${app.tenants.config-required:false}")
    private boolean tenantsConfigRequired;

    /** Хеш означает только полностью применённое содержимое файла. */
    private String lastConfigHash = "";
    private ConfigSnapshot lastAppliedSnapshot;

    private String failedRevisionKey = "";
    private int consecutiveFailures;
    private long retryNotBeforeMillis;

    /**
     * Fence хранит семантические снимки, а не байтовые представления JSON.
     * Поэтому форматирование файла не может ошибочно снять или установить fence.
     */
    private ConfigSnapshot expectedCommittedSnapshot;
    private final Set<ConfigSnapshot> deferredSnapshots = new LinkedHashSet<>();

    private boolean configurationUnavailable;
    private LongSupplier currentTimeMillis = System::currentTimeMillis;

    public TenantConfigWatcher(TenantLifecycleService tenantLifecycleService,
                               TenantReadinessRegistry readinessRegistry) {
        this.tenantLifecycleService = tenantLifecycleService;
        this.readinessRegistry = readinessRegistry;
    }

    @Scheduled(fixedDelay = 30_000, initialDelay = 30_000)
    public void watchForChanges() {
        tenantLifecycleService.executeSerialized(this::watchForChangesSerialized);
    }

    private void watchForChangesSerialized() {
        long now = currentTimeMillis.getAsLong();
        Path path = Path.of(tenantsConfigPath);
        if (!Files.exists(path)) {
            if (tenantsConfigRequired) {
                handleRequiredMissingFile(now);
            }
            return;
        }

        if (isRetryDeferred(READ_FAILURE_KEY, now)) {
            logDeferredRetry(now);
            return;
        }

        String failureKey = READ_FAILURE_KEY;
        try {
            String content = Files.readString(path, StandardCharsets.UTF_8);
            String hash = configHash(content);
            failureKey = hash;
            Projection projection = parseProjection(content, hash);

            // Даже неизменившееся содержимое сначала разбирается: fence никогда не
            // принимает решение по сырым байтам до проверки структуры JSON.
            if (hash.equals(lastConfigHash) && !configurationUnavailable) {
                clearFenceWhenExpectedSnapshotIsAlreadyApplied(projection.snapshot());
                return;
            }
            if (isRetryDeferred(hash, now)) {
                logDeferredRetry(now);
                return;
            }
            if (!hash.equals(failedRevisionKey)) {
                resetRetryState();
            }
            if (shouldDefer(projection.snapshot())) {
                return;
            }

            log.info("Обнаружено изменение tenants.json, выполняется синхронизация конфигурации тенантов");
            syncTenants(projection.tenants());
            markApplied(projection);
        } catch (Exception failure) {
            recordFailure(failureKey, failure);
        }
    }

    /**
     * Перед API-мутацией приводит runtime к актуальной безопасной проекции файла.
     * Ошибка чтения или синхронизации не скрывается: API-операция не должна начинаться
     * с неизвестного baseline.
     */
    public void prepareForApiMutation() {
        tenantLifecycleService.executeSerialized(this::prepareForApiMutationSerialized);
    }

    private void prepareForApiMutationSerialized() {
        Path path = Path.of(tenantsConfigPath);
        if (!Files.exists(path)) {
            if (!tenantsConfigRequired) {
                return;
            }
            TenantLifecycleException failure = new TenantLifecycleException(
                    "Обязательный файл конфигурации тенантов недоступен"
            );
            recordFailure(MISSING_FAILURE_KEY, failure);
            throw failure;
        }

        String failureKey = READ_FAILURE_KEY;
        try {
            String content = Files.readString(path, StandardCharsets.UTF_8);
            String hash = configHash(content);
            failureKey = hash;
            Projection projection = parseProjection(content, hash);

            if (hash.equals(lastConfigHash) && !configurationUnavailable) {
                clearFenceWhenExpectedSnapshotIsAlreadyApplied(projection.snapshot());
                return;
            }
            if (shouldDefer(projection.snapshot())) {
                return;
            }

            syncTenants(projection.tenants());
            markApplied(projection);
        } catch (Exception failure) {
            recordFailure(failureKey, failure);
            if (failure instanceof TenantLifecycleException lifecycleFailure) {
                throw lifecycleFailure;
            }
            throw new TenantLifecycleException(
                    "Не удалось подготовить актуальную конфигурацию тенантов перед изменением",
                    failure
            );
        }
    }

    /**
     * Устанавливает snapshot fence только после подтверждённой записи в общее
     * хранилище. Локальные и no-op операции не создают ложных ожиданий от mount.
     */
    public void registerSuccessfulMutation(TenantSecretUpdateReceipt receipt) {
        Objects.requireNonNull(receipt, "Квитанция изменения tenant Secret не задана");
        tenantLifecycleService.executeSerialized(() -> registerSuccessfulMutationSerialized(receipt));
    }

    private void registerSuccessfulMutationSerialized(TenantSecretUpdateReceipt receipt) {
        if (!receipt.persisted() || !receipt.changed()) {
            return;
        }

        ConfigSnapshot previous = snapshotOf(receipt.previousTenants());
        ConfigSnapshot committed = snapshotOf(receipt.committedTenants());

        if (lastAppliedSnapshot != null) {
            deferredSnapshots.add(lastAppliedSnapshot);
        }
        if (expectedCommittedSnapshot != null) {
            deferredSnapshots.add(expectedCommittedSnapshot);
        }
        deferredSnapshots.add(previous);
        deferredSnapshots.remove(committed);
        expectedCommittedSnapshot = committed;
    }

    static String configHash(String content) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            return HexFormat.of().formatHex(digest.digest(content.getBytes(StandardCharsets.UTF_8)));
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("SHA-256 недоступен в текущем окружении", e);
        }
    }

    private Projection parseProjection(String content, String hash) throws IOException {
        List<TenantConfig> tenants = objectMapper.readValue(content, new TypeReference<>() { });
        if (tenants == null) {
            throw new IllegalArgumentException(
                    "Корневое значение конфигурации тенантов должно быть массивом"
            );
        }
        if (tenantsConfigRequired && tenants.isEmpty()) {
            throw new IllegalArgumentException(
                    "Обязательная конфигурация тенантов не может быть пустой"
            );
        }
        return new Projection(hash, List.copyOf(tenants), snapshotOf(tenants));
    }

    private ConfigSnapshot snapshotOf(List<TenantConfig> tenants) {
        Objects.requireNonNull(tenants, "Список конфигураций тенантов не задан");
        List<NormalizedTenantConfig> normalized = new ArrayList<>(tenants.size());
        for (TenantConfig tenant : tenants) {
            Objects.requireNonNull(tenant, "Конфигурация тенанта не задана");
            String domain = normalizeDomain(tenant.getDomain());
            String name = tenant.getName() == null || tenant.getName().isBlank()
                    ? domain
                    : tenant.getName().trim();
            String url = tenant.getUrl() == null ? null : tenant.getUrl().trim();
            normalized.add(new NormalizedTenantConfig(
                    name,
                    domain,
                    url,
                    tenant.getUsername(),
                    tenant.getPassword()
            ));
        }
        normalized.sort(NormalizedTenantConfig.ORDER);
        return new ConfigSnapshot(normalized);
    }

    private String normalizeDomain(String domain) {
        return domain == null ? null : domain.trim().toLowerCase(Locale.ROOT);
    }

    private void syncTenants(List<TenantConfig> tenants) {
        tenantLifecycleService.synchronizeFromPersistedConfig(tenants);
    }

    private boolean shouldDefer(ConfigSnapshot snapshot) {
        return expectedCommittedSnapshot != null && deferredSnapshots.contains(snapshot);
    }

    private void clearFenceWhenExpectedSnapshotIsAlreadyApplied(ConfigSnapshot snapshot) {
        if (snapshot.equals(expectedCommittedSnapshot)) {
            clearSnapshotFence();
        }
    }

    private void markApplied(Projection projection) {
        lastConfigHash = projection.hash();
        lastAppliedSnapshot = projection.snapshot();
        configurationUnavailable = false;
        resetRetryState();
        clearSnapshotFence();
    }

    private void handleRequiredMissingFile(long now) {
        if (isRetryDeferred(MISSING_FAILURE_KEY, now)) {
            logDeferredRetry(now);
            return;
        }
        recordFailure(
                MISSING_FAILURE_KEY,
                new TenantLifecycleException("Обязательный файл конфигурации тенантов недоступен")
        );
    }

    private boolean isRetryDeferred(String failureKey, long now) {
        return failureKey.equals(failedRevisionKey) && now < retryNotBeforeMillis;
    }

    private void logDeferredRetry(long now) {
        log.debug("Повторная синхронизация tenants.json отложена ещё на {} мс",
                Math.max(0L, retryNotBeforeMillis - now));
    }

    private void recordFailure(String failureKey, Exception failure) {
        readinessRegistry.markConfigurationFailure();
        configurationUnavailable = true;
        long retryDelay = registerFailure(failureKey);
        log.error("Ошибка синхронизации конфигурации тенантов: попытка={}, "
                        + "следующий повтор не ранее чем через {} мс, errorType={}",
                consecutiveFailures,
                retryDelay,
                failure.getClass().getSimpleName());
        log.debug("Технические детали синхронизации конфигурации тенантов", failure);
    }

    private long registerFailure(String failureKey) {
        if (failureKey.equals(failedRevisionKey) && consecutiveFailures > 0) {
            consecutiveFailures = Math.min(consecutiveFailures + 1, 31);
        } else {
            failedRevisionKey = failureKey;
            consecutiveFailures = 1;
        }
        long delay = retryDelayMillis(consecutiveFailures);
        retryNotBeforeMillis = safeAdd(currentTimeMillis.getAsLong(), delay);
        return delay;
    }

    static long retryDelayMillis(int failureCount) {
        long delay = RETRY_BASE_DELAY_MILLIS;
        for (int attempt = 1; attempt < failureCount && delay < RETRY_MAX_DELAY_MILLIS; attempt++) {
            delay = Math.min(delay * 2, RETRY_MAX_DELAY_MILLIS);
        }
        return delay;
    }

    private long safeAdd(long value, long increment) {
        return value > Long.MAX_VALUE - increment ? Long.MAX_VALUE : value + increment;
    }

    private void resetRetryState() {
        failedRevisionKey = "";
        consecutiveFailures = 0;
        retryNotBeforeMillis = 0L;
    }

    private void clearSnapshotFence() {
        expectedCommittedSnapshot = null;
        deferredSnapshots.clear();
    }

    private record Projection(String hash, List<TenantConfig> tenants, ConfigSnapshot snapshot) {
    }

    private record ConfigSnapshot(List<NormalizedTenantConfig> tenants) {
        private ConfigSnapshot {
            tenants = List.copyOf(tenants);
        }
    }

    private record NormalizedTenantConfig(
            String name,
            String domain,
            String url,
            String username,
            String password
    ) {
        private static final Comparator<String> NULL_SAFE_TEXT = Comparator.nullsFirst(String::compareTo);
        private static final Comparator<NormalizedTenantConfig> ORDER =
                Comparator.comparing(NormalizedTenantConfig::domain, NULL_SAFE_TEXT)
                        .thenComparing(NormalizedTenantConfig::name, NULL_SAFE_TEXT)
                        .thenComparing(NormalizedTenantConfig::url, NULL_SAFE_TEXT)
                        .thenComparing(NormalizedTenantConfig::username, NULL_SAFE_TEXT)
                        .thenComparing(NormalizedTenantConfig::password, NULL_SAFE_TEXT);
    }
}
