package be.iminds.ilabt.jfed.lowlevel.connection;

import org.apache.http.Header;
import org.apache.http.HttpStatus;
import org.apache.http.StatusLine;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.BasicHttpEntity;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.protocol.HttpContext;
import org.apache.xmlrpc.XmlRpcException;
import org.apache.xmlrpc.XmlRpcRequest;
import org.apache.xmlrpc.client.*;
import org.apache.xmlrpc.common.XmlRpcStreamRequestConfig;
import org.apache.xmlrpc.util.HttpUtil;
import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException;

import javax.annotation.Nonnull;
import javax.net.ssl.SSLException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

/**
 * CommonsHttpClientXmlRpcTransportFactory is an implementation of the apache commons XML RPC library {@link XmlRpcTransportFactory} interface.
 * which makes use of the apache commons {@link CloseableHttpClient}.
 * <p/>
 * It also captures the data sent and received, for debugging purposes.
 */
public class CommonsHttpClientXmlRpcTransportFactory implements HttpInfoXmlRpcTransportFactory {
    private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(CommonsHttpClientXmlRpcTransportFactory.class);

    private final XmlRpcClient xmlRpcClient;


    private String httpResult = null;
    private String httpSent = null;

    private String httpRequestHeaders = null;
    private String httpResponseHeaders = null;
    private List<Header> rawHttpResponseHeaders = null;
    private StatusLine httpResultStatusLine = null;
    private String httpRequestLine = null;

    private final MyXmlRpcTransport transport;

    /**
     * Note: the "Http History" automatically resets on new XmlRpc call
     */
    public CommonsHttpClientXmlRpcTransportFactory(XmlRpcClient xmlRpcClient, HttpClientBuilder httpClientBuilder, HttpContext httpContext) {
        this.xmlRpcClient = xmlRpcClient;
        this.transport = new MyXmlRpcTransport(httpClientBuilder, httpContext);
    }


    @Override
    public String getHttpReceivedHistory() {
        return httpResult;
    }

    @Override
    public String getHttpSentHistory() {
        return httpSent;
    }

    @Override
    public StatusLine getHttpStatusLine() {
        return httpResultStatusLine;
    }

    @Override
    public String getHttpRequestLine() {
        return httpRequestLine;
    }

    @Override
    public String getHttpRequestHeaders() {
        return httpRequestHeaders;
    }

    @Override
    public String getHttpResponseHeaders() {
        return httpResponseHeaders;
    }

    @Override
    public List<Header> getRawHttpResponseHeaders() {
        if (rawHttpResponseHeaders == null) return null;
        return rawHttpResponseHeaders;
    }

    private void resetHttpHistory() {
        httpResult = null;
        httpRequestHeaders = null;
        httpResultStatusLine = null;
        httpRequestLine = null;
        httpResponseHeaders = null;
        rawHttpResponseHeaders = null;
        httpSent = null;
    }

    @Override
    public void clearLastCallData() {
        resetHttpHistory();
    }

    @Override
    public void close() {
        try {
            transport.close();
        } catch (XmlRpcClientException ignored) {
            LOG.info("Error while closing transport: {}", ignored.getMessage(), ignored);
        }

        resetHttpHistory();
    }

    @Override
    public XmlRpcTransport getTransport() {
        return transport;
    }

    @Override
    public void setProperty(String s, Object o) {
    }


    class MyXmlRpcTransport extends XmlRpcHttpTransport {
        private final HttpClientBuilder httpClientBuilder;
        private final HttpContext httpContext;

        private CredentialsProvider credsProvider = null;
        private CloseableHttpClient httpClient = null;
        private HttpPost method = null;

        private int contentLength;

        private CloseableHttpResponse response;
        private InputStream responseInputStream;

        public MyXmlRpcTransport(HttpClientBuilder httpClientBuilder, HttpContext httpContext) {
            super(xmlRpcClient, null);
            this.httpClientBuilder = httpClientBuilder;
            this.httpContext = httpContext;
        }

        @Override
        protected void initHttpHeaders(XmlRpcRequest pRequest) throws XmlRpcClientException {
            XmlRpcHttpClientConfig config = (XmlRpcHttpClientConfig) pRequest.getConfig();
            this.method = new HttpPost(config.getServerURL().toString());

            super.initHttpHeaders(pRequest);

        }

        @Override
        protected void setRequestHeader(String pHeader, String pValue) {
            //parent XmlRpcHttpTransport wants to handle the user agent, but Apache HttpClient already takes care of this!
            if (!Objects.equals(pHeader, "User-Agent"))
                method.setHeader(pHeader, pValue);
        }

        @Override
        protected void close() throws XmlRpcClientException {
            try {
                if (response != null)
                    response.close();
            } catch (IOException e) {
                throw new XmlRpcClientException("Error while closing HttpClient", e);
            }
        }

        @Override
        protected boolean isResponseGzipCompressed(XmlRpcStreamRequestConfig pConfig) {
            Header h = response.getFirstHeader("Content-Encoding");

            return h != null && HttpUtil.isUsingGzipEncoding(h.getValue());
        }

        @Override
        protected InputStream getInputStream() throws XmlRpcException {
            return responseInputStream;
        }

        @Override
        protected void setContentLength(int pLength) {
            this.contentLength = pLength;
        }

        @Override
        protected void writeRequest(ReqWriter pWriter) throws XmlRpcException, IOException, SAXException {
            ByteArrayOutputStream bOutRequest = new ByteArrayOutputStream(contentLength);
            pWriter.write(bOutRequest);

            httpSent = bOutRequest.toString();

            BasicHttpEntity requestEntity = new BasicHttpEntity();
            requestEntity.setContent(new ByteArrayInputStream(bOutRequest.toByteArray()));
            requestEntity.setContentLength(contentLength);
            method.setEntity(requestEntity);

            //now first do some logging of the request
            httpRequestLine = method.getRequestLine().toString();
            httpRequestHeaders = logHeaders(method.getAllHeaders());

            if (credsProvider != null) {
                LOG.info("Setting CredentialsProvider to {}", credsProvider);
                httpClientBuilder.setDefaultCredentialsProvider(credsProvider);
            }

            this.httpClient = httpClientBuilder.build();

            try {
                response = httpClient.execute(method, httpContext);

                if (response.getStatusLine() != null)
                    httpResultStatusLine = response.getStatusLine();

                if (response.getAllHeaders() != null) {
                    httpResponseHeaders = logHeaders(response.getAllHeaders());
                    rawHttpResponseHeaders = Arrays.asList(response.getAllHeaders());
                }

                if (response.getEntity() != null) {
                    ByteArrayOutputStream bOutResponse = new ByteArrayOutputStream();
                    response.getEntity().writeTo(bOutResponse);

                    httpResult = bOutResponse.toString();
                    responseInputStream = new ByteArrayInputStream(bOutResponse.toByteArray());

                }

                if (response.getStatusLine() != null &&
                        (response.getStatusLine().getStatusCode() == HttpStatus.SC_INTERNAL_SERVER_ERROR
                                || response.getStatusLine().getStatusCode() == HttpStatus.SC_SERVICE_UNAVAILABLE))
                    throw new HttpServerErrorException(response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase());
            } catch (javax.net.ssl.SSLPeerUnverifiedException ex) {
                throw new XmlRpcClientException("The server certificate for '" + method.getURI() + "' could not be verified. " +
                        "Error message \"" + ex.getMessage() + "\". " +
                        "Possible causes:\n" +
                        "  - The server's self-signed certificate is not in our trust store. \n" +
                        "  - The server's certificate \"CN\" field is not the server hostname or a known alias. \n" +
                        "  - The server is not accepting our client login certificate+key pair (perhaps is is not federated with our \"login provider\"). \n" +
                        "  - If the server's certificate is not self signed, we might not have the root certificate of the trust chain in our trust store.", ex);

            } catch (SSLException ex) {
                method.abort();
                throw new XmlRpcClientException("SSLException: " + ex.getMessage(), ex);
            } catch (HttpServerErrorException ex) {
                method.abort();
                throw new XmlRpcClientException("HTTP Server error", ex);
            }
        }


        @Override
        protected void setCredentials(XmlRpcHttpClientConfig pConfig) throws XmlRpcClientException {

            if (pConfig.getBasicUserName() != null) {
                int port;
                if (pConfig.getServerURL().getPort() <= 0)
                    port = pConfig.getServerURL().getDefaultPort();
                else
                    port = pConfig.getServerURL().getPort();


                credsProvider = new BasicCredentialsProvider();
                credsProvider.setCredentials(
                        new AuthScope(pConfig.getServerURL().getHost(), port),
                        new UsernamePasswordCredentials(pConfig.getBasicUserName(), pConfig.getBasicPassword())
                );
            }
        }
    }

    @Nonnull
    private static String logHeaders(Header[] allHeaders) {
        StringBuilder headers = new StringBuilder();
        for (Header header : allHeaders) {
            headers.append(header.toString()).append("\n");
        }
        headers.append("\n");
        return headers.toString();
    }

}
