package com.julienvey.trello.impl.http;

import static com.julienvey.trello.impl.http.UrlExpander.expandUrl;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.util.Objects;
import java.util.Optional;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.julienvey.trello.NotAuthorizedException;
import com.julienvey.trello.NotFoundException;
import com.julienvey.trello.TrelloBadRequestException;
import com.julienvey.trello.TrelloHttpClient;
import com.julienvey.trello.exception.TrelloHttpException;

import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.Request.Builder;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okio.GzipSource;
import okio.Okio;

/**
 * The {@code TrelloHttpClient} backed by {@link OkHttpClient}.
 *
 * @author Edgar Asatryan
 */
public class OkHttpTrelloHttpClient implements TrelloHttpClient {
    private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
    private static final MediaType APPLICATION_JSON = MediaType.parse("application/json; charset=utf-8");
    private static final MediaType APPLICATION_OCTET_STREAM = MediaType.parse("application/octet-stream");
    private final OkHttpClient httpClient;
    private final ObjectMapper objectMapper;

    /**
     * Constructs new instance of {@code OkHttpTrelloHttpClient}.
     *
     * @param httpClient   The HTTP client to perform network calls.
     * @param objectMapper The request/response encoder/decoder.
     *
     * @throws NullPointerException When any argument is {@code null}.
     */
    public OkHttpTrelloHttpClient(OkHttpClient httpClient, ObjectMapper objectMapper) {
        this.httpClient = Objects.requireNonNull(httpClient);
        this.objectMapper = Objects.requireNonNull(objectMapper);
    }

    /**
     * Constructs new instance of {@code OkHttpTrelloHttpClient} with default values.
     */
    public OkHttpTrelloHttpClient() {
        this(new OkHttpClient(), new ObjectMapper());
    }

    private static Builder requestBuilder(String url, String[] params) {
        return new Builder()
                .header("Accept", APPLICATION_JSON.toString())
                .url(expandUrl(url, params));
    }

    @Override
    public <T> T get(String url, Class<T> responseType, String... params) {
        try (Response response = httpClient.newCall(requestBuilder(url, params)
                .get()
                .build())
                .execute()) {

            return readResponse(responseType, response);
        } catch (IOException e) {
            throw new TrelloHttpException(e);
        }
    }

    @Override
    public <T> T postForObject(String url, Object body, Class<T> responseType, String... params) {
        try (Response response = httpClient.newCall(requestBuilder(url, params)
                .post(requestBody(body))
                .build())
                .execute()) {

            return readResponse(responseType, response);
        } catch (IOException e) {
            throw new TrelloHttpException(e);
        }
    }

    @Override
    public URI postForLocation(String url, Object body, String... params) {
        try (Response response = httpClient.newCall(requestBuilder(url, params)
                .post(requestBody(body))
                .build())
                .execute()) {

            checkStatusCode(response);

            String location = Optional.ofNullable(response.header("Location"))
                    .orElseThrow(() -> new NullPointerException("Location header is not present!"));

            return URI.create(location);
        } catch (IOException e) {
            throw new TrelloHttpException(e);
        }
    }

    @Override
    public <T> T putForObject(String url, Object body, Class<T> responseType, String... params) {
        try (Response response = httpClient.newCall(requestBuilder(url, params)
                .put(requestBody(body))
                .build())
                .execute()) {

            return readResponse(responseType, response);
        } catch (IOException e) {
            throw new TrelloHttpException(e);
        }
    }

    @Override
    public <T> T delete(String url, Class<T> responseType, String... params) {
        try (Response response = httpClient.newCall(requestBuilder(url, params)
                .delete()
                .build())
                .execute()) {

            return readResponse(responseType, response);
        } catch (IOException e) {
            throw new TrelloHttpException(e);
        }
    }

    @Override
    public <T> T postFileForObject(String url, File file, Class<T> responseType, String... params) {
        RequestBody requestBody = new MultipartBody.Builder()
                .setType(MultipartBody.FORM)
                .addFormDataPart("file", file.getName(), RequestBody.create(APPLICATION_OCTET_STREAM, file))
                .build();

        try (Response response = httpClient.newCall(new Builder()
                .url(expandUrl(url, params))
                .post(requestBody)
                .build())
                .execute()) {
            return readResponse(responseType, response);
        } catch (IOException e) {
            throw new TrelloHttpException(e);
        }
    }

    private <T> T readResponse(Class<T> responseType, Response response) throws IOException {
        checkStatusCode(response);
        ResponseBody body = Optional.ofNullable(response.body())
                .orElseThrow(() -> new IllegalStateException("Cannot read response body because it is null."));

        try {
            return objectMapper.readValue(body.byteStream(), responseType);
        } catch (JsonProcessingException e) {
            throw new TrelloHttpException("Cannot parse Trello response.", e);
        }
    }

    private void checkStatusCode(Response response) throws IOException {
        if (response.isSuccessful()) {
            return;
        }

        switch (response.code()) {
            case HttpURLConnection.HTTP_BAD_REQUEST:
                throw new TrelloBadRequestException(response.body() != null ? response.body().string() : "Bad Request");
            case HttpURLConnection.HTTP_UNAUTHORIZED:
                throw new NotAuthorizedException();
            case HttpURLConnection.HTTP_NOT_FOUND:
                throw new NotFoundException("Resource not found: " + response.request().url().uri());
        }
    }

    private RequestBody requestBody(Object object) throws JsonProcessingException {
        byte[] body = object == null ? EMPTY_BYTE_ARRAY : objectMapper.writeValueAsBytes(object);

        return RequestBody.create(APPLICATION_JSON, body);
    }
}