/*
 * Licensed Materials - Property of HCL
 * UrbanCode Deploy
 * (c) Copyright HCL Technologies Ltd. 2019. All Rights Reserved.
 */
package com.urbancode.urelease.integration.xldeploy.http

import com.urbancode.air.ExitCodeException
import com.urbancode.commons.httpcomponentsutil.CloseableHttpClientBuilder

import groovy.json.JsonSlurper

import org.apache.http.impl.client.CloseableHttpClient
import org.apache.http.client.methods.HttpDelete
import org.apache.http.client.methods.HttpGet
import org.apache.http.client.methods.HttpPut
import org.apache.http.client.methods.HttpPost
import org.apache.http.client.methods.HttpUriRequest
import org.apache.http.entity.StringEntity
import org.apache.http.HttpException
import org.apache.http.util.EntityUtils
import org.apache.log4j.Logger
import org.apache.http.entity.mime.MultipartFormEntity
import org.apache.http.Header
import org.apache.http.HttpEntity
import org.apache.http.HttpResponse
import org.apache.http.StatusLine

/**
 * Helper class used to generate an apache HTTP client, and execute HTTP methods.
 */
public class RestClientHelper {
    private CloseableHttpClientBuilder clientBuilder  // Configuration for building a client
    private CloseableHttpClient client    // Client will be built in the constructor
    private String serverUrl  // Base URL of the server listening for HTTP requests
    private HashMap requestHeaders = [:]  // Request headers 'def headers = ["key1":"val1", "key2":"val2"]'
    private IntRange successRange = 100..300  // Range of successful exit codes
    private Logger logger

    public RestClientHelper(String serverUrl) {
        this(serverUrl, null, null, false)
    }

    public RestClientHelper(String serverUrl, boolean trustAllCerts) {
        this(serverUrl, null, null, trustAllCerts, null)
    }

    public RestClientHelper(String serverUrl, String username, String password, boolean trustAllCerts) {
        this (serverUrl, username, password, trustAllCerts, null)
    }

    /**
     * @param serverUrl Base URL of the server
     * @param username User for basic auth
     * @param password Password for basic auth
     * @param trustAllCerts Set true to ignore SSL verification
     * @param closure Groovy closure to make changes to builder before client is built
     */
    public RestClientHelper(
        String serverUrl,
        String username,
        String password,
        boolean trustAllCerts,
        Closure closure)
    {
        this.serverUrl = serverUrl
        clientBuilder = new CloseableHttpClientBuilder()
        clientBuilder.setPreemptiveAuthentication(true)
        logger = Logger.getLogger(getClass());

        if (username) {
            clientBuilder.setUsername(username)

            if (password) {
                clientBuilder.setPassword(password)
            }
        }

        if (trustAllCerts) {
            clientBuilder.setTrustAllCerts(trustAllCerts)
        }

        if (serverUrl) {
            if (!serverUrl.substring(0, 7).equalsIgnoreCase("http://")
                && !serverUrl.substring(0, 8).equalsIgnoreCase("https://"))
            {
                logger.error("An HTTP protocol (http:// or https://) must be prepended to server URL: ${serverUrl}")
                throw new RuntimeException("Missing HTTP protocol in server URL: ${serverUrl}.")
            }

            if (serverUrl.endsWith("/")) {
                serverUrl = serverUrl.substring(0, serverUrl.length() - 1)
            }
        }
        else {
            logger.error("No server URL was specified, a connection cannot be created.")
            throw new RuntimeException("Missing server URL.")
        }

        /* Change settings directly on the client builder before building the client */
        if (closure) {
            closure(clientBuilder)
        }

        client = clientBuilder.buildClient()

        /* Default headers can be overwritten if specified in requestHeaders */
        requestHeaders.put("Content-Type", "application/json")
        requestHeaders.put("Accept", "application/json")
    }

    /**
     * Directly set a map of HTTP request headers, replacing all existing ones
     *
     * @param headers Map of headers to set 'def headers = ["key1":"val1", "key2":"val2"]'
     */
    public void setRequestHeaders(HashMap headers) {
        this.requestHeaders = headers
    }

    /**
     * Add or replace an existing request header
     *
     * @param name
     * @param value
     */
    public void addRequestHeader(String name, String value) {
        this.requestHeaders.put(name, value)
    }

    /**
     * Remove a request header if it exists
     *
     * @param name
     */
    public void removeRequestHeader(String name) {
        this.requestHeaders.remove(name)
    }

    /**
     * Execute an HTTP PUT request with JSON
     *
     * @param endpoint An absolute URL endpoint, or one relative to the serverUrl
     * @param jsonString String representation of a JSON object
     * @return The HTTP response
     */
    public HttpResponse doPutRequest(String endpoint, String jsonString) {
        HttpPut request = new HttpPut(getAbsoluteUrl(endpoint))
        HttpResponse response = doRequest(request, jsonString)

        return response
    }

    /**
     * Execute an HTTP POST request
     *
     * @param endpoint An absolute url endpoint, or one relative to the serverUrl
     * @param jsonString String representation of a JSON object
     * @return The HTTP response body
     */
    public void doPostRequest(String endpoint) {
        HttpPost request = new HttpPost(getAbsoluteUrl(endpoint))
        doRequest(request)
    }

    /**
     * Execute an HTTP POST request
     *
     * @param endpoint An absolute url endpoint, or one relative to the serverUrl
     * @param jsonString String representation of a JSON object
     * @return The HTTP response body
     */
    public String doPostRequest(String endpoint, String jsonString) {
        HttpPost request = new HttpPost(getAbsoluteUrl(endpoint))
        HttpResponse response = doRequest(request, jsonString)

        return getResponseBody(response)
    }

    /**
     * Execute an HTTP GET request
     * @param endpoint
     * @return
     */
    public String doGetRequest(String endpoint) {
        HttpGet request = new HttpGet(getAbsoluteUrl(endpoint))
        HttpResponse response = doRequest(request)

        return getResponseBody(response)
    }

    /**
     * @param request
     * @return
     */
    public HttpResponse doDeleteRequest(String endpoint) {
        HttpDelete request = new HttpDelete(getAbsoluteUrl(endpoint))
        HttpResponse response = doRequest(request)

        return response
    }


    /**
     * Execute a general HTTP request
     *
     * @param request The HTTP request to send
     * @return The HTTP response
     */
    protected HttpResponse doRequest(HttpUriRequest request) {
        return doRequest(request, (HttpEntity) null)
    }

    /**
     * Specify a JSON string to provide a string entity to the HTTP request
     *
     * @param request
     * @param contentString
     * @param headers Any additional headers to set for the request
     * @return The HTTP response
     */
    protected HttpResponse doRequest(HttpUriRequest request, String contentString) {
        HttpEntity entity = null

        if (contentString) {
            StringEntity input

            try {
                entity = new StringEntity(contentString)
            }
            catch (UnsupportedEncodingException ex) {
                logger.error("Unsupported characters in http request content: ${contentString}")
                throw ex
            }
        }

        return doRequest(request, entity)
    }

    protected HttpResponse doRequest(HttpUriRequest request, HttpEntity httpEntity) {
        if (requestHeaders) {
            for (def header : requestHeaders) {
                request.addHeader(header.key, header.value)
            }
        }

        if (httpEntity) {
            request.setEntity(httpEntity)
        }

        HttpResponse response = client.execute(request)

        StatusLine statusLine = response.getStatusLine()
        String reasonPhrase = statusLine.getReasonPhrase()
        int statusCode = statusLine.getStatusCode()

        if (!successRange.contains(statusCode)) {
            throw new ExitCodeException("HTTP request failed with a response code of " +
                "${statusCode}:${reasonPhrase}: " + response.entity?.content?.text)
        }

        return response
    }

    /**
     * Acquire an absolute URL path from either a relative or absolute one
     * @param endpoint
     * @return
     */
    protected String getAbsoluteUrl(String endpoint) {
        /* Will return the absolute path if it's already provided */
        if (!endpoint.substring(0, 7).equalsIgnoreCase("http://")
            && !endpoint.substring(0, 8).equalsIgnoreCase("https://"))
        {
            if (!endpoint.startsWith('/')) {
                endpoint = '/' + endpoint
            }

            return serverUrl + endpoint
        }
        else {
            return endpoint
        }
    }

    public String getResponseBody(HttpResponse response) {
        return EntityUtils.toString(response.getEntity())
    }

    //return an map of JSON properties from an http response
    public Map<String, String> parseResponse(HttpResponse response) {
        String json = EntityUtils.toString(response.getEntity())
        JsonSlurper slurper = new JsonSlurper()

        return slurper.parseText(json)
    }

    /**
     * Test connection to the server
     */
    public void pingServer() {
        HttpGet ping = new HttpGet(serverUrl)
        HttpResponse response = client.execute(ping)
        def statusLine = response.getStatusLine()
        def statusCode = statusLine.getStatusCode()

        if (!successRange.contains(statusCode)) {
            throw new ExitCodeException("Connection to server URL: ${serverUrl} failed with an " +
                "http status code of ${statusCode}: ${statusLine.getReasonPhrase()}.")
        }
        else {
            logger.info("Connection to server URL: ${serverUrl} successful.")
        }
    }
}
