package org.apereo.cas.util.http;

import org.apache.commons.io.IOUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpStatus;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.FutureRequestExecutionService;
import org.apache.http.impl.client.HttpRequestFutureTask;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.util.Assert;

import java.io.Serializable;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.RejectedExecutionException;
import java.util.stream.Collectors;

/**
 * Implementation of CAS {@link HttpClient}
 * which delegates requests to a {@link #httpClient} instance.
 *
 * @author Jerome Leleu
 * @author Scott Battaglia
 * @author Misagh Moayyed
 * @since 3.1
 */
public class SimpleHttpClient implements HttpClient, Serializable, DisposableBean {

    /**
     * Unique Id for serialization.
     */
    private static final long serialVersionUID = -4949380008568071855L;

    private static final Logger LOGGER = LoggerFactory.getLogger(SimpleHttpClient.class);

    /**
     * the acceptable codes supported by this client.
     */
    private List<Integer> acceptableCodes;

    /**
     * the HTTP client for this client.
     */
    private CloseableHttpClient httpClient;

    /**
     * the request executor service for this client.
     */
    private FutureRequestExecutionService requestExecutorService;

    /**
     * Instantiates a new Simple HTTP client, based on the provided inputs.
     *
     * @param acceptableCodes        the acceptable codes of the client
     * @param httpClient             the HTTP client used by the client
     * @param requestExecutorService the request executor service used by the client
     */
    SimpleHttpClient(final List<Integer> acceptableCodes, final CloseableHttpClient httpClient, final FutureRequestExecutionService requestExecutorService) {
        this.acceptableCodes = acceptableCodes.stream().sorted().collect(Collectors.toList());
        this.httpClient = httpClient;
        this.requestExecutorService = requestExecutorService;
    }

    @Override
    public boolean sendMessageToEndPoint(final HttpMessage message) {
        Assert.notNull(this.httpClient);

        try {
            final HttpPost request = new HttpPost(message.getUrl().toURI());
            request.addHeader("Content-Type", message.getContentType());

            final StringEntity entity = new StringEntity(message.getMessage(), ContentType.create(message.getContentType()));
            request.setEntity(entity);

            final ResponseHandler<Boolean> handler = response -> response.getStatusLine().getStatusCode() == HttpStatus.SC_OK;
            LOGGER.debug("Created HTTP post message payload [{}]", request);
            final HttpRequestFutureTask<Boolean> task = this.requestExecutorService.execute(request, HttpClientContext.create(), handler);
            if (message.isAsynchronous()) {
                return true;
            }
            return task.get();
        } catch (final RejectedExecutionException e) {
            LOGGER.warn(e.getMessage(), e);
            return false;
        } catch (final Exception e) {
            LOGGER.debug(e.getMessage(), e);
            return false;
        }
    }

    @Override
    public HttpMessage sendMessageToEndPoint(final URL url) {
        Assert.notNull(this.httpClient);

        HttpEntity entity = null;

        try (CloseableHttpResponse response = this.httpClient.execute(new HttpGet(url.toURI()))) {
            final int responseCode = response.getStatusLine().getStatusCode();

            for (final int acceptableCode : this.acceptableCodes) {
                if (responseCode == acceptableCode) {
                    LOGGER.debug("Response code received from server matched [{}].", responseCode);
                    entity = response.getEntity();
                    final HttpMessage msg = new HttpMessage(url, IOUtils.toString(entity.getContent(), StandardCharsets.UTF_8));
                    msg.setContentType(entity.getContentType().getValue());
                    msg.setResponseCode(responseCode);
                    return msg;
                }
            }
            LOGGER.warn("Response code [{}] from [{}] did not match any of the acceptable response codes.",
                    responseCode, url);
            if (responseCode == HttpStatus.SC_INTERNAL_SERVER_ERROR) {
                final String value = response.getStatusLine().getReasonPhrase();
                LOGGER.error("There was an error contacting the endpoint: [{}]; The error:\n[{}]", url.toExternalForm(),
                        value);
            }
        } catch (final Exception e) {
            LOGGER.error(e.getMessage(), e);
        } finally {
            EntityUtils.consumeQuietly(entity);
        }
        return null;
    }

    @Override
    public boolean isValidEndPoint(final String url) {
        try {
            final URL u = new URL(url);
            return isValidEndPoint(u);
        } catch (final MalformedURLException e) {
            LOGGER.error(e.getMessage(), e);
            return false;
        }
    }

    @Override
    public boolean isValidEndPoint(final URL url) {
        Assert.notNull(this.httpClient);

        HttpEntity entity = null;

        try (CloseableHttpResponse response = this.httpClient.execute(new HttpGet(url.toURI()))) {
            final int responseCode = response.getStatusLine().getStatusCode();

            final int idx = Collections.binarySearch(this.acceptableCodes, responseCode);
            if (idx >= 0) {
                LOGGER.debug("Response code from server matched [{}].", responseCode);
                return true;
            }

            LOGGER.debug("Response code did not match any of the acceptable response codes. Code returned was [{}]", responseCode);

            if (responseCode == HttpStatus.SC_INTERNAL_SERVER_ERROR) {
                final String value = response.getStatusLine().getReasonPhrase();
                LOGGER.error("There was an error contacting the endpoint: [{}]; The error was:\n[{}]", url.toExternalForm(), value);
            }

            entity = response.getEntity();
        } catch (final Exception e) {
            LOGGER.error(e.getMessage(), e);
        } finally {
            EntityUtils.consumeQuietly(entity);
        }
        return false;
    }

    /**
     * Shutdown the executor service and close the http client.
     *
     * @throws Exception if the executor cannot properly shut down
     */
    @Override
    public void destroy() throws Exception {
        IOUtils.closeQuietly(this.requestExecutorService);
    }

    @Override
    public org.apache.http.client.HttpClient getWrappedHttpClient() {
        return this.httpClient;
    }
}
