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

import com.alibaba.fastjson.JSON;
import net.northking.cctp.element.core.IExecuteContext;
import net.northking.cctp.element.internal.protocol.http.auth.AuthScheme;
import net.northking.cctp.element.internal.protocol.http.auth.oauth2.OAuth2Credentials;
import net.northking.cctp.element.internal.utils.FileServer;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.NameValuePair;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.client.AuthCache;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.*;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.client.utils.URIUtils;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.FileEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.MIME;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.message.BasicNameValuePair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;

/**
 * RequestBuilder
 *
 * @author fengxiang.zhang
 * @date 2022/4/14
 * @since 1.0
 */
public class RequestBuilder {

    private static final Logger logger= LoggerFactory.getLogger(RequestBuilder.class);

    private static final int MAX_REDIRECTS = 20;
    private RequestParam rp;
    private HttpClientContext context;

    /**
     * @description: 脚本上下文
     * @author: wei.zhang
     * @date: 2023/2/16 21:21
     *
     */

    private IExecuteContext execContext;

    public static RequestBuilder builder() {
        return new RequestBuilder();
    }

    public RequestBuilder requestParam(RequestParam rp) {
        this.rp = rp;
        return this;
    }

    public RequestBuilder context(HttpClientContext context) {
        this.context = context;
        return this;
    }

    public RequestBuilder scriptContext(IExecuteContext context) {
        this.execContext = context;
        return this;
    }

    public HttpRequestBase build() throws Exception {
        HttpRequestBase httpRequestBase = createHttpRequest(buildQueryUrl(), rp.getMethod(), rp.isFollowingRedirect());
        setupRequest(httpRequestBase);
        return httpRequestBase;
    }

    /**
     * 创建请求
     */
    private static HttpRequestBase createHttpRequest(String url, RequestMethod method, boolean isFollowingRedirect) throws Exception {
        URI uri = URI.create(url);
        switch (method) {
            case POST:
                return new HttpPost(uri);
            case GET:
                return isFollowingRedirect ? new HttpGet(uri) : new HttpGetWithEntity(uri);
            case PUT:
                return new HttpPut(uri);
            case HEAD:
                return new HttpHead(uri);
            case TRACE:
                return new HttpTrace(uri);
            case OPTIONS:
                return new HttpOptions(uri);
            case DELETE:
                return new HttpDelete(uri);
            case PATCH:
                return new HttpPatch(uri);
            default: {
                if (!HttpWebDav.isWebDavMethod(method.name())) {
                    throw new IllegalArgumentException("Unexpected method: '" + method + "'");
                }
                return new HttpWebDav(method.name(), uri);
            }
        }
    }

    private void setupRequest(HttpRequestBase httpRequest) throws Exception {
        RequestConfig.Builder rcb = RequestConfig.custom()
                .setSocketTimeout(rp.getTimeout() * 1000)
                .setConnectTimeout(rp.getTimeout() * 1000)
                .setRedirectsEnabled(rp.isFollowingRedirect())
                .setMaxRedirects(MAX_REDIRECTS);
        httpRequest.setConfig(rcb.build());
        // Header设置
        Map<String, String> headers = rp.getHeaders();
        if (headers != null && headers.size() > 0) {
            headers.forEach((k, v) -> httpRequest.setHeader(k, v));
        }
        // 认证设置
        setupAuthCredentials();
        // 请求体设置
        if (httpRequest instanceof HttpEntityEnclosingRequestBase) {
            String postedBody = this.setupHttpEntityEnclosingRequestData((HttpEntityEnclosingRequestBase) httpRequest);
        }
    }

    /**
     * Query参数设置
     */
    private String buildQueryUrl() throws Exception {
        String url = rp.getUrl();
        String encoding = this.getContentEncoding();
        Map<String, String> queryParams = rp.getQueryParams();
        if (queryParams != null && queryParams.size() > 0) {
            StringBuffer sb = new StringBuffer();
            for (Map.Entry<String, String> entry : queryParams.entrySet()) {
                if (StringUtils.isNotBlank(entry.getKey())) {
                    String key = URLDecoder.decode(entry.getKey());
                    String value = URLDecoder.decode(entry.getValue());
                    sb.append(URLEncoder.encode(key, encoding))
                            .append("=")
                            .append(URLEncoder.encode(value, encoding))
                            .append("&");
                    /*sb.append(URLEncoder.encode(entry.getKey(), encoding))
                            .append("=")
                            .append(URLEncoder.encode(entry.getValue(), encoding))
                            .append("&");*/
                }
            }
            String ps = StringUtils.removeEnd(sb.toString(), "&");
            if (StringUtils.indexOf(url, "?") > -1) {
                url =  url + "&" + ps;
            } else {
                url =  url + "?" + ps;
            }
        }
        logger.debug("请求URL：{}", url);
        return url;
    }

    /**
     * 认证授权设置
     */
    private void setupAuthCredentials() throws Exception {
        RequestParam.Authentication auth = rp.getAuthentication();
        if (auth != null) {
            AuthScheme scheme = AuthScheme.from(auth.scheme);
            CredentialsProvider credentialsProvider = context.getCredentialsProvider();
            HttpHost targetHost = URIUtils.extractHost(new URI(rp.getUrl()));
            AuthCache authCache = context.getAuthCache();
            switch (scheme) {
                case Basic: {
                    /** Basic认证 **/
                    AuthScope authScope = new AuthScope(targetHost, null, AuthScheme.Basic.name());
                    Credentials credentials = auth.toUsernamePasswordCredentials();
                    credentialsProvider.setCredentials(authScope, credentials);
                    authCache.put(targetHost, context.getAuthSchemeRegistry().lookup(AuthScheme.Basic.name()).create(context));
                    break;
                }
                case Digest: {
                    /** Digest认证 **/
                    AuthScope authScope = new AuthScope(targetHost, null, AuthScheme.Digest.name());
                    Credentials credentials = auth.toUsernamePasswordCredentials();
                    credentialsProvider.setCredentials(authScope, credentials);
                    authCache.put(targetHost, context.getAuthSchemeRegistry().lookup(AuthScheme.Digest.name()).create(context));
                    break;
                }
                case NTLM: {
                    /** Microsoft NTLM认证 **/
                    AuthScope authScope = new AuthScope(targetHost, null, AuthScheme.NTLM.name());
                    Credentials credentials = auth.toNTCredentials();
                    credentialsProvider.setCredentials(authScope, credentials);
                    authCache.put(targetHost, context.getAuthSchemeRegistry().lookup(AuthScheme.NTLM.name()).create(context));
                    break;
                }
                case OAuth2: {
                    /** OAuth2 Bearer认证 **/
                    AuthScope authScope = new AuthScope(targetHost, null, AuthScheme.OAuth2.name());
                    OAuth2Credentials credentials = auth.toOAuth2Credentials();
                    credentialsProvider.setCredentials(authScope, credentials);
                    authCache.put(targetHost, context.getAuthSchemeRegistry().lookup(AuthScheme.OAuth2.name()).create(context));
                }
                default: // NONE
            }
        }
    }

    /**
     * 请求Entity组装
     */
    private String setupHttpEntityEnclosingRequestData(HttpEntityEnclosingRequestBase entityEnclosingRequest) throws IOException {
        RequestParam.BodySet body = rp.getBodySet();
        RequestParam.BodyType type = RequestParam.BodyType.from(body.type);
        StringBuilder postedBody = new StringBuilder(1000);
        String contentEncoding = this.getContentEncoding();
        switch (type) {
            case RAW: {
                setContentType(entityEnclosingRequest, contentEncoding);
                StringEntity requestEntity = new StringEntity(body.content, contentEncoding);
                entityEnclosingRequest.setEntity(requestEntity);
                postedBody.append(body.content);
                break;
            }
            case X_WWW_FORM_URLENCODED: {
                List<RequestParam.Parameter> parameters = JSON.parseArray(body.content, RequestParam.Parameter.class);
                UrlEncodedFormEntity entity = this.createUrlEncodedFormEntity(contentEncoding, parameters);
                entityEnclosingRequest.setEntity(entity);
                break;
            }
            case FORM_DATA: {
                List<RequestParam.Parameter> parameters = JSON.parseArray(body.content, RequestParam.Parameter.class);
                entityEnclosingRequest.setEntity(this.createMultipartEntity(contentEncoding, parameters, postedBody));
                break;
            }
            case BINARY: {
                RequestParam.Parameter parameter = JSON.parseObject(body.content, RequestParam.Parameter.class);
                File file = FileServer.getFile(this.execContext,parameter.getPath(), 60, TimeUnit.SECONDS);
                entityEnclosingRequest.setEntity(new FileEntity(file, ContentType.DEFAULT_BINARY));
                postedBody.append("<actual file content, not shown here>");
                break;
            }
            default: // NONE
        }
        return postedBody.toString();
    }

    /**
     * 设置请求Content-Type
     */
    private void setContentType(HttpEntityEnclosingRequestBase entityEnclosingRequest, String encoding) {
        Header contentType = entityEnclosingRequest.getLastHeader("Content-Type");
        if (contentType != null) {
            String[] parts = StringUtils.split(contentType.getValue(), ";");
            if (parts.length > 0) {
                entityEnclosingRequest.removeHeaders("Content-Type");
                entityEnclosingRequest.addHeader("Content-Type", parts[0] + ";charset=" + encoding);
            }
        }
    }

    /**
     * ContentEncoding
     */
    private String getContentEncoding() {
        String ce = rp.getContentEncoding();
        return StringUtils.isBlank(ce) ? "UTF-8" : ce;
    }

    /**
     * UrlEncodedFormEntity
     */
    private UrlEncodedFormEntity createUrlEncodedFormEntity(String encoding, List<RequestParam.Parameter> parameters) throws UnsupportedEncodingException {
        List<NameValuePair> nvpList = new ArrayList();
        String urlEncoding = StringUtils.isBlank(encoding) ? StandardCharsets.UTF_8.name() : encoding;
        parameters.forEach(p -> nvpList.add(new BasicNameValuePair(p.getKey(), p.getValue())));
        return new UrlEncodedFormEntity(nvpList, urlEncoding);
    }

    /**
     * MultipartEntity
     */
    private HttpEntity createMultipartEntity(String encoding, List<RequestParam.Parameter> parameters, StringBuilder postedBody) throws IOException {
        Charset charset = StringUtils.isBlank(encoding) ? MIME.DEFAULT_CHARSET : Charset.forName(encoding);
        MultipartEntityBuilder builder = MultipartEntityBuilder.create().setCharset(charset);
        List<ViewableFileBody> bodyList = new ArrayList<>();
        parameters.forEach(p -> {
            RequestParam.ParamType paramType = RequestParam.ParamType.from(p.getType());
            if (RequestParam.ParamType.FILE == paramType) {
                File file = FileServer.getFile(this.execContext,p.getPath(), 60, TimeUnit.SECONDS);
                ViewableFileBody fileBody = new ViewableFileBody(file, ContentType.DEFAULT_BINARY, p.getValue());
                builder.addPart("file", fileBody);
                bodyList.add(fileBody);
            } else {
                builder.addTextBody(p.getKey(), p.getValue(), ContentType.create("text/plain", charset));
            }
        });
        HttpEntity httpEntity = builder.build();
        this.writeEntity2sb(postedBody, httpEntity, bodyList, encoding);
        return httpEntity;
    }

    /**
     * 日志内容输出
     */
    private void writeEntity2sb(StringBuilder postedBody, HttpEntity entity, List<ViewableFileBody> fileBodies, String contentEncoding) throws IOException {
        if (entity.isRepeatable()) {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            for (ViewableFileBody fileBody : fileBodies) {
                fileBody.hideFileData = true;
            }
            entity.writeTo(bos);
            for (ViewableFileBody fileBody : fileBodies) {
                fileBody.hideFileData = false;
            }
            bos.flush();
            postedBody.append(bos.toString(contentEncoding == null ? StandardCharsets.ISO_8859_1.name() : contentEncoding));
            bos.close();
        } else {
            postedBody.append("<Entity was not repeatable, cannot view what was sent>");
        }
    }

    /**
     * FileBody包装
     */
    private static class ViewableFileBody extends FileBody {

        private boolean hideFileData = false;

        public ViewableFileBody(File file, ContentType contentType, String fileName) {
            super(file, contentType, fileName);
        }

        public void writeTo(OutputStream out) throws IOException {
            if (this.hideFileData) {
                out.write("<actual file content, not shown here>".getBytes());
            } else {
                super.writeTo(out);
            }
        }
    }

    /**
     * Delete请求
     */
    public static final class HttpDelete extends HttpEntityEnclosingRequestBase {

        public HttpDelete(URI uri) {
            this.setURI(uri);
        }

        public String getMethod() {
            return "DELETE";
        }
    }

    /**
     * Get请求扩展
     */
    public static final class HttpGetWithEntity extends HttpEntityEnclosingRequestBase {

        public HttpGetWithEntity(URI uri) {
            this.setURI(uri);
        }

        public String getMethod() {
            return "GET";
        }
    }

    /**
     * HttpWebDav扩展
     */
    public static final class HttpWebDav extends HttpEntityEnclosingRequestBase {

        private static final Pattern WEB_DAV_METHOD_PATTERN = Pattern.compile("(?i)[\\da-z!#$%&'*+\\-.^_`|~]+");

        private final String davMethod;

        public HttpWebDav(String davMethod, URI uri) {
            this.davMethod = davMethod;
            this.setURI(uri);
        }

        public String getMethod() {
            return this.davMethod;
        }

        public static boolean isWebDavMethod(String method) {
            return method != null && WEB_DAV_METHOD_PATTERN.matcher(method).matches();
        }
    }
}
