package org.jboss.resteasy.client.jaxrs.engines;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.util.List;
import java.util.Map;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;

import jakarta.ws.rs.ProcessingException;
import jakarta.ws.rs.client.Invocation;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;

import org.jboss.resteasy.client.jaxrs.ClientHttpEngine;
import org.jboss.resteasy.client.jaxrs.i18n.Messages;
import org.jboss.resteasy.client.jaxrs.internal.ClientInvocation;
import org.jboss.resteasy.client.jaxrs.internal.ClientResponse;
import org.jboss.resteasy.client.jaxrs.internal.FinalizedClientResponse;
import org.jboss.resteasy.tracing.RESTEasyTracingLogger;
import org.jboss.resteasy.util.CaseInsensitiveMap;

/**
 * @author <a href="mailto:alexey.ogarkov@gmail.com">Alexey Ogarkov</a>
 * @version $Revision: 1 $
 */
public class URLConnectionEngine implements ClientHttpEngine {

    protected SSLContext sslContext;
    protected HostnameVerifier hostnameVerifier;
    protected Integer readTimeout;
    protected Integer connectTimeout;
    protected String proxyHost;
    protected Integer proxyPort;
    protected String proxyScheme;
    protected boolean followRedirects;

    /**
     * {@inheritDoc}
     */
    @Override
    public Response invoke(Invocation inv) {
        ClientInvocation request = (ClientInvocation) inv;
        final HttpURLConnection connection;

        final int status;
        try {

            connection = createConnection(request);

            executeRequest(request, connection);

            status = connection.getResponseCode();
        } catch (IOException e) {
            throw new ProcessingException(Messages.MESSAGES.unableToInvokeRequest(e.toString()), e);
        }

        //Creating response with stream content
        ClientResponse response = new FinalizedClientResponse(request.getClientConfiguration(), RESTEasyTracingLogger.empty()) {
            private InputStream stream;

            @Override
            protected InputStream getInputStream() {
                if (stream == null) {
                    try {
                        stream = (status < 300) ? connection.getInputStream() : connection.getErrorStream();
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }

                return stream;
            }

            @Override
            protected void setInputStream(InputStream is) {
                stream = is;
                resetEntity();
            }

            @Override
            public void releaseConnection() throws IOException {
                releaseConnection(false);
            }

            @Override
            public void releaseConnection(boolean consumeInputStream) throws IOException {
                InputStream is = getInputStream();
                if (is != null) {
                    // https://docs.oracle.com/javase/8/docs/technotes/guides/net/http-keepalive.html
                    if (consumeInputStream) {
                        while (is.read() > 0) {
                        }
                    }
                    is.close();
                }
                connection.disconnect();
            }

        };

        //Setting attributes
        response.setStatus(status);
        response.setHeaders(getHeaders(connection));

        return response;
    }

    /**
     * Create map with response headers.
     *
     * @param connection - HttpURLConnection
     * @return map key - list of values
     */
    protected MultivaluedMap<String, String> getHeaders(
            final HttpURLConnection connection) {
        MultivaluedMap<String, String> headers = new CaseInsensitiveMap<String>();

        for (Map.Entry<String, List<String>> header : connection.getHeaderFields()
                .entrySet()) {
            if (header.getKey() != null) {
                for (String value : header.getValue()) {
                    headers.add(header.getKey(), value);
                }
            }
        }
        return headers;
    }

    @Override
    public void close() {
        //empty
    }

    /**
     * Create HttpUrlConnection from ClientInvorcation and set request method.
     *
     * @param request ClientInvocation
     * @return HttpURLConnection with method {@literal &} url already set
     * @throws IOException if url or io exceptions
     */
    protected HttpURLConnection createConnection(final ClientInvocation request) throws IOException {
        Proxy proxy = null;
        if (this.proxyHost != null && this.proxyPort != null) {
            proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(this.proxyHost, this.proxyPort));
        } else {
            proxy = Proxy.NO_PROXY;
        }

        HttpURLConnection connection = (HttpURLConnection) request.getUri().toURL().openConnection(proxy);
        connection.setRequestMethod(request.getMethod());

        if (this.connectTimeout != null) {
            connection.setConnectTimeout(this.connectTimeout);
        }
        if (this.readTimeout != null) {
            connection.setReadTimeout(this.readTimeout);
        }

        return connection;
    }

    /**
     * Execute request using HttpURLConnection with body from invocation if needed.
     *
     * @param request    ClientInvocation
     * @param connection HttpURLConnection
     */
    protected void executeRequest(final ClientInvocation request, HttpURLConnection connection) {
        connection.setInstanceFollowRedirects(request.getMethod().equals("GET"));

        if (request.getEntity() != null) {
            if (request.getMethod().equals("GET"))
                throw new ProcessingException(Messages.MESSAGES.getRequestCannotHaveBody());

            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            request.getDelegatingOutputStream().setDelegate(baos);
            try {

                request.writeRequestBody(request.getEntityStream());
                baos.close();
                commitHeaders(request, connection);
                connection.setDoOutput(true);
                OutputStream os = connection.getOutputStream();
                os.write(baos.toByteArray());
                os.flush();
                os.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        } else // no body
        {
            commitHeaders(request, connection);
        }
    }

    /**
     * Add headers to HttpURLConnection from ClientInvocation. Should be executed before writing body.
     *
     * @param request    ClientInvocation
     * @param connection HttpURLConnection
     */
    protected void commitHeaders(ClientInvocation request, HttpURLConnection connection) {
        MultivaluedMap<String, String> headers = request.getHeaders().asMap();
        for (Map.Entry<String, List<String>> header : headers.entrySet()) {
            List<String> values = header.getValue();
            for (String value : values) {
                connection.addRequestProperty(header.getKey(), value);
            }
        }
    }

    /**
     * {inheritDoc}
     */
    @Override
    public SSLContext getSslContext() {
        return sslContext;
    }

    /**
     * {inheritDoc}
     */
    @Override
    public HostnameVerifier getHostnameVerifier() {
        return hostnameVerifier;
    }

    public void setSslContext(SSLContext sslContext) {
        this.sslContext = sslContext;
    }

    public void setHostnameVerifier(HostnameVerifier hostnameVerifier) {
        this.hostnameVerifier = hostnameVerifier;
    }

    public void setConnectTimeout(Integer connectTimeout) {
        this.connectTimeout = connectTimeout;
    }

    public void setReadTimeout(Integer readTimeout) {
        this.readTimeout = readTimeout;
    }

    public void setProxyHost(String proxyHost) {
        this.proxyHost = proxyHost;
    }

    public void setProxyPort(Integer proxyPort) {
        this.proxyPort = proxyPort;
    }

    public void setProxyScheme(String proxyScheme) {
        this.proxyScheme = proxyScheme;
    }

    public void setFollowRedirects(boolean followRedirects) {
        this.followRedirects = followRedirects;
    }

    public boolean isFollowRedirects() {
        return this.followRedirects;
    }
}
