package com.acgist.snail.net.http;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Map;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.X509TrustManager;

import com.acgist.snail.config.SystemConfig;
import com.acgist.snail.context.wrapper.HttpHeaderWrapper;
import com.acgist.snail.logger.Logger;
import com.acgist.snail.logger.LoggerFactory;
import com.acgist.snail.net.NetException;
import com.acgist.snail.utils.IoUtils;
import com.acgist.snail.utils.MapUtils;
import com.acgist.snail.utils.NumberUtils;

/**
 * HTTP客户端
 * 配置参考：https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html
 * 
 * @author acgist
 */
public final class HttpClient {

    private static final Logger LOGGER = LoggerFactory.getLogger(HttpClient.class);
    
    /**
     * 请求方式
     * 
     * @author acgist
     */
    public enum Method {
        
        /**
         * GET请求
         */
        GET,
        /**
         * HEAD请求
         */
        HEAD,
        /**
         * POST请求
         */
        POST;
        
    }
    
    /**
     * HTTP客户端信息（User-Agent）
     */
    private static final String USER_AGENT;
    
    static {
        final StringBuilder userAgentBuilder = new StringBuilder();
        userAgentBuilder
            .append(SystemConfig.getNameEn())
            .append("/")
            .append(SystemConfig.getVersion())
            .append(" (+")
            .append(SystemConfig.getSupport())
            .append(")");
        USER_AGENT = userAgentBuilder.toString();
        LOGGER.debug("HTTP客户端信息（User-Agent）：{}", USER_AGENT);
        // 配置HTTPS
        final SSLContext sslContext = HttpClient.buildSSLContext();
        if(sslContext != null) {
            HttpsURLConnection.setDefaultHostnameVerifier(SnailHostnameVerifier.INSTANCE);
            HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
        }
    }
    
    /**
     * 请求地址
     */
    private final String url;
    /**
     * 请求连接
     */
    private final HttpURLConnection httpURLConnection;
    /**
     * 状态码
     */
    private int code;
    /**
     * 响应头
     */
    private HttpHeaderWrapper httpHeaderWrapper;
    
    /**
     * @param url            请求地址
     * @param connectTimeout 连接超时时间（毫秒）
     * @param receiveTimeout 响应超时时间（毫秒）
     * 
     * @throws NetException 网络异常
     */
    private HttpClient(String url, int connectTimeout, int receiveTimeout) throws NetException {
        this.url               = url;
        this.httpURLConnection = this.buildHttpURLConnection(connectTimeout, receiveTimeout);
        this.setDefaultHeader();
    }
    
    /**
     * 新建下载HTTP客户端
     * 
     * @param url 请求地址
     * 
     * @return {@link HttpClient}
     * 
     * @throws NetException 网络异常
     */
    public static final HttpClient newDownloader(String url) throws NetException {
        return HttpClient.newInstance(url, SystemConfig.CONNECT_TIMEOUT_MILLIS, SystemConfig.DOWNLOAD_TIMEOUT_MILLIS);
    }
    
    /**
     * 新建HTTP客户端
     * 
     * @param url 请求地址
     * 
     * @return {@link HttpClient}
     * 
     * @throws NetException 网络异常
     */
    public static final HttpClient newInstance(String url) throws NetException {
        return HttpClient.newInstance(url, SystemConfig.CONNECT_TIMEOUT_MILLIS, SystemConfig.RECEIVE_TIMEOUT_MILLIS);
    }
    
    /**
     * 新建HTTP客户端
     * 
     * @param url            请求地址
     * @param connectTimeout 连接超时时间（毫秒）
     * @param receiveTimeout 响应超时时间（毫秒）
     * 
     * @return {@link HttpClient}
     * 
     * @throws NetException 网络异常
     */
    public static final HttpClient newInstance(String url, int connectTimeout, int receiveTimeout) throws NetException {
        return new HttpClient(url, connectTimeout, receiveTimeout);
    }
    
    /**
     * 使用缓存
     * 
     * @return {@link HttpClient}
     */
    public HttpClient useCache() {
        this.httpURLConnection.setUseCaches(true);
        return this;
    }
    
    /**
     * 设置请求头
     * 
     * @param key   请求头名称
     * @param value 请求头值
     * 
     * @return {@link HttpClient}
     */
    public HttpClient header(String key, String value) {
        this.httpURLConnection.setRequestProperty(key, value);
        return this;
    }
    
    /**
     * 启用长连接
     * 
     * @return {@link HttpClient}
     */
    public HttpClient keepAlive() {
        return this.header("Connection", "keep-alive");
    }
    
    /**
     * 禁用长连接
     * 
     * @return {@link HttpClient}
     */
    public HttpClient disableKeepAlive() {
        return this.header("Connection", "close");
    }
    
    /**
     * 设置请求范围
     * 
     * @param pos 开始位置
     * 
     * @return {@link HttpClient}
     */
    public HttpClient range(long pos) {
        return this.header(HttpHeaderWrapper.HEADER_RANGE, "bytes=" + pos + "-");
    }
    
    /**
     * 执行GET请求
     * 
     * @return {@link HttpClient}
     * 
     * @throws NetException 网络异常
     */
    public HttpClient get() throws NetException {
        return this.execute(Method.GET, null);
    }
    
    /**
     * 执行HEAD请求
     * 
     * @return {@link HttpClient}
     * 
     * @throws NetException 网络异常
     */
    public HttpClient head() throws NetException {
        return this.execute(Method.HEAD, null);
    }
    
    /**
     * 执行POST请求
     * 
     * @param data 请求数据
     * 
     * @return {@link HttpClient}
     * 
     * @throws NetException 网络异常
     */
    public HttpClient post(String data) throws NetException {
        this.header(HttpHeaderWrapper.HEADER_CONTENT_TYPE, "application/json");
        return this.execute(Method.POST, data);
    }
    
    /**
     * 执行POST表单请求
     * 
     * @param data 请求数据
     * 
     * @return {@link HttpClient}
     * 
     * @throws NetException 网络异常
     */
    public HttpClient post(Map<String, String> data) throws NetException {
        // 设置表单请求
        this.header(HttpHeaderWrapper.HEADER_CONTENT_TYPE, "application/x-www-form-urlencoded;charset=" + SystemConfig.DEFAULT_CHARSET);
        if(MapUtils.isEmpty(data)) {
            return this.execute(Method.POST, null);
        } else {
            return this.execute(Method.POST, MapUtils.toUrlQuery(data));
        }
    }
    
    /**
     * 执行请求
     * 
     * @param method 请求方法
     * @param body   请求数据
     * 
     * @return {@link HttpClient}
     * 
     * @throws NetException 网络异常
     */
    public HttpClient execute(Method method, String body) throws NetException {
        OutputStream output = null;
        try {
            // 设置请求方式
            this.httpURLConnection.setRequestMethod(method.name());
            if(method == Method.GET) {
                // 是否写出：GET不要写出
                this.httpURLConnection.setDoOutput(false);
            } else if(method == Method.HEAD) {
                // 是否写出：HEAD不要写出
                this.httpURLConnection.setDoOutput(false);
            } else if(method == Method.POST) {
                // 是否写出：POST需要写出
                this.httpURLConnection.setDoOutput(true);
            } else {
                throw new NetException("不支持的请求方式：" + method);
            }
            // 发起连接
            this.httpURLConnection.connect();
            // 发送请求参数
            if(body != null) {
                output = this.httpURLConnection.getOutputStream();
                output.write(body.getBytes());
            }
            // 设置状态码
            this.code = this.httpURLConnection.getResponseCode();
        } catch (IOException e) {
            throw new NetException(e);
        } finally {
            IoUtils.close(output);
        }
        return this;
    }
    
    /**
     * @return 状态码
     */
    public int getCode() {
        return this.code;
    }
    
    /**
     * @return 是否成功
     * 
     * @see HttpURLConnection#HTTP_OK
     */
    public boolean ok() {
        return HttpURLConnection.HTTP_OK == this.code;
    }
    
    /**
     * @return 是否部分内容
     * 
     * @see HttpURLConnection#HTTP_PARTIAL
     */
    public boolean partial() {
        return HttpURLConnection.HTTP_PARTIAL == this.code;
    }
    
    /**
     * @return 是否服务器错误
     * 
     * @see HttpURLConnection#HTTP_INTERNAL_ERROR
     */
    public boolean internalError() {
        return HttpURLConnection.HTTP_INTERNAL_ERROR == this.code;
    }
    
    /**
     * @return 是否可以下载
     * 
     * @see #ok()
     * @see #partial()
     */
    public boolean downloadable() {
        return this.ok() || this.partial();
    }
    
    /**
     * 使用完成需要关闭（归还连接）：下次相同地址端口继续使用（复用底层Socket）
     * 
     * @return 响应数据流
     * 
     * @throws NetException 网络异常
     */
    public InputStream response() throws NetException {
        try {
            return this.httpURLConnection.getInputStream();
        } catch (IOException e) {
            throw new NetException(e);
        }
    }

    /**
     * @return 响应字节数组
     * 
     * @throws NetException 网络异常
     */
    public byte[] responseToBytes() throws NetException {
        try (
            final InputStream input = this.response()
        ) {
            final int size     = input.available();
            final byte[] bytes = new byte[size];
            final int length   = input.read(bytes);
            if(length == size) {
                return bytes;
            } else {
                return Arrays.copyOf(bytes, length);
            }
        } catch (IOException e) {
            throw new NetException(e);
        }
    }
    
    /**
     * @return 响应文本
     * 
     * @throws NetException 网络异常
     */
    public String responseToString() throws NetException {
        final byte[] bytes          = new byte[SystemConfig.DEFAULT_EXCHANGE_LENGTH];
        final StringBuilder builder = new StringBuilder();
        try (
            final InputStream input = this.response()
        ) {
            int length;
            while((length = input.read(bytes)) >= 0) {
                builder.append(new String(bytes, 0, length));
            }
        } catch (IOException e) {
            throw new NetException(e);
        }
        return builder.toString();
    }
    
    /**
     * @return 响应头
     */
    public HttpHeaderWrapper responseHeader() {
        if(this.httpHeaderWrapper == null) {
            this.httpHeaderWrapper = HttpHeaderWrapper.newInstance(this.httpURLConnection.getHeaderFields());
        }
        return this.httpHeaderWrapper;
    }
    
    /**
     * 关闭连接
     * 关闭连接和底层Socket：不能保持长连接
     * 
     * @return {@link HttpClient}
     */
    public HttpClient shutdown() {
        this.httpURLConnection.disconnect();
        return this;
    }
    
    /**
     * 设置默认请求头
     */
    private void setDefaultHeader() {
        // 接收所有类型参数
        this.header("Accept", "*/*");
        // 设置客户端信息
        this.header(HttpHeaderWrapper.HEADER_USER_AGENT, USER_AGENT);
    }
    
    /**
     * 新建请求连接
     * 
     * @param connectTimeout 连接超时时间（毫秒）
     * @param receiveTimeout 响应超时时间（毫秒）
     * 
     * @return 请求连接
     * 
     * @throws NetException 网络异常
     */
    private HttpURLConnection buildHttpURLConnection(int connectTimeout, int receiveTimeout) throws NetException {
        try {
            final URL requestUrl               = new URL(this.url);
            final HttpURLConnection connection = (HttpURLConnection) requestUrl.openConnection();
            // 是否读取
            connection.setDoInput(true);
            // 是否缓存
            connection.setUseCaches(false);
            // 响应超时时间
            connection.setReadTimeout(receiveTimeout);
            // 连接超时时间
            connection.setConnectTimeout(connectTimeout);
            // 是否自动重定向
            connection.setInstanceFollowRedirects(true);
            return connection;
        } catch (IOException e) {
            throw new NetException(e);
        }
    }
    
    /**
     * 新建SSLContext
     * 
     * @return {@link SSLContext}
     */
    private static final SSLContext buildSSLContext() {
        try {
            // SSL协议：SSL、SSLv2、SSLv3、TLS、TLSv1、TLSv1.1、TLSv1.2、TLSv1.3
            final SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
            sslContext.init(
                null,
                new X509TrustManager[] {
                    SnailTrustManager.INSTANCE
                },
                NumberUtils.random()
            );
            return sslContext;
        } catch (KeyManagementException | NoSuchAlgorithmException e) {
            LOGGER.error("新建SSLContext异常", e);
        }
        try {
            return SSLContext.getDefault();
        } catch (NoSuchAlgorithmException e) {
            LOGGER.error("新建SSLContext异常", e);
        }
        return null;
    }

    /**
     * 域名验证
     * 
     * @author acgist
     */
    public static class SnailHostnameVerifier implements HostnameVerifier {

        private static final SnailHostnameVerifier INSTANCE = new SnailHostnameVerifier();
        
        private SnailHostnameVerifier() {
        }
        
        @Override
        public boolean verify(String requestHost, SSLSession remoteSslSession) {
            // 证书域名必须匹配
            return requestHost.equalsIgnoreCase(remoteSslSession.getPeerHost());
        }
        
    }
    
    /**
     * 证书验证
     * 
     * @author acgist
     */
    public static class SnailTrustManager implements X509TrustManager {

        private static final SnailTrustManager INSTANCE = new SnailTrustManager();

        private SnailTrustManager() {
        }
        
        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return new X509Certificate[0];
        }

        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            if(chain == null) {
                throw new CertificateException("证书验证失败");
            }
        }
        
        @Override
        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            if(chain == null) {
                throw new CertificateException("证书验证失败");
            }
        }
        
    }
    
}
