package com.cf.imes.framework.es.config;

import cn.hutool.core.io.file.PathUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ObjectUtil;
import co.elastic.clients.elasticsearch.ElasticsearchAsyncClient;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import com.cf.imes.framework.common.util.encrypt.AesUtils;
import com.cf.imes.framework.es.core.service.ESDocumentService;
import com.cf.imes.framework.es.core.service.ESDocumentServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
import org.apache.http.impl.nio.reactor.IOReactorConfig;
import org.apache.http.message.BasicHeader;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.ssl.SSLContexts;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.util.StringUtils;

import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.time.Duration;
import java.util.List;


/**
 * @author there
 */
@AutoConfiguration
@ConditionalOnClass(ElasticsearchClient.class)
@EnableConfigurationProperties(EsProperties.class)
@Slf4j
public class ChenfengElasticsearchAutoConfiguration {

    @Value("${chenfeng.encrypt.publicKey:}")
    private String publicKey;

    /**
     * 同步方式
     *
     */
    @Bean
    public ElasticsearchClient elasticsearchClient(RestClientTransport transport) {
        return new ElasticsearchClient(transport);
    }

    /**
     * 异步方式
     *
     */
    @Bean
    public ElasticsearchAsyncClient elasticsearchAsyncClient(RestClientTransport transport) {
        return new ElasticsearchAsyncClient(transport);
    }


    @Bean
    public ESDocumentService esDocumentService(ElasticsearchClient elasticsearchClient, ElasticsearchAsyncClient elasticsearchAsyncClient) {
        return new ESDocumentServiceImpl(elasticsearchClient, elasticsearchAsyncClient);
    }

    /**
     * 获取客户端 RestClientTransport
     */
    @Bean
    public RestClientTransport getTransport(RestClient client){
        return new RestClientTransport(client, new JacksonJsonpMapper());
    }


    /**
     * 获取客户端RestClient
     * chenfeng.encrypt.enable：true
     *
     * @param properties es配置
     */
    @Bean
    @ConditionalOnProperty(name = "chenfeng.encrypt.enable", havingValue = "true")
    public RestClient getAuthRestClient(EsProperties properties) {
        return getRestClient(properties, true);
    }

    /**
     * 获取客户端RestClient（默认）
     * chenfeng.encrypt.enable：false或者缺省
     *
     * @param properties es配置
     */
    @Bean
    @ConditionalOnProperty(name = "chenfeng.encrypt.enable", havingValue = "false", matchIfMissing = true)
    public RestClient getRestClient(EsProperties properties) {
        return getRestClient(properties, false);
    }

    /**
     * 动态获取es client连接
     *
     * @param properties  配置
     * @param needDecrypt 敏感信息是否需要解密
     * @return
     */
    private RestClient getRestClient(EsProperties properties, boolean needDecrypt) {
        // 配置账号密码
        CredentialsProvider credentialsProvider;
        String username = properties.getUsername();
        String password = properties.getPassword();
        if (CharSequenceUtil.isAllNotEmpty(username, password)) {
            credentialsProvider = new BasicCredentialsProvider();
            credentialsProvider.setCredentials(AuthScope.ANY,
                    new UsernamePasswordCredentials(needDecrypt ? AesUtils.decrypt(username, publicKey) : username, needDecrypt ? AesUtils.decrypt(password, publicKey) : password));
        } else {
            credentialsProvider = null;
        }
        RestClientBuilder.HttpClientConfigCallback httpClientConfigCallback = httpClientBuilder -> {
            // 设置账号密码、连接信息
            HttpAsyncClientBuilder httpAsyncClientBuilder = httpClientBuilder
                    .setDefaultRequestConfig(RequestConfig.custom()
                            .setConnectTimeout((int) properties.getConnectionTimeout().toMillis())
                            .setSocketTimeout((int) properties.getSocketTimeout().toMillis())
                            .setConnectionRequestTimeout((int) properties.getConnectionRequestTimeout().toMillis())
                            // 启用数据压缩
                            .setContentCompressionEnabled(true)
                            .build());
            if (ObjectUtil.isNotNull(credentialsProvider)) {
                httpAsyncClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
            }
            if (properties.isSecurityHttpSslEnable()) {
                // 开启ssl配置连接证书
                httpAsyncClientBuilder.setSSLContext(buildSSLContext(properties)).setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE);
            }
            // 手动开启keepalive
            httpAsyncClientBuilder.setDefaultIOReactorConfig(IOReactorConfig.custom().setSoKeepAlive(true).build());
            httpAsyncClientBuilder.setDefaultHeaders(List.of(new BasicHeader("Accept-Encoding", "gzip")));
            // 手动设置保活时长
            httpAsyncClientBuilder.setKeepAliveStrategy(((response, context) -> Duration.ofMinutes(5).toMillis()));
            return httpAsyncClientBuilder;
        };
        return RestClient.builder(toHttpHost(properties.getUris(), properties.isSecurityHttpSslEnable())).setHttpClientConfigCallback(httpClientConfigCallback).build();
    }

    /**
     * 构建ssl请求信息
     *
     * @param properties es配置
     * @return
     */
    private SSLContext buildSSLContext(EsProperties properties) {
        SSLContext sslContext = null;
        try {
            Path trustStorePath = Paths.get(properties.getCertificatePath());
            KeyStore trustStore = KeyStore.getInstance("pkcs12");
            try (InputStream is = PathUtil.getInputStream(trustStorePath)) {
                trustStore.load(is, properties.getPassword().toCharArray());
            }
            SSLContextBuilder sslContextBuilder = SSLContexts.custom()
                    .loadTrustMaterial(trustStore, null);
            sslContext = sslContextBuilder.build();
        } catch (CertificateException | IOException | KeyStoreException | NoSuchAlgorithmException |
                 KeyManagementException e) {
            log.error("ES连接认证失败", e);
        }
        return sslContext;
    }


    /**
     * 解析域名
     *
     * @param hosts       域名字符串
     * @param isSslEnable 是否开启ssl
     * @return
     */
    private HttpHost[] toHttpHost(String hosts, boolean isSslEnable) {
        if (!StringUtils.hasLength(hosts)) {
            throw new IllegalArgumentException("invalid elasticsearch configuration. elasticsearch.hosts不能为空！");
        }
        // 多个IP逗号隔开
        String[] hostArray = hosts.split(",");
        HttpHost[] httpHosts = new HttpHost[hostArray.length];
        HttpHost httpHost;
        for (int i = 0; i < hostArray.length; i++) {
            String[] strings = hostArray[i].split(":");
            httpHost = new HttpHost(strings[0], Integer.parseInt(strings[1]), isSslEnable ? "https" : "http");
            httpHosts[i] = httpHost;
        }
        return httpHosts;
    }
}
