package org.kohsuke.github;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.InjectableValues;
import com.fasterxml.jackson.databind.JsonMappingException;
import org.apache.commons.io.IOUtils;
import org.kohsuke.github.function.FunctionThrows;

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Array;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;

/**
 * A GitHubResponse
 * <p>
 * A {@link GitHubResponse} generated by from sending a {@link GitHubRequest} to a {@link GitHubClient}.
 * </p>
 *
 * @param <T>
 *            the type of the data parsed from the body of a {@link ResponseInfo}.
 */
class GitHubResponse<T> {

    private static final Logger LOGGER = Logger.getLogger(GitHubResponse.class.getName());

    private final int statusCode;

    @Nonnull
    private final GitHubRequest request;

    @Nonnull
    private final Map<String, List<String>> headers;

    @CheckForNull
    private final T body;

    GitHubResponse(GitHubResponse<T> response, @CheckForNull T body) {
        this.statusCode = response.statusCode();
        this.request = response.request();
        this.headers = response.headers();
        this.body = body;
    }

    GitHubResponse(ResponseInfo responseInfo, @CheckForNull T body) {
        this.statusCode = responseInfo.statusCode();
        this.request = responseInfo.request();
        this.headers = responseInfo.headers();
        this.body = body;
    }

    /**
     * Parses a {@link ResponseInfo} body into a new instance of {@link T}.
     *
     * @param responseInfo
     *            response info to parse.
     * @param type
     *            the type to be constructed.
     * @param <T>
     *            the type
     * @return a new instance of {@link T}.
     * @throws IOException
     *             if there is an I/O Exception.
     */
    @CheckForNull
    static <T> T parseBody(ResponseInfo responseInfo, Class<T> type) throws IOException {

        if (responseInfo.statusCode() == HttpURLConnection.HTTP_NO_CONTENT) {
            if (type != null && type.isArray()) {
                // no content for array should be empty array
                return type.cast(Array.newInstance(type.getComponentType(), 0));
            } else {
                // no content for object should be null
                return null;
            }
        }

        String data = responseInfo.getBodyAsString();
        try {
            InjectableValues.Std inject = new InjectableValues.Std();
            inject.addValue(ResponseInfo.class, responseInfo);

            return GitHubClient.getMappingObjectReader(responseInfo).forType(type).readValue(data);
        } catch (JsonMappingException | JsonParseException e) {
            String message = "Failed to deserialize: " + data;
            LOGGER.log(Level.FINE, message);
            throw e;
        }
    }

    /**
     * Parses a {@link ResponseInfo} body into a new instance of {@link T}.
     *
     * @param responseInfo
     *            response info to parse.
     * @param instance
     *            the object to fill with data parsed from body
     * @param <T>
     *            the type
     * @return a new instance of {@link T}.
     * @throws IOException
     *             if there is an I/O Exception.
     */
    @CheckForNull
    static <T> T parseBody(ResponseInfo responseInfo, T instance) throws IOException {

        String data = responseInfo.getBodyAsString();
        try {
            return GitHubClient.getMappingObjectReader(responseInfo).withValueToUpdate(instance).readValue(data);
        } catch (JsonMappingException | JsonParseException e) {
            String message = "Failed to deserialize: " + data;
            LOGGER.log(Level.FINE, message);
            throw e;
        }
    }

    /**
     * The {@link URL} for this response.
     *
     * @return the {@link URL} for this response.
     */
    @Nonnull
    public URL url() {
        return request.url();
    }

    /**
     * The {@link GitHubRequest} for this response.
     *
     * @return the {@link GitHubRequest} for this response.
     */
    @Nonnull
    public GitHubRequest request() {
        return request;
    }

    /**
     * The status code for this response.
     *
     * @return the status code for this response.
     */
    public int statusCode() {
        return statusCode;
    }

    /**
     * The headers for this response.
     *
     * @return the headers for this response.
     */
    @Nonnull
    public Map<String, List<String>> headers() {
        return headers;
    }

    /**
     * Gets the value of a header field for this response.
     *
     * @param name
     *            the name of the header field.
     * @return the value of the header field, or {@code null} if the header isn't set.
     */
    @CheckForNull
    public String headerField(String name) {
        String result = null;
        if (headers.containsKey(name)) {
            result = headers.get(name).get(0);
        }
        return result;
    }

    /**
     * The body of the response parsed as a {@link T}
     *
     * @return body of the response
     */
    public T body() {
        return body;
    }

    /**
     * Represents a supplier of results that can throw.
     *
     * @param <T>
     *            the type of results supplied by this supplier
     */
    @FunctionalInterface
    interface BodyHandler<T> extends FunctionThrows<ResponseInfo, T, IOException> {
    }

    /**
     * Initial response information supplied to a {@link BodyHandler} when a response is initially received and before
     * the body is processed.
     */
    static abstract class ResponseInfo implements Closeable {

        private static final Comparator<String> nullableCaseInsensitiveComparator = Comparator
                .nullsFirst(String.CASE_INSENSITIVE_ORDER);

        private final int statusCode;
        @Nonnull
        private final GitHubRequest request;
        @Nonnull
        private final Map<String, List<String>> headers;

        protected ResponseInfo(@Nonnull GitHubRequest request,
                int statusCode,
                @Nonnull Map<String, List<String>> headers) {
            this.request = request;
            this.statusCode = statusCode;

            // Response header field names must be case-insensitive.
            TreeMap<String, List<String>> caseInsensitiveMap = new TreeMap<>(nullableCaseInsensitiveComparator);
            caseInsensitiveMap.putAll(headers);

            this.headers = Collections.unmodifiableMap(caseInsensitiveMap);
        }

        /**
         * Gets the value of a header field for this response.
         *
         * @param name
         *            the name of the header field.
         * @return the value of the header field, or {@code null} if the header isn't set.
         */
        @CheckForNull
        public String headerField(String name) {
            String result = null;
            if (headers.containsKey(name)) {
                result = headers.get(name).get(0);
            }
            return result;
        }

        /**
         * The response body as an {@link InputStream}.
         *
         * @return the response body
         * @throws IOException
         *             if an I/O Exception occurs.
         */
        abstract InputStream bodyStream() throws IOException;

        /**
         * The error message for this response.
         *
         * @return if there is an error with some error string, that is returned. If not, {@code null}.
         */
        abstract String errorMessage();

        /**
         * The {@link URL} for this response.
         *
         * @return the {@link URL} for this response.
         */
        @Nonnull
        public URL url() {
            return request.url();
        }

        /**
         * Gets the {@link GitHubRequest} for this response.
         *
         * @return the {@link GitHubRequest} for this response.
         */
        @Nonnull
        public GitHubRequest request() {
            return request;
        }

        /**
         * The status code for this response.
         *
         * @return the status code for this response.
         */
        public int statusCode() {
            return statusCode;
        }

        /**
         * The headers for this response.
         *
         * @return the headers for this response.
         */
        @Nonnull
        public Map<String, List<String>> headers() {
            return headers;
        }

        /**
         * Gets the body of the response as a {@link String}.
         *
         * @return the body of the response as a {@link String}.
         * @throws IOException
         *             if an I/O Exception occurs.
         */
        @Nonnull
        String getBodyAsString() throws IOException {
            InputStreamReader r = null;
            r = new InputStreamReader(this.bodyStream(), StandardCharsets.UTF_8);
            return IOUtils.toString(r);
        }
    }

}
