package com.usthe.collector.common.http;

import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpRequest;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.impl.client.RequestWrapper;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.Args;

import javax.net.ssl.SSLException;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

/**
 * http request网络请求重试处理器,替换默认的 DefaultHttpRequestRetryHandler
 * 判断是否需要重试请求
 * @author tomsun28
 * @date 2021/8/30 22:00
 */
@Slf4j
public class CustomHttpRequestRetryHandler implements HttpRequestRetryHandler {

    /**
     * 不需要重试的异常情况
     */
    private final static Set<Class<? extends IOException>> NON_RETRY_CLASSES = new HashSet<>(Arrays.asList(
            // io 中断
            InterruptedIOException.class,
            // 对端不可达
            UnknownHostException.class,
            // 连接异常
            ConnectException.class,
            // ssl异常
            SSLException.class));
    /**
     * 最大重试次数
     */
    private static final int MAX_RETRY_TIMES = 3;

    @Override
    public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {
        Args.notNull(exception, "Exception parameter");
        Args.notNull(context, "HTTP context");
        if (executionCount > MAX_RETRY_TIMES) {
            return false;
        }
        if (NON_RETRY_CLASSES.contains(exception.getClass())) {
            return false;
        }
        for (final Class<? extends IOException> rejectException : NON_RETRY_CLASSES) {
            if (rejectException.isInstance(exception)) {
                return false;
            }
        }
        // SocketTimeoutException: Read timed 只做一次重试
        // read time out 可能是对端不支持长连接 重试前清理边空闲无效连接
        if (executionCount == 1 && exception instanceof SocketTimeoutException && exception.getMessage() != null
                && (exception.getMessage().contains("Read timed out")
                || exception.getMessage().contains("Request already aborted"))) {
            // 清理连接池无效空闲连接
            HttpClientPool.getConnectionManager().closeExpiredConnections();
            return true;
        }

        final HttpClientContext clientContext = HttpClientContext.adapt(context);
        final HttpRequest request = clientContext.getRequest();

        if(requestIsAborted(request)){
            return false;
        }

        if (handleAsIdempotent(request)) {
            // Retry if the request is considered idempotent
            return true;
        }

        if (!clientContext.isRequestSent()) {
            // Retry if the request has not been sent fully or
            // if it's OK to retry methods that have been sent
            return true;
        }
        // otherwise do not retry
        return false;
    }

    /**
     * @since 4.2
     */
    protected boolean handleAsIdempotent(final HttpRequest request) {
        return !(request instanceof HttpEntityEnclosingRequest);
    }

    /**
     * @since 4.2
     *
     * @deprecated (4.3)
     */
    @Deprecated
    protected boolean requestIsAborted(final HttpRequest request) {
        HttpRequest req = request;
        if (request instanceof RequestWrapper) {
            // does not forward request to original
            req = ((RequestWrapper) request).getOriginal();
        }
        return (req instanceof HttpUriRequest && ((HttpUriRequest)req).isAborted());
    }
}
