package cn.jiguang.common.connection;

import cn.jiguang.common.ClientConfig;
import cn.jiguang.common.resp.APIConnectionException;
import cn.jiguang.common.resp.APIRequestException;
import cn.jiguang.common.resp.ResponseWrapper;
import cn.jiguang.common.utils.StringUtils;

import org.apache.http.*;
import org.apache.http.client.HttpRequestRetryHandler;
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.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.LayeredConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.FileEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import java.io.*;
import java.net.URI;
import java.net.UnknownHostException;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * Apache HttpClient 实现的版本,提供了连接池来实现高并发网络请求。
 */
public class ApacheHttpClient implements IHttpClient {

    private static Logger LOG = LoggerFactory.getLogger(ApacheHttpClient.class);

    private static CloseableHttpClient _httpClient = null;
    private static PoolingHttpClientConnectionManager _cm;
    private final static Object syncLock = new Object();
    private final int _connectionTimeout;
    private final int _connectionRequestTimeout;
    private final int _socketTimeout;
    private final int _maxRetryTimes;
    private String _authCode;
    private HttpHost _proxy;
    // 最大连接数
    private int _maxConnectionCount = 200;
    // 每个路由的最大连接数
    private int _maxConnectionPerRoute = 40;
    // 目标主机的最大连接数
    private int _maxRoute = 100;

    private final String _encryptType;

    public ApacheHttpClient(String authCode, HttpProxy proxy, ClientConfig config) {
        _maxRetryTimes = config.getMaxRetryTimes();
        _connectionTimeout = config.getConnectionTimeout();
        _connectionRequestTimeout = config.getConnectionRequestTimeout();
        _socketTimeout = config.getSocketTimeout();
        _authCode = authCode;
        _encryptType = config.getEncryptType();
        if (proxy != null) {
            _proxy = new HttpHost(proxy.getHost(), proxy.getPort());
        }
    }

    private void configHttpRequest(HttpRequestBase httpRequestBase) {
        RequestConfig requestConfig;
        if (_proxy != null) {
            requestConfig = RequestConfig.custom()
                    .setConnectionRequestTimeout(_connectionRequestTimeout)
                    .setConnectTimeout(_connectionTimeout)
                    .setSocketTimeout(_socketTimeout)
                    .setProxy(_proxy)
                    .build();
        } else {
            requestConfig = RequestConfig.custom()
                    .setConnectionRequestTimeout(_connectionRequestTimeout)
                    .setConnectTimeout(_connectionTimeout)
                    .setSocketTimeout(_socketTimeout)
                    .build();
        }

        httpRequestBase.setConfig(requestConfig);
    }

    public CloseableHttpClient getHttpClient(String url) {
        String hostname = url.split("/")[2];
        int port = 80;
        if (hostname.contains(":")) {
            String[] arr = hostname.split(":");
            hostname = arr[0];
            port = Integer.parseInt(arr[1]);
        }
        if (_httpClient == null) {
            synchronized (syncLock) {
                if (_httpClient == null) {
                    _httpClient = createHttpClient(_maxConnectionCount, _maxConnectionPerRoute, _maxRoute, hostname, port);
                }
            }
        }
        return _httpClient;

    }

    /**
     * 设置最大连接数
     * @param count 连接数
     */
    public void setMaxConnectionCount(int count) {
        this._maxConnectionCount = count;
    }

    /**
     * 设置每个路由最大连接数
     * @param count 连接数
     */
    public void setMaxConnectionPerRoute(int count) {
        this._maxConnectionPerRoute = count;
    }

    /**
     * 设置目标主机最大连接数
     * @param count 连接数
     */
    public void setMaxHostConnection(int count) {
        this._maxRoute = count;
    }

    public CloseableHttpClient createHttpClient(int maxTotal, int maxPerRoute, int maxRoute,
                                                String hostname, int port) {
        ConnectionSocketFactory plainsf = PlainConnectionSocketFactory
                .getSocketFactory();
        LayeredConnectionSocketFactory sslsf = SSLConnectionSocketFactory
                .getSocketFactory();
        Registry<ConnectionSocketFactory> registry = RegistryBuilder
                .<ConnectionSocketFactory>create().register("http", plainsf)
                .register("https", sslsf).build();
        _cm = new PoolingHttpClientConnectionManager(
                registry);
        // 将最大连接数增加
        _cm.setMaxTotal(maxTotal);
        // 将每个路由基础的连接增加
        _cm.setDefaultMaxPerRoute(maxPerRoute);
        HttpHost httpHost = new HttpHost(hostname, port);
        // 将目标主机的最大连接数增加
        _cm.setMaxPerRoute(new HttpRoute(httpHost), maxRoute);

        // 请求重试处理
        HttpRequestRetryHandler httpRequestRetryHandler = new HttpRequestRetryHandler() {
            public boolean retryRequest(IOException exception,
                                        int executionCount, HttpContext context) {
                if (executionCount >= _maxRetryTimes) {
                    return false;
                }
                if (exception instanceof NoHttpResponseException) {// 如果服务器丢掉了连接,那么就重试
                    return true;
                }
                if (exception instanceof SSLHandshakeException) {// 不要重试SSL握手异常
                    return false;
                }
                if (exception instanceof InterruptedIOException) {// 超时
                    return false;
                }
                if (exception instanceof UnknownHostException) {// 目标服务器不可达
                    return false;
                }
                if (exception instanceof ConnectTimeoutException) {// 连接被拒绝
                    return false;
                }
                if (exception instanceof SSLException) {// SSL握手异常
                    return false;
                }

                HttpClientContext clientContext = HttpClientContext
                        .adapt(context);
                HttpRequest request = clientContext.getRequest();
                // 如果请求是幂等的,就再次尝试
                if (!(request instanceof HttpEntityEnclosingRequest)) {
                    return true;
                }
                return false;
            }
        };

        CloseableHttpClient httpClient = HttpClients.custom()
                .setConnectionManager(_cm)
                .setRetryHandler(httpRequestRetryHandler).build();

        return httpClient;

    }


    @Override
    public ResponseWrapper sendGet(String url) throws APIConnectionException, APIRequestException {
        ResponseWrapper wrapper = new ResponseWrapper();
        CloseableHttpResponse response = null;
        HttpGet httpGet = new HttpGet(url);
        try {
            httpGet.setHeader(HttpHeaders.AUTHORIZATION, _authCode);
            if (!StringUtils.isEmpty(_encryptType)) {
                httpGet.setHeader("X-Encrypt-Type", _encryptType);
            }
            configHttpRequest(httpGet);
            response = getHttpClient(url).execute(httpGet, HttpClientContext.create());
            processResponse(response, wrapper);
        } catch (IOException e) {
            httpGet.abort();
            LOG.debug(IO_ERROR_MESSAGE, e);
            throw new APIConnectionException(READ_TIMED_OUT_MESSAGE, e, true);
        } finally {
            try {
                if (response != null) {
                    response.close();
                }
            } catch (IOException e) {
            	 e.printStackTrace();
            }
        }
        return wrapper;

    }

    public ResponseWrapper sendGet(String url, String content)
            throws APIConnectionException, APIRequestException {
        ResponseWrapper wrapper = new ResponseWrapper();
        CloseableHttpResponse response = null;
        HttpGet httpGet = new HttpGet(url);
        try {
            httpGet.setHeader(HttpHeaders.AUTHORIZATION, _authCode);
            if (!StringUtils.isEmpty(_encryptType)) {
                httpGet.setHeader("X-Encrypt-Type", _encryptType);
            }
            httpGet.setHeader("Content-Type", NativeHttpClient.CONTENT_TYPE_JSON);
            configHttpRequest(httpGet);
            response = getHttpClient(url).execute(httpGet, HttpClientContext.create());
            processResponse(response, wrapper);
        } catch (IOException e) {
            httpGet.abort();
            LOG.debug(IO_ERROR_MESSAGE, e);
            throw new APIConnectionException(READ_TIMED_OUT_MESSAGE, e, true);
        } finally {
            try {
                if (response != null) {
                    response.close();
                }
            } catch (IOException e) {
            	 e.printStackTrace();
            }
        }
        return wrapper;
    }

    @Override
    public ResponseWrapper sendDelete(String url) throws APIConnectionException, APIRequestException {
        ResponseWrapper wrapper = new ResponseWrapper();
        CloseableHttpResponse response = null;
        HttpDelete httpDelete = new HttpDelete(url);
        try {
            httpDelete.setHeader(HttpHeaders.AUTHORIZATION, _authCode);
            configHttpRequest(httpDelete);
            response = getHttpClient(url).execute(httpDelete, HttpClientContext.create());
            processResponse(response, wrapper);
        } catch (IOException e) {
            httpDelete.abort();
            LOG.debug(IO_ERROR_MESSAGE, e);
            throw new APIConnectionException(READ_TIMED_OUT_MESSAGE, e, true);
        } finally {
            try {
                if (response != null) {
                    response.close();
                }
            } catch (IOException e) {
            	 e.printStackTrace();
            }
        }
        return wrapper;
    }

    public ResponseWrapper sendDelete(String url, String content)
            throws APIConnectionException, APIRequestException {
        ResponseWrapper wrapper = new ResponseWrapper();
        CloseableHttpResponse response = null;
        HttpDeleteWithBody httpDelete = new HttpDeleteWithBody(url);
        try {
            httpDelete.setHeader(HttpHeaders.AUTHORIZATION, _authCode);
            httpDelete.setHeader("Content-Type", "application/json");
            configHttpRequest(httpDelete);
            StringEntity params = new StringEntity(StringUtils.notNull(content), CHARSET);
            httpDelete.setEntity(params);
            response = getHttpClient(url).execute(httpDelete, HttpClientContext.create());
            processResponse(response, wrapper);
        } catch (IOException e) {
            httpDelete.abort();
            LOG.debug(IO_ERROR_MESSAGE, e);
            throw new APIConnectionException(READ_TIMED_OUT_MESSAGE, e, true);
        } finally {
            try {
                if (response != null) {
                    response.close();
                }
            } catch (IOException e) {
            	 e.printStackTrace();
            }
        }
        return wrapper;
    }

    @Override
    public ResponseWrapper sendPost(String url, String content) throws APIConnectionException, APIRequestException {
        ResponseWrapper wrapper = new ResponseWrapper();
        CloseableHttpResponse response = null;
        HttpPost httpPost = new HttpPost(url);
        try {
            httpPost.setHeader(HttpHeaders.AUTHORIZATION, _authCode);
            if (!StringUtils.isEmpty(_encryptType)) {
                httpPost.setHeader("X-Encrypt-Type", _encryptType);
            }
            httpPost.setHeader("Content-Type", "application/json");
            configHttpRequest(httpPost);
            StringEntity params = new StringEntity(StringUtils.notNull(content), CHARSET);
            httpPost.setEntity(params);
            response = getHttpClient(url).execute(httpPost, HttpClientContext.create());
            processResponse(response, wrapper);
        } catch (IOException e) {
            httpPost.abort();
            LOG.debug(IO_ERROR_MESSAGE, e);
            throw new APIConnectionException(READ_TIMED_OUT_MESSAGE, e, true);
        } finally {
            try {
                if (response != null) {
                    response.close();
                }
            } catch (IOException e) {
            	 e.printStackTrace();
            }
        }
        return wrapper;
    }

    @Override
    public ResponseWrapper sendPut(String url, String content) throws APIConnectionException, APIRequestException {
//        return doRequest(url, content, RequestMethod.PUT);
        ResponseWrapper wrapper = new ResponseWrapper();
        CloseableHttpResponse response = null;
        HttpPut httpPut = new HttpPut(url);
        try {
            httpPut.setHeader(HttpHeaders.AUTHORIZATION, _authCode);
            if (!StringUtils.isEmpty(_encryptType)) {
                httpPut.setHeader("X-Encrypt-Type", _encryptType);
            }
            httpPut.setHeader("Content-Type", "application/json");
            configHttpRequest(httpPut);
            StringEntity params = new StringEntity(StringUtils.notNull(content), CHARSET);
            httpPut.setEntity(params);
            response = getHttpClient(url).execute(httpPut, HttpClientContext.create());
            processResponse(response, wrapper);
        } catch (IOException e) {
            httpPut.abort();
            LOG.debug(IO_ERROR_MESSAGE, e);
            throw new APIConnectionException(READ_TIMED_OUT_MESSAGE, e, true);
        } finally {
            try {
                if (response != null) {
                    response.close();
                }
            } catch (IOException e) {
            	 e.printStackTrace();
            }
        }
        return wrapper;
    }

    public ResponseWrapper uploadFile(String url, String path, String fileType) throws APIConnectionException, APIRequestException {
        LOG.info("Upload file: " + url + "filePath:" + path);
        ResponseWrapper wrapper = new ResponseWrapper();
        File file = new File(path);
        if (!file.exists() || file.isDirectory()) {
            LOG.error("File not exist!");
            wrapper.setErrorObject();
            return wrapper;
        }
        String boundary = "---------------------------" + new Date().getTime();
        CloseableHttpResponse response = null;
        try {
            HttpPost httpPost = new HttpPost(url);
            httpPost.setHeader(HttpHeaders.AUTHORIZATION, _authCode);
            FileInputStream fis = new FileInputStream(file);
            File tempFile = File.createTempFile(new SimpleDateFormat("yyyy-MM-dd HH:mm").format(new Date()), null);
            FileOutputStream fos = new FileOutputStream(tempFile);
            fos.write((boundary + "\r\n").getBytes());
            fos.write(("Content-Disposition: form-data; name=\"" + fileType + "\"; filename=\"" + file.getName() + "\"\r\n").getBytes());
            BufferedInputStream bis = new BufferedInputStream(fis);
            byte[] buff = new byte[8096];
            int len = 0;
            while ((len = bis.read(buff)) != -1) {
                fos.write(buff, 0, len);
            }
            fos.write(("\r\n--" + boundary + "--\r\n").getBytes());
            FileEntity entity = new FileEntity(tempFile, ContentType.MULTIPART_FORM_DATA);
            entity.setContentEncoding("UTF-8");
            httpPost.setEntity(entity);
            response = getHttpClient(url).execute(httpPost);
            processResponse(response, wrapper);
        } catch (IOException e) {
        	LOG.debug(IO_ERROR_MESSAGE, e);
            throw new APIConnectionException(READ_TIMED_OUT_MESSAGE, e, true);
        } finally {
            try {
                if (response != null) {
                    response.close();
                }
            } catch (IOException e) {
            	 e.printStackTrace();
            }
        }
        return wrapper;
    }

    public void processResponse(CloseableHttpResponse response, ResponseWrapper wrapper)
            throws APIConnectionException, APIRequestException, IOException {
        HttpEntity entity = response.getEntity();
        LOG.debug("Response", response.toString());
        int status = response.getStatusLine().getStatusCode();
        String responseContent = "";
        if(entity != null){
        	responseContent = EntityUtils.toString(entity, "utf-8");
        }
        wrapper.responseCode = status;
        wrapper.responseContent = responseContent;
        String quota = getFirstHeader(response, RATE_LIMIT_QUOTA);
        String remaining = getFirstHeader(response, RATE_LIMIT_Remaining);
        String reset = getFirstHeader(response, RATE_LIMIT_Reset);
        wrapper.setRateLimit(quota, remaining, reset);

        LOG.debug(wrapper.responseContent);
        EntityUtils.consume(entity);
        if (status >= 200 && status < 300) {
            LOG.debug("Succeed to get response OK - responseCode:" + status);
            LOG.debug("Response Content - " + responseContent);

        } else if (status >= 300 && status < 400) {
            LOG.warn("Normal response but unexpected - responseCode:" + status + ", responseContent:" + responseContent);

        } else {
            LOG.warn("Got error response - responseCode:" + status + ", responseContent:" + responseContent);

            switch (status) {
                case 400:
                    LOG.error("Your request params is invalid. Please check them according to error message.");
                    wrapper.setErrorObject();
                    break;
                case 401:
                    LOG.error("Authentication failed! Please check authentication params according to docs.");
                    wrapper.setErrorObject();
                    break;
                case 403:
                    LOG.error("Request is forbidden! Maybe your appkey is listed in blacklist or your params is invalid.");
                    wrapper.setErrorObject();
                    break;
                case 404:
                    LOG.error("Request page is not found! Maybe your params is invalid.");
                    wrapper.setErrorObject();
                    break;
                case 410:
                    LOG.error("Request resource is no longer in service. Please according to notice on official website.");
                    wrapper.setErrorObject();
                case 429:
                    LOG.error("Too many requests! Please review your appkey's request quota.");
                    wrapper.setErrorObject();
                    break;
                case 500:
                case 502:
                case 503:
                case 504:
                    LOG.error("Seems encountered server error. Maybe JPush is in maintenance? Please retry later.");
                    break;
                default:
                    LOG.error("Unexpected response.");
            }

            throw new APIRequestException(wrapper);
        }
    }

    private static String getFirstHeader(CloseableHttpResponse response, String name) {
        Header header = response.getFirstHeader(name);
        return header == null ? null : header.getValue();
    }

    public void close() {
        try {
            if (_httpClient != null) {
                _httpClient.close();
            }
            if (_cm != null) {
                _cm.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}


class HttpDeleteWithBody extends HttpEntityEnclosingRequestBase {
    public static final String METHOD_NAME = "DELETE";

    public String getMethod() {
        return METHOD_NAME;
    }

    public HttpDeleteWithBody(final String uri) {
        super();
        setURI(URI.create(uri));
    }
}