package com.cameleer.server.app.http;

import com.cameleer.server.core.http.OutboundHttpClientFactory;
import com.cameleer.server.core.http.OutboundHttpProperties;
import com.cameleer.server.core.http.OutboundHttpRequestContext;
import com.cameleer.server.core.http.TrustMode;
import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactoryBuilder;
import org.apache.hc.core5.util.Timeout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.Duration;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.stream.Stream;

public class ApacheOutboundHttpClientFactory implements OutboundHttpClientFactory {

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

    private final OutboundHttpProperties systemProps;
    private final SslContextBuilder sslBuilder;
    private final ConcurrentMap<CacheKey, CloseableHttpClient> clients = new ConcurrentHashMap<>();

    public ApacheOutboundHttpClientFactory(OutboundHttpProperties systemProps, SslContextBuilder sslBuilder) {
        this.systemProps = systemProps;
        this.sslBuilder = sslBuilder;
    }

    @Override
    public CloseableHttpClient clientFor(OutboundHttpRequestContext ctx) {
        CacheKey key = CacheKey.of(systemProps, ctx);
        return clients.computeIfAbsent(key, k -> build(ctx));
    }

    private CloseableHttpClient build(OutboundHttpRequestContext ctx) {
        try {
            var sslContext = sslBuilder.build(systemProps, ctx);
            boolean trustAll = systemProps.trustAll() || ctx.trustMode() == TrustMode.TRUST_ALL;
            var sslFactoryBuilder = SSLConnectionSocketFactoryBuilder.create()
                    .setSslContext(sslContext);
            if (trustAll) {
                sslFactoryBuilder.setHostnameVerifier(NoopHostnameVerifier.INSTANCE);
            }
            var sslFactory = sslFactoryBuilder.build();
            var connMgr = PoolingHttpClientConnectionManagerBuilder.create()
                    .setSSLSocketFactory(sslFactory)
                    .setDefaultConnectionConfig(ConnectionConfig.custom()
                            .setConnectTimeout(Timeout.of(
                                    ctx.connectTimeout() != null ? ctx.connectTimeout() : systemProps.defaultConnectTimeout()))
                            .setSocketTimeout(Timeout.of(
                                    ctx.readTimeout() != null ? ctx.readTimeout() : systemProps.defaultReadTimeout()))
                            .build())
                    .build();
            log.debug("Built outbound HTTP client: trustMode={}, caPaths={}",
                    ctx.trustMode(), ctx.trustedCaPemPaths());
            return HttpClients.custom()
                    .setConnectionManager(connMgr)
                    .setDefaultRequestConfig(RequestConfig.custom().build())
                    .build();
        } catch (Exception e) {
            throw new IllegalStateException("Failed to build outbound HTTP client", e);
        }
    }

    private record CacheKey(
            boolean trustAll,
            List<String> caPaths,
            TrustMode mode,
            Duration connect,
            Duration read
    ) {
        static CacheKey of(OutboundHttpProperties sp, OutboundHttpRequestContext ctx) {
            List<String> mergedPaths = Stream.concat(
                    sp.trustedCaPemPaths().stream(),
                    ctx.trustedCaPemPaths().stream()
            ).toList();
            return new CacheKey(
                    sp.trustAll(),
                    List.copyOf(mergedPaths),
                    ctx.trustMode(),
                    ctx.connectTimeout() != null ? ctx.connectTimeout() : sp.defaultConnectTimeout(),
                    ctx.readTimeout() != null ? ctx.readTimeout() : sp.defaultReadTimeout()
            );
        }
    }
}
