package org.alm;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import javax.ws.rs.HttpMethod;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.Invocation.Builder;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.NewCookie;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.glassfish.jersey.uri.UriComponent;

public class RestConnector
{
    private final static Logger Log = LoggerFactory.getLogger(RestConnector.class);

    private String host;
    private String port;
    private String domain;
    private String project;

    private Map<String, Cookie> cookies = new HashMap<String, Cookie>();

    private RestConnector()
    {
    }

    private static class RestConnectorHolder
    {
        static final RestConnector Instance = new RestConnector();
    }

    public static RestConnector instance()
    {
        return RestConnectorHolder.Instance;
    }

    public void init(String host, String port, String domain, String project)
    {
        this.host = host;
        this.port = port;
        this.domain = domain;
        this.project = project;
    }

    public static MultivaluedMap<String, Object> createBasicAuthHeader(String username, String password)
    {
        byte[] credBytes = (username + ":" + password).getBytes();
        String credEncodedString = "Basic " + Base64.encodeBase64String(credBytes);

        MultivaluedMap<String, Object> authHeader = new MultivaluedHashMap<String, Object>();
        authHeader.add(HttpHeaders.AUTHORIZATION, credEncodedString);

        return authHeader;
    }

    public String host()
    {
        return host;
    }

    public String port()
    {
        return port;
    }

    public String domain()
    {
        return domain;
    }

    public String project()
    {
        return project;
    }

    public String buildUrl(String path) throws Exception
    {
        if(StringUtils.isNotBlank(host) && StringUtils.isNotBlank(port))
        {
            path = path.startsWith("/") ? path.substring(1) : path;

            return String.format("http://%s:%s/%s", host, port, path);
        }

        throw new Exception("Host/Port are invalid. Call init() to initialize them properly.");
    }

    public String buildEntityCollectionUrl(String entityType) throws Exception
    {
        if(StringUtils.isNotBlank(domain) && StringUtils.isNotBlank(project))
        {
            return String.format("/qcbin/rest/domains/%s/projects/%s/%ss", domain, project, entityType);
        }

        throw new Exception("Domain/Project are invalid. Call init() to initialize them properly.");
    }

    public String buildEntityUrl(String entityType, String id) throws Exception
    {
        return buildEntityCollectionUrl(entityType) + "/" + id;
    }

    public <T> T get(
            String path,
            Class<T> entityType,
            MultivaluedMap<String, Object> headers,
            Map<String, String> queryParams) throws Exception
    {
        Log.debug("GET: {}", path);

        return call(HttpMethod.GET, path, headers, queryParams, null, entityType);
    }

    public <T> T post(
            String path,
            Class<T> entityType,
            MultivaluedMap<String, Object> headers,
            Map<String, String> queryParams,
            Object payload) throws Exception
    {
        Log.debug("POST: {}", path);

        return call(HttpMethod.POST, path, headers, queryParams, payload, entityType);
    }

    public <T> T post(
            String path,
            Class<T> entityType,
            MultivaluedMap<String, Object> headers,
            Map<String, String> queryParams,
            Object payload,
            String contentType) throws Exception
    {
        Log.debug("POST: {}", path);

        return call(HttpMethod.POST, path, headers, queryParams, payload, contentType, entityType);
    }

    public <T> T put(
            String path,
            Class<T> entityType,
            MultivaluedMap<String, Object> headers,
            Map<String, String> queryParams,
            Object payload) throws Exception
    {
        Log.debug("PUT: {}", path);

        return call(HttpMethod.PUT, path, headers, queryParams, payload, entityType);
    }

    public Response delete(
            String path,
            MultivaluedMap<String, Object> headers,
            Map<String, String> queryParams) throws Exception
    {
        Log.debug("DELETE: {}", path);

        return call(HttpMethod.DELETE, path, headers, queryParams, null);
    }

    private static WebTarget createWebTarget(String uri, Map<String, String> queryParams) throws URISyntaxException
    {
        WebTarget webTarget = null;

        URI u = new URI(uri);
        Client client = ClientBuilder.newClient();

        webTarget = client.target(u);

        if (MapUtils.isNotEmpty(queryParams))
        {
            for (Entry<String, String> entry : queryParams.entrySet())
            {
                if (StringUtils.isNotBlank(entry.getKey()) && StringUtils.isNotBlank(entry.getValue()))
                {
                    String value = UriComponent.encode(
                            entry.getValue(),
                            UriComponent.Type.QUERY_PARAM_SPACE_ENCODED);

                    webTarget = webTarget.queryParam(entry.getKey(), value);
                }
            }
        }

        return webTarget;
    }

    private static boolean isStatusCodeOK(int statusCode)
    {
        return statusCode >= Status.OK.getStatusCode() &&
               statusCode <= Status.PARTIAL_CONTENT.getStatusCode();
    }

    private <T> T call(
            String methodName,
            String path,
            MultivaluedMap<String, Object> headers,
            Map<String, String> queryParams,
            Object payload,
            Class<T> entityType) throws Exception
    {
        return call(methodName, path, headers, queryParams, payload, MediaType.APPLICATION_XML, entityType);
    }

    private <T> T call(
            String methodName,
            String path,
            MultivaluedMap<String, Object> headers,
            Map<String, String> queryParams,
            Object payload,
            String contentType,
            Class<T> entityType) throws Exception
    {
        Response res = call(methodName, path, headers, queryParams, payload, contentType);

        if(!res.hasEntity())
        {
            return null;
        }

        return (T) res.readEntity(entityType);
    }

    private Response call(
            String methodName,
            String path,
            MultivaluedMap<String, Object> headers,
            Map<String, String> queryParams,
            Object payload) throws Exception
    {
        return call(methodName, path, headers, queryParams, payload, MediaType.APPLICATION_XML);
    }

    private Response call(
            String methodName,
            String path,
            MultivaluedMap<String, Object> headers,
            Map<String, String> queryParams,
            Object payload,
            String contentType) throws Exception
    {
        WebTarget webTarget = createWebTarget(buildUrl(path), queryParams);

        Builder result = webTarget.request().headers(headers);

        if (MapUtils.isNotEmpty(cookies))
        {
            for (Entry<String, Cookie> cookie : cookies.entrySet())
            {
                result = result.cookie(cookie.getValue());
            }
        }

        Response res = result.method(
                methodName, Entity.entity(payload, contentType), Response.class);

        int statusCode = res.getStatus();

        if (!isStatusCodeOK(statusCode))
        {
            throw new ResponseException(res, buildUrl(path));
        }

        updateCookies(res.getCookies());

        return res;
    }

    private void updateCookies(Map<String, NewCookie> newCookies)
    {
        if (MapUtils.isNotEmpty(newCookies))
        {
            cookies.putAll(newCookies);
        }
    }
}