package com.magistr.app.config.tenant;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.TrustManagerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.regex.Pattern;

/**
 * Атомарно изменяет Kubernetes Secret с конфигурацией тенантов через Kubernetes REST API.
 *
 * <p>Каждая мутация сначала читает актуальный Secret, применяет изменение к полученному
 * списку и отправляет полный объект обратно условным {@code PUT} с
 * {@code metadata.resourceVersion}. При конфликте операция повторно читает актуальное
 * состояние и заново применяет только свою мутацию.</p>
 */
@Service
public class KubernetesTenantSecretUpdater implements TenantConfigStore {

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

    private static final Path DEFAULT_TOKEN_PATH =
            Path.of("/var/run/secrets/kubernetes.io/serviceaccount/token");
    private static final Path DEFAULT_NAMESPACE_PATH =
            Path.of("/var/run/secrets/kubernetes.io/serviceaccount/namespace");
    private static final Path DEFAULT_CA_PATH =
            Path.of("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt");
    private static final String DEFAULT_API_BASE = "https://kubernetes.default.svc";
    private static final String DEFAULT_SECRET_NAME = "tenants-secret";
    private static final int DEFAULT_MAX_ATTEMPTS = 3;
    private static final Duration DEFAULT_RETRY_DELAY = Duration.ofMillis(50);
    private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(5);
    private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(10);
    private static final Pattern DOMAIN_PATTERN = Pattern.compile(
            "[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?"
    );
    private static final TypeReference<List<TenantConfig>> TENANT_LIST_TYPE = new TypeReference<>() {
    };

    private final ObjectMapper objectMapper;
    private final Path tokenPath;
    private final Path namespacePath;
    private final Path caPath;
    private final String apiBase;
    private final String secretName;
    private final boolean runningInKubernetes;
    private final int maxAttempts;
    private final Duration retryDelay;

    private volatile HttpClient httpClient;

    public KubernetesTenantSecretUpdater() {
        this(
                DEFAULT_TOKEN_PATH,
                DEFAULT_NAMESPACE_PATH,
                DEFAULT_CA_PATH,
                DEFAULT_API_BASE,
                DEFAULT_SECRET_NAME,
                new ObjectMapper()
        );
    }

    KubernetesTenantSecretUpdater(Path tokenPath,
                                  Path namespacePath,
                                  Path caPath,
                                  String apiBase,
                                  String secretName,
                                  ObjectMapper objectMapper) {
        this(
                tokenPath,
                namespacePath,
                caPath,
                apiBase,
                secretName,
                objectMapper,
                DEFAULT_MAX_ATTEMPTS,
                DEFAULT_RETRY_DELAY
        );
    }

    KubernetesTenantSecretUpdater(Path tokenPath,
                                  Path namespacePath,
                                  Path caPath,
                                  String apiBase,
                                  String secretName,
                                  ObjectMapper objectMapper,
                                  int maxAttempts,
                                  Duration retryDelay) {
        this(
                tokenPath,
                namespacePath,
                caPath,
                apiBase,
                secretName,
                objectMapper,
                maxAttempts,
                retryDelay,
                detectKubernetesEnvironment(tokenPath)
        );
    }

    KubernetesTenantSecretUpdater(Path tokenPath,
                                  Path namespacePath,
                                  Path caPath,
                                  String apiBase,
                                  String secretName,
                                  ObjectMapper objectMapper,
                                  int maxAttempts,
                                  Duration retryDelay,
                                  boolean kubernetesEnvironment) {
        this.tokenPath = Objects.requireNonNull(tokenPath, "Путь к ServiceAccount token не задан");
        this.namespacePath = Objects.requireNonNull(namespacePath, "Путь к namespace не задан");
        this.caPath = Objects.requireNonNull(caPath, "Путь к ServiceAccount CA не задан");
        this.apiBase = normalizeApiBase(apiBase);
        this.secretName = requireText(secretName, "Имя tenant Secret не задано");
        this.objectMapper = Objects.requireNonNull(objectMapper, "ObjectMapper не задан");
        if (maxAttempts < 1) {
            throw new IllegalArgumentException("Количество попыток должно быть положительным");
        }
        if (retryDelay == null || retryDelay.isNegative()) {
            throw new IllegalArgumentException("Задержка повтора не может быть отрицательной");
        }
        this.maxAttempts = maxAttempts;
        this.retryDelay = retryDelay;
        this.runningInKubernetes = kubernetesEnvironment || Files.exists(tokenPath);

        if (!runningInKubernetes) {
            log.info("Приложение запущено вне Kubernetes — постоянное изменение tenant Secret отключено");
        }
    }

    @Override
    public TenantSecretUpdateReceipt upsertTenant(TenantConfig tenant) {
        TenantConfig requested = TenantSecretUpdateReceipt.copyOf(
                Objects.requireNonNull(tenant, "Конфигурация тенанта не задана")
        );
        String domain = normalizeDomain(requested.getDomain());
        requested.setDomain(domain);

        if (!runningInKubernetes) {
            log.warn("Приложение запущено вне Kubernetes, постоянное добавление тенанта пропущено");
            return new TenantSecretUpdateReceipt(false, false, null, List.of(), List.of(requested));
        }

        return mutate(tenants -> {
            List<TenantConfig> updated = new ArrayList<>(tenants);
            updated.removeIf(existing -> domain.equals(existing.getDomain()));
            updated.add(requested);
            return updated;
        });
    }

    @Override
    public TenantSecretUpdateReceipt removeTenant(String domain) {
        String normalizedDomain = normalizeDomain(domain);
        if (!runningInKubernetes) {
            log.warn("Приложение запущено вне Kubernetes, постоянное удаление тенанта пропущено");
            return new TenantSecretUpdateReceipt(false, false, null, List.of(), List.of());
        }

        return mutate(tenants -> {
            List<TenantConfig> updated = new ArrayList<>(tenants);
            updated.removeIf(existing -> normalizedDomain.equals(existing.getDomain()));
            return updated;
        });
    }

    @Override
    public TenantSecretCompensationResult compensate(TenantSecretUpdateReceipt receipt) {
        Objects.requireNonNull(receipt, "Квитанция изменения tenant Secret не задана");
        if (!receipt.persisted() || !receipt.changed()) {
            return TenantSecretCompensationResult.NOT_REQUIRED;
        }
        if (!runningInKubernetes) {
            return TenantSecretCompensationResult.NOT_REQUIRED;
        }

        String committedVersion = requireText(
                receipt.committedResourceVersion(),
                "В квитанции отсутствует resourceVersion сохранённого tenant Secret"
        );
        RequestContext context = requestContext();
        TenantConfigPersistenceException lastFailure = null;

        for (int attempt = 1; attempt <= maxAttempts; attempt++) {
            CurrentSecret current;
            try {
                current = readCurrent(context);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new TenantConfigPersistenceException("Компенсация tenant Secret прервана", e);
            } catch (IOException e) {
                lastFailure = persistenceFailure("Не удалось прочитать tenant Secret для компенсации", e);
                if (!prepareRetry(attempt, "чтение перед компенсацией")) {
                    throw lastFailure;
                }
                continue;
            }

            if (!committedVersion.equals(current.resourceVersion())) {
                log.warn("Компенсация tenant Secret пропущена: после исходной операции Secret уже изменён");
                return TenantSecretCompensationResult.SKIPPED_CONCURRENT_CHANGE;
            }
            if (!sameTenants(current.tenants(), receipt.committedTenants())) {
                throw new TenantConfigPersistenceException(
                        "Содержимое tenant Secret не соответствует resourceVersion из квитанции"
                );
            }

            HttpResponse<String> response;
            try {
                response = putSecret(context, current, receipt.previousTenants());
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new TenantConfigPersistenceException("Компенсация tenant Secret прервана", e);
            } catch (IOException e) {
                TenantSecretCompensationResult reconciled = reconcileCompensation(context, receipt);
                if (reconciled != null) {
                    return reconciled;
                }
                lastFailure = persistenceFailure("Не удалось подтвердить компенсацию tenant Secret", e);
                if (!prepareRetry(attempt, "компенсация после сетевой ошибки")) {
                    throw lastFailure;
                }
                continue;
            }

            if (isSuccess(response.statusCode())) {
                TenantSecretCompensationResult confirmed = confirmCompensation(
                        context,
                        receipt,
                        response.body()
                );
                if (confirmed != null) {
                    if (confirmed == TenantSecretCompensationResult.RESTORED) {
                        log.info("Конфигурация tenant Secret восстановлена: tenantCount={}",
                                receipt.previousTenants().size());
                    }
                    return confirmed;
                }
                lastFailure = new TenantConfigPersistenceException(
                        "Kubernetes API не подтвердил содержимое после компенсации tenant Secret"
                );
                if (!prepareRetry(attempt, "проверка компенсации tenant Secret")) {
                    throw lastFailure;
                }
                continue;
            }
            if (response.statusCode() == 409) {
                log.warn("Компенсация tenant Secret пропущена: обнаружено конкурентное изменение");
                return TenantSecretCompensationResult.SKIPPED_CONCURRENT_CHANGE;
            }
            if (isRetryable(response.statusCode())) {
                TenantSecretCompensationResult reconciled = reconcileCompensation(context, receipt);
                if (reconciled != null) {
                    return reconciled;
                }
                lastFailure = new TenantConfigPersistenceException(
                        "Kubernetes API временно отклонил компенсацию tenant Secret: HTTP "
                                + response.statusCode()
                );
                if (!prepareRetry(attempt, "компенсация tenant Secret")) {
                    throw lastFailure;
                }
                continue;
            }
            throw new TenantConfigPersistenceException(
                    "Kubernetes API отклонил компенсацию tenant Secret: HTTP " + response.statusCode()
            );
        }

        throw lastFailure == null
                ? new TenantConfigPersistenceException("Не удалось компенсировать tenant Secret")
                : lastFailure;
    }

    private TenantSecretUpdateReceipt mutate(Function<List<TenantConfig>, List<TenantConfig>> mutation) {
        RequestContext context = requestContext();
        TenantConfigPersistenceException lastFailure = null;

        for (int attempt = 1; attempt <= maxAttempts; attempt++) {
            CurrentSecret current;
            try {
                current = readCurrent(context);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new TenantConfigPersistenceException("Изменение tenant Secret прервано", e);
            } catch (IOException e) {
                lastFailure = persistenceFailure("Не удалось прочитать актуальный tenant Secret", e);
                if (!prepareRetry(attempt, "чтение tenant Secret")) {
                    throw lastFailure;
                }
                continue;
            }

            List<TenantConfig> previous = normalizeTenants(current.tenants());
            List<TenantConfig> committed = normalizeTenants(mutation.apply(previous));
            if (sameTenants(previous, committed)) {
                return new TenantSecretUpdateReceipt(
                        true,
                        false,
                        current.resourceVersion(),
                        previous,
                        committed
                );
            }

            HttpResponse<String> response;
            try {
                response = putSecret(context, current, committed);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new TenantConfigPersistenceException("Изменение tenant Secret прервано", e);
            } catch (IOException e) {
                TenantSecretUpdateReceipt reconciled = reconcileMutation(context, previous, committed);
                if (reconciled != null) {
                    return reconciled;
                }
                lastFailure = persistenceFailure("Не удалось подтвердить изменение tenant Secret", e);
                if (!prepareRetry(attempt, "изменение после сетевой ошибки")) {
                    throw lastFailure;
                }
                continue;
            }

            if (isSuccess(response.statusCode())) {
                TenantSecretUpdateReceipt confirmed = confirmMutation(
                        context,
                        current,
                        previous,
                        committed,
                        response.body()
                );
                if (confirmed != null) {
                    log.info("Tenant Secret атомарно изменён: tenantCount={}", committed.size());
                    return confirmed;
                }
                throw new TenantConfigPersistenceException(
                        "Kubernetes API не подтвердил сохранённое содержимое tenant Secret"
                );
            }

            if (response.statusCode() == 409) {
                lastFailure = new TenantConfigPersistenceException(
                        "Исчерпаны попытки изменения tenant Secret из-за конкурентных обновлений"
                );
                if (!prepareRetry(attempt, "конфликт resourceVersion")) {
                    throw lastFailure;
                }
                continue;
            }

            if (isRetryable(response.statusCode())) {
                TenantSecretUpdateReceipt reconciled = reconcileMutation(context, previous, committed);
                if (reconciled != null) {
                    return reconciled;
                }
                lastFailure = new TenantConfigPersistenceException(
                        "Kubernetes API временно отклонил изменение tenant Secret: HTTP "
                                + response.statusCode()
                );
                if (!prepareRetry(attempt, "изменение tenant Secret")) {
                    throw lastFailure;
                }
                continue;
            }

            throw new TenantConfigPersistenceException(
                    "Kubernetes API отклонил изменение tenant Secret: HTTP " + response.statusCode()
            );
        }

        throw lastFailure == null
                ? new TenantConfigPersistenceException("Не удалось изменить tenant Secret")
                : lastFailure;
    }

    private CurrentSecret readCurrent(RequestContext context) throws IOException, InterruptedException {
        HttpRequest request = requestBuilder(context)
                .GET()
                .build();
        HttpResponse<String> response = secureClient().send(
                request,
                HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)
        );
        if (response.statusCode() == 200) {
            return parseSecret(response.body());
        }
        if (isRetryable(response.statusCode())) {
            throw new RetryableApiException(
                    "Kubernetes API временно не вернул tenant Secret: HTTP " + response.statusCode()
            );
        }
        throw new TenantConfigPersistenceException(
                "Kubernetes API не вернул tenant Secret: HTTP " + response.statusCode()
        );
    }

    private HttpResponse<String> putSecret(RequestContext context,
                                           CurrentSecret current,
                                           List<TenantConfig> tenants)
            throws IOException, InterruptedException {
        ObjectNode body = current.secret().deepCopy();
        ObjectNode metadata = objectNode(body, "metadata");
        metadata.put("resourceVersion", current.resourceVersion());
        ObjectNode data = objectNode(body, "data");
        data.put("tenants.json", encodeTenants(tenants));

        HttpRequest request = requestBuilder(context)
                .header("Content-Type", "application/json")
                .PUT(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body)))
                .build();
        return secureClient().send(
                request,
                HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)
        );
    }

    private TenantSecretUpdateReceipt reconcileMutation(RequestContext context,
                                                         List<TenantConfig> previous,
                                                         List<TenantConfig> committed) {
        try {
            CurrentSecret observed = readCurrent(context);
            if (sameTenants(observed.tenants(), committed)) {
                log.info("Изменение tenant Secret подтверждено повторным чтением: tenantCount={}",
                        committed.size());
                return new TenantSecretUpdateReceipt(
                        true,
                        false,
                        observed.resourceVersion(),
                        previous,
                        observed.tenants()
                );
            }
            return null;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new TenantConfigPersistenceException("Проверка изменения tenant Secret прервана", e);
        } catch (IOException e) {
            log.warn("Не удалось подтвердить состояние tenant Secret повторным чтением");
            return null;
        }
    }

    private TenantSecretUpdateReceipt confirmMutation(RequestContext context,
                                                       CurrentSecret previousSecret,
                                                       List<TenantConfig> previous,
                                                       List<TenantConfig> committed,
                                                       String responseBody) {
        try {
            CurrentSecret confirmed = parseSecret(responseBody);
            if (sameTenants(confirmed.tenants(), committed)
                    && !Objects.equals(confirmed.resourceVersion(), previousSecret.resourceVersion())) {
                return new TenantSecretUpdateReceipt(
                        true,
                        true,
                        confirmed.resourceVersion(),
                        previous,
                        confirmed.tenants()
                );
            }
            log.warn("Ответ Kubernetes API после изменения tenant Secret не совпал с ожидаемым состоянием");
        } catch (TenantConfigPersistenceException invalidResponse) {
            log.warn("Kubernetes API вернул неполный ответ после изменения tenant Secret, выполняется повторное чтение");
            log.debug("Технические детали неполного ответа tenant Secret", invalidResponse);
        }
        return reconcileMutation(context, previous, committed);
    }

    private TenantSecretCompensationResult confirmCompensation(RequestContext context,
                                                                TenantSecretUpdateReceipt receipt,
                                                                String responseBody) {
        try {
            CurrentSecret confirmed = parseSecret(responseBody);
            if (sameTenants(confirmed.tenants(), receipt.previousTenants())) {
                return TenantSecretCompensationResult.RESTORED;
            }
            log.warn("Ответ Kubernetes API после компенсации tenant Secret не совпал с ожидаемым состоянием");
        } catch (TenantConfigPersistenceException invalidResponse) {
            log.warn("Kubernetes API вернул неполный ответ после компенсации tenant Secret, выполняется повторное чтение");
            log.debug("Технические детали неполного ответа компенсации tenant Secret", invalidResponse);
        }
        return reconcileCompensation(context, receipt);
    }

    private TenantSecretCompensationResult reconcileCompensation(RequestContext context,
                                                                  TenantSecretUpdateReceipt receipt) {
        try {
            CurrentSecret observed = readCurrent(context);
            if (sameTenants(observed.tenants(), receipt.previousTenants())) {
                log.info("Компенсация tenant Secret подтверждена повторным чтением");
                return TenantSecretCompensationResult.RESTORED;
            }
            if (!Objects.equals(observed.resourceVersion(), receipt.committedResourceVersion())) {
                log.warn("Компенсация tenant Secret остановлена из-за конкурентного изменения");
                return TenantSecretCompensationResult.SKIPPED_CONCURRENT_CHANGE;
            }
            return null;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new TenantConfigPersistenceException("Проверка компенсации tenant Secret прервана", e);
        } catch (IOException e) {
            log.warn("Не удалось подтвердить компенсацию tenant Secret повторным чтением");
            return null;
        }
    }

    private CurrentSecret parseSecret(String responseBody) {
        try {
            JsonNode root = objectMapper.readTree(responseBody);
            if (!(root instanceof ObjectNode secret)) {
                throw new TenantConfigPersistenceException("Kubernetes API вернул некорректный tenant Secret");
            }
            String resourceVersion = requireText(
                    root.path("metadata").path("resourceVersion").asText(null),
                    "В tenant Secret отсутствует metadata.resourceVersion"
            );
            JsonNode data = root.path("data");
            if (!data.isObject() || !data.hasNonNull("tenants.json")) {
                throw new TenantConfigPersistenceException(
                        "В tenant Secret отсутствует обязательный ключ data.tenants.json"
                );
            }
            String encoded = requireText(
                    data.path("tenants.json").asText(null),
                    "В tenant Secret ключ data.tenants.json не содержит конфигурацию"
            );
            byte[] decoded = Base64.getDecoder().decode(encoded);
            List<TenantConfig> tenants = objectMapper.readValue(decoded, TENANT_LIST_TYPE);
            return new CurrentSecret(secret, resourceVersion, normalizeTenants(tenants));
        } catch (TenantConfigPersistenceException e) {
            throw e;
        } catch (Exception e) {
            throw new TenantConfigPersistenceException(
                    "Не удалось разобрать конфигурацию из tenant Secret",
                    e
            );
        }
    }

    private List<TenantConfig> normalizeTenants(List<TenantConfig> tenants) {
        if (tenants == null) {
            throw new TenantConfigPersistenceException("Список конфигураций в tenant Secret не задан");
        }
        List<TenantConfig> normalized = new ArrayList<>(tenants.size());
        Set<String> domains = new HashSet<>();
        for (TenantConfig source : tenants) {
            TenantConfig tenant = TenantSecretUpdateReceipt.copyOf(
                    Objects.requireNonNull(source, "В tenant Secret обнаружена пустая конфигурация")
            );
            String domain = normalizeDomain(tenant.getDomain());
            tenant.setDomain(domain);
            if (!domains.add(domain)) {
                throw new TenantConfigPersistenceException(
                        "В tenant Secret обнаружены повторяющиеся домены"
                );
            }
            normalized.add(tenant);
        }
        normalized.sort(Comparator.comparing(TenantConfig::getDomain));
        return List.copyOf(normalized);
    }

    private boolean sameTenants(List<TenantConfig> first, List<TenantConfig> second) {
        List<TenantConfig> left = normalizeTenants(first);
        List<TenantConfig> right = normalizeTenants(second);
        if (left.size() != right.size()) {
            return false;
        }
        for (int i = 0; i < left.size(); i++) {
            if (!sameTenant(left.get(i), right.get(i))) {
                return false;
            }
        }
        return true;
    }

    private boolean sameTenant(TenantConfig first, TenantConfig second) {
        return Objects.equals(first.getName(), second.getName())
                && Objects.equals(first.getDomain(), second.getDomain())
                && Objects.equals(first.getUrl(), second.getUrl())
                && Objects.equals(first.getUsername(), second.getUsername())
                && Objects.equals(first.getPassword(), second.getPassword());
    }

    private RequestContext requestContext() {
        try {
            String token = requireText(
                    Files.readString(tokenPath).trim(),
                    "ServiceAccount token не настроен"
            );
            String namespace = requireText(
                    Files.readString(namespacePath).trim(),
                    "ServiceAccount namespace не настроен"
            );
            URI uri = URI.create(String.format(
                    "%s/api/v1/namespaces/%s/secrets/%s",
                    apiBase,
                    namespace,
                    secretName
            ));
            return new RequestContext(token, uri);
        } catch (TenantConfigPersistenceException e) {
            throw e;
        } catch (Exception e) {
            throw new TenantConfigPersistenceException(
                    "Не удалось прочитать параметры ServiceAccount для tenant Secret",
                    e
            );
        }
    }

    private HttpRequest.Builder requestBuilder(RequestContext context) {
        return HttpRequest.newBuilder()
                .uri(context.uri())
                .timeout(REQUEST_TIMEOUT)
                .header("Authorization", "Bearer " + context.token())
                .header("Accept", "application/json");
    }

    private synchronized HttpClient secureClient() {
        if (httpClient == null) {
            try {
                httpClient = createSecureClient(caPath);
            } catch (Exception e) {
                throw new TenantConfigPersistenceException(
                        "Не удалось настроить защищённое соединение с Kubernetes API",
                        e
                );
            }
        }
        return httpClient;
    }

    HttpClient createSecureClient(Path serviceAccountCaPath) throws Exception {
        CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
        Collection<? extends Certificate> certificates;
        try (InputStream input = Files.newInputStream(serviceAccountCaPath)) {
            certificates = certificateFactory.generateCertificates(input);
        }
        if (certificates.isEmpty()) {
            throw new IllegalStateException("ServiceAccount CA не содержит сертификатов");
        }

        KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
        trustStore.load(null, null);
        int index = 0;
        for (Certificate certificate : certificates) {
            trustStore.setCertificateEntry("kubernetes-ca-" + index++, certificate);
        }

        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
                TrustManagerFactory.getDefaultAlgorithm()
        );
        trustManagerFactory.init(trustStore);

        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, trustManagerFactory.getTrustManagers(), null);

        SSLParameters sslParameters = new SSLParameters();
        sslParameters.setEndpointIdentificationAlgorithm("HTTPS");

        return HttpClient.newBuilder()
                .connectTimeout(CONNECT_TIMEOUT)
                .sslContext(sslContext)
                .sslParameters(sslParameters)
                .build();
    }

    private boolean prepareRetry(int attempt, String operation) {
        if (attempt >= maxAttempts) {
            return false;
        }
        log.warn("Временная ошибка tenant Secret, операция будет повторена: операция={}, попытка={}/{}",
                operation,
                attempt,
                maxAttempts);
        try {
            long delayMillis = Math.multiplyExact(retryDelay.toMillis(), attempt);
            if (delayMillis > 0) {
                Thread.sleep(delayMillis);
            }
            return true;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new TenantConfigPersistenceException("Ожидание повтора tenant Secret прервано", e);
        } catch (ArithmeticException e) {
            throw new TenantConfigPersistenceException("Некорректная задержка повтора tenant Secret", e);
        }
    }

    private String encodeTenants(List<TenantConfig> tenants) throws IOException {
        String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(tenants);
        return Base64.getEncoder().encodeToString(json.getBytes(StandardCharsets.UTF_8));
    }

    private ObjectNode objectNode(ObjectNode parent, String fieldName) {
        JsonNode existing = parent.get(fieldName);
        if (existing instanceof ObjectNode objectNode) {
            return objectNode;
        }
        ObjectNode created = objectMapper.createObjectNode();
        parent.set(fieldName, created);
        return created;
    }

    private static boolean isSuccess(int statusCode) {
        return statusCode >= 200 && statusCode < 300;
    }

    private static boolean isRetryable(int statusCode) {
        return statusCode == 408 || statusCode == 409 || statusCode == 429 || statusCode >= 500;
    }

    private static boolean detectKubernetesEnvironment(Path tokenPath) {
        String kubernetesServiceHost = System.getenv("KUBERNETES_SERVICE_HOST");
        return (tokenPath != null && Files.exists(tokenPath))
                || (kubernetesServiceHost != null && !kubernetesServiceHost.isBlank());
    }

    private static String requireText(String value, String message) {
        if (value == null || value.isBlank()) {
            throw new TenantConfigPersistenceException(message);
        }
        return value.trim();
    }

    private static String normalizeDomain(String value) {
        String domain = requireText(value, "Домен тенанта не задан").toLowerCase(Locale.ROOT);
        if (!DOMAIN_PATTERN.matcher(domain).matches()) {
            throw new TenantConfigPersistenceException("Домен тенанта содержит недопустимые символы");
        }
        return domain;
    }

    private static String normalizeApiBase(String apiBase) {
        String normalized = requireText(apiBase, "Адрес Kubernetes API не задан");
        return normalized.endsWith("/")
                ? normalized.substring(0, normalized.length() - 1)
                : normalized;
    }

    private static TenantConfigPersistenceException persistenceFailure(String message, Exception cause) {
        return cause instanceof TenantConfigPersistenceException persistenceException
                ? persistenceException
                : new TenantConfigPersistenceException(message, cause);
    }

    private record RequestContext(String token, URI uri) {
    }

    private record CurrentSecret(ObjectNode secret,
                                 String resourceVersion,
                                 List<TenantConfig> tenants) {
    }

    private static final class RetryableApiException extends IOException {
        private RetryableApiException(String message) {
            super(message);
        }
    }
}
