package com.braintree.graphql;

import com.braintreegateway.Configuration;
import com.braintreegateway.exceptions.AuthenticationException;
import com.braintreegateway.exceptions.AuthorizationException;
import com.braintreegateway.exceptions.NotFoundException;
import com.braintreegateway.exceptions.ServerException;
import com.braintreegateway.exceptions.ServiceUnavailableException;
import com.braintreegateway.exceptions.TooManyRequestsException;
import com.braintreegateway.exceptions.UnexpectedException;
import com.braintreegateway.exceptions.UpgradeRequiredException;
import com.braintreegateway.util.GraphQLClient;
import com.braintreegateway.util.Http;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;


public class BrainTreeGraphQLClient extends GraphQLClient {

    private static final String ERROR_OBJECT_KEY = "errors";
    private static final String ERROR_MESSAGE_KEY = "message";
    private static final String ERROR_EXTENSIONS_KEY = "extensions";
    private static final String ERROR_CLASS_KEY = "errorClass";
    private Configuration configuration;


    public BrainTreeGraphQLClient(Configuration configuration) {
        super(configuration);
        this.configuration = configuration;
    }

    @Override
    public Map<String, Object> query(String definition, Map<String, Object> variables) {
        HttpURLConnection connection = null;
        Map<String, Object> jsonMap = null;
        final ObjectMapper objectMapper = new ObjectMapper();

        final String requestString = formatGraphQLRequest(definition, variables);
        final String contentType = "application/json";

        final Map<String, String> headers = constructHeaders(contentType, contentType);
        headers.put("Braintree-Version", Configuration.GRAPHQL_API_VERSION);

        try {
            connection = getBuildConnection(headers);
        } catch (IOException e) {
            throw new UnexpectedException(e.getMessage(), e);
        }

        final String jsonString = getHttpDo(requestString, connection, headers);

        try {
            jsonMap = objectMapper.readValue(jsonString, HashMap.class);
        } catch (IOException e) {
            throw new UnexpectedException(e.getMessage(), e);
        }

        throwExceptionIfGraphQLErrorResponse(jsonMap);

        return jsonMap;
    }

    private void throwExceptionIfGraphQLErrorResponse(Map<String, Object> response) {
        final List<Map<String, Object>> errors = (List<Map<String, Object>>) response.get(ERROR_OBJECT_KEY);
        if (errors == null) {
            return;
        }

        for (Map<String, Object> error : errors) {
            final String message = (String) error.get(ERROR_MESSAGE_KEY);
            final Map<String, Object> extensions = (Map) error.get(ERROR_EXTENSIONS_KEY);
            String errorClass = null;
            if (extensions == null || (errorClass = (String) extensions.get(ERROR_CLASS_KEY)) == null) {
                continue;
            }
            if (!ErrorClass.valueOf(errorClass).equals(ErrorClass.VALIDATION)) {
                typeCheckingAndThrowingErrors(errorClass, message);
            }
        }

        return;
    }

    private void typeCheckingAndThrowingErrors(String errorClass, String message) {
        switch (ErrorClass.valueOf(errorClass)) {
            case AUTHENTICATION:
                throw new AuthenticationException();
            case AUTHORIZATION:
                throw new AuthorizationException(message);
            case NOT_FOUND:
                throw new NotFoundException(message);
            case UNSUPPORTED_CLIENT:
                throw new UpgradeRequiredException();
            case RESOURCE_LIMIT:
                throw new TooManyRequestsException("Request rate or complexity limit exceeded");
            case INTERNAL:
                throw new ServerException();
            case SERVICE_AVAILABILITY:
                throw new ServiceUnavailableException();
            case UNKNOWN:
                throw new UnexpectedException("Unexpected Response: " + message);
            default:
                break;
        }
    }

    protected HttpURLConnection getBuildConnection(Map<String, String> headers) throws IOException {
        return buildConnection(Http.RequestMethod.POST, configuration.getGraphQLURL(), headers);
    }

    protected String getHttpDo(String requestString, HttpURLConnection connection, Map<String, String> headers) {
        return httpDo(Http.RequestMethod.POST, "/graphql", requestString, null, connection, headers, null);
    }

    public static String formatGraphQLRequest(String definition, Map<String, Object> variables) {
        String json = null;
        final ObjectMapper objectMapper = new ObjectMapper();

        final Map<String, Object> map = new TreeMap<String, Object>();

        map.put("query", definition);
        map.put("variables", variables);

        try {
            json = objectMapper.writeValueAsString(map);
        } catch (IOException e) {
            throw new AssertionError("An IOException occurred when writing JSON object. " + e);
        }

        return json;
    }

}
