package net.northking.cctp.element.internal.protocol.http;

import net.northking.cctp.element.core.IExecuteContext;
import net.northking.cctp.element.core.annotation.Bean;
import net.northking.cctp.element.core.exception.ComponentRuntimeException;
import net.northking.cctp.element.internal.protocol.http.auth.AuthScheme;
import net.northking.cctp.element.internal.protocol.http.auth.oauth2.OAuth2SchemeFactory;
import net.northking.cctp.element.internal.utils.FileServer;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.*;
import org.apache.http.auth.AuthSchemeProvider;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.*;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.config.Lookup;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.config.SocketConfig;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustAllStrategy;
import org.apache.http.cookie.CookieSpecProvider;
import org.apache.http.impl.auth.BasicSchemeFactory;
import org.apache.http.impl.auth.DigestSchemeFactory;
import org.apache.http.impl.auth.NTLMSchemeFactory;
import org.apache.http.impl.client.*;
import org.apache.http.impl.conn.DefaultSchemePortResolver;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.impl.cookie.IgnoreSpecProvider;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpRequestExecutor;
import org.apache.http.ssl.SSLContextBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.net.ssl.SSLContext;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URL;
import java.net.UnknownHostException;
import java.security.KeyStore;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

/**
 * HttpClient
 *
 * @author fengxiang.zhang
 * @date 2022/4/13
 * @since 1.0
 */
@Bean
public class HttpClient {

    private static final Logger logger = LoggerFactory.getLogger(HttpClient.class);
    /**
     * 重试次数
     */
    private static final int RETRY_COUNT = 0;
    /**
     * 重试激活
     */
    private static final boolean REQUEST_SENT_RETRY_ENABLED = false;
    /**
     * KEEP ALIVE时间
     */
    private static final long TIME_TO_LIVE = (long) 60 * 1000;
    /**
     * 标记
     */
    public static final String HTTP_AUTH_CLIENT = "http.auth.http-client";
    /**
     * SOCKET_FACTORY_REGISTRY
     */
    private static final String SOCKET_FACTORY_REGISTRY = "http.socket-factory-registry";
    /**
     * 连接管理
     */
    private PoolingHttpClientConnectionManager connectionManager;
    /**
     * 缓存客户端
     */
    private Map<HttpClientKey, CloseableHttpClient> clientMap = new ConcurrentHashMap<>();
    /**
     * 共享客户端KEY
     */
    private HttpClientKey sharedKey = new HttpClientKey();

    public HttpClient() {
        connectionManager = new PoolingHttpClientConnectionManager(TIME_TO_LIVE, TimeUnit.MILLISECONDS);
        connectionManager.setDefaultMaxPerRoute(1);
        connectionManager.setMaxTotal(2);
    }

    /**
     * 发送请求 2023/02/16增加上下文获取minio地址
     */
    public HttpResponseData executeRequest(RequestParam rp, IExecuteContext execContext) throws Exception {
        // 获取HttpClient
        CloseableHttpClient httpClient = getHttpClient(rp);
        // 初始化上下文
        HttpClientContext context = HttpClientContext.create();
        context.setAttribute(HTTP_AUTH_CLIENT, httpClient);
        context.setAuthCache(new BasicAuthCache());
        context.setCredentialsProvider(new BasicCredentialsProvider());
        context.setAuthSchemeRegistry(authSchemeRegistry());
        context.setAttribute(SOCKET_FACTORY_REGISTRY, RegistryBuilder.create()
                .register("http", PlainConnectionSocketFactory.getSocketFactory())
                .register("https", new SSLConnectionSocketFactory(createSslContext(execContext,rp.getSslKey()), NoopHostnameVerifier.INSTANCE))
                .build());
        // 构建请求信息
        HttpRequestBase httpRequest = RequestBuilder.builder()
                .context(context)
                .scriptContext(execContext)
                .requestParam(rp)
                .build();
        RequestConfig config = RequestConfig.custom()
                .setConnectTimeout(rp.getTimeout())
                .setSocketTimeout(rp.getTimeout())
                .build();
        httpRequest.setConfig(config);
        // 执行请求
        CloseableHttpResponse response = null;
        try {
            long start = System.currentTimeMillis();
            response = httpClient.execute(httpRequest, context);
            long end = System.currentTimeMillis();
            return new HttpResponseData().response(response).time(end - start).urlDecode(execContext.getVariableValue("url_decode_charset"));
        } catch (Exception e) {
            if (e instanceof UnknownHostException) {
                URL url = new URL(rp.getUrl());
                throw new ComponentRuntimeException("Unknown host " + url.getHost(), e);
            }
            throw e;
        }finally {
            if(response!=null){
                try {
                    logger.debug("关闭流====");
                    response.close();
                } catch (IOException e) {
                    logger.error("关闭流异常: ", e);
                }
            }
        }
    }

    /**
     * 获取请求客户端
     */
    private CloseableHttpClient getHttpClient(RequestParam rp) throws Exception {
        HttpClientKey key = (rp.getProxy() == null) ? sharedKey : new HttpClientKey(new URL(rp.getUrl()), rp.getProxy());
        CloseableHttpClient httpClient = clientMap.get(key);
        if (httpClient == null) {
            httpClient = initHttpClient(rp);
            clientMap.put(key, httpClient);
        }
        return httpClient;
    }

    /**
     * 初始化请求客户端
     */
    private CloseableHttpClient initHttpClient(RequestParam rp) {
        Registry<CookieSpecProvider> cookieRegistry = RegistryBuilder.<CookieSpecProvider>create()
                .register("ignoreCookies", new IgnoreSpecProvider())
                .build();
        HttpClientBuilder builder = HttpClients.custom()
                .setSchemePortResolver(new DefaultSchemePortResolver())
                .setRequestExecutor(new NkHttpRequestExecutor())
                .setDefaultCookieSpecRegistry(cookieRegistry)
                .setDefaultSocketConfig(SocketConfig.DEFAULT)
                .setRedirectStrategy(new LaxRedirectStrategy())
                .setRetryHandler(new StandardHttpRequestRetryHandler(RETRY_COUNT, REQUEST_SENT_RETRY_ENABLED))
                .disableDefaultUserAgent()
                .disableContentCompression()
                .setConnectionReuseStrategy(DefaultClientConnectionReuseStrategy.INSTANCE)
                .setConnectionManager(connectionManager)
                .setConnectionManagerShared(true);
        return builder.build();
    }

    /**
     * 注册支持的认证方式
     */
    private static Lookup<AuthSchemeProvider> authSchemeRegistry() {
        return RegistryBuilder.<AuthSchemeProvider>create()
                .register(AuthScheme.Basic.name(), new BasicSchemeFactory())
                .register(AuthScheme.Digest.name(), new DigestSchemeFactory())
                .register(AuthScheme.NTLM.name(), new NTLMSchemeFactory())
                .register(AuthScheme.OAuth2.name(), new OAuth2SchemeFactory())
                .build();
    }

    /**
     * 创建SSLContext
     */
    private static SSLContext createSslContext(IExecuteContext execContext,RequestParam.SslKey sslKey) throws Exception {
        SSLContextBuilder builder = new SSLContextBuilder();
        if (sslKey == null || StringUtils.isBlank(sslKey.getFileId())) {
            builder.loadTrustMaterial(TrustAllStrategy.INSTANCE);
        } else {
            File keyFile = FileServer.getCredentialsFile(execContext,sslKey.getFileId(), 60, TimeUnit.SECONDS);
            KeyStoreType type = KeyStoreType.from(sslKey.getType());
            KeyStore identityStore = KeyStore.getInstance(type.name());
            FileInputStream inStream = new FileInputStream(keyFile);
            try {
                identityStore.load(inStream, sslKey.getStorePasswordChar());
            } finally {
                inStream.close();
            }
            builder.loadKeyMaterial(identityStore, sslKey.getKeyPasswordChar());
        }
        return builder.build();
    }

    /**
     * 关闭资源
     */
    public void close() {
        clientMap.forEach((k, v) -> {
            try {
                v.close();
            } catch (Exception e) {
            }
        });
        connectionManager.close();
    }

    private static final class NkHttpRequestExecutor extends HttpRequestExecutor {

        @Override
        protected HttpResponse doSendRequest(HttpRequest request, HttpClientConnection conn, HttpContext context) throws IOException, HttpException {
            // EMO
            return super.doSendRequest(request, conn, context);
        }
    }

}
