package ru.yandex.webmaster3.core.openapi.internal;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.methods.HttpPatch;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.config.ConnectionConfig;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;
import ru.yandex.webmaster3.core.openapi.internal.auth.Authentication;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

@javax.annotation.Generated(value = "ru.yandex.webmaster3.openapi.codegen.WebmasterOpenAPICodegen", date = "2018-02-26T00:52:18.072+03:00")
public class ApiClient {
    private static final Logger log = LoggerFactory.getLogger(ApiClient.class);

    private Map<String, String> defaultHeaderMap = new HashMap<String, String>();
    private String basePath;

    private CloseableHttpClient httpClient;
    private JSON json = new JSON();
    private String tempFolderPath = null;

    private Map<String, Authentication> authentications;

    private DateFormat dateFormat;

    private int connectTimeout = 50;
    private int socketTimeout = 1000;
    private int maxConnections = 10;
    private String defaultAuth;

    public void init() {
        this.dateFormat = new RFC3339DateFormat();

        ConnectionConfig connectionConfig = ConnectionConfig.custom().setCharset(StandardCharsets.UTF_8).build();
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectTimeout(connectTimeout)
                .setSocketTimeout(socketTimeout)
                .build();

        HttpClientBuilder httpClientBuilder = HttpClients.custom()
                .setDefaultConnectionConfig(connectionConfig)
                .setDefaultRequestConfig(requestConfig)
                .setMaxConnPerRoute(maxConnections);

        this.httpClient = httpClientBuilder.build();
    }


    /**
     * Get authentications (key: authentication name, value: authentication).
     *
     * @return Map of authentication object
     */
    private Map<String, Authentication> getAuthentications() {
        return authentications;
    }

    /**
     * Get authentication for the given name.
     *
     * @param authName The authentication name
     * @return The authentication, null if not found
     */
    private Authentication getAuthentication(String authName) {
        return authentications.get(authName);
    }

    /**
     * Parse the given string into Date object.
     *
     * @param str String
     * @return Date
     */
    private Date parseDate(String str) {
        try {
            return dateFormat.parse(str);
        } catch (java.text.ParseException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Format the given Date object into string.
     *
     * @param date Date
     * @return Date in string format
     */
    public String formatDate(Date date) {
        return dateFormat.format(date);
    }

    /**
     * Format the given parameter object into string.
     *
     * @param param Object
     * @return Object in string format
     */
    public String parameterToString(Object param) {
        if (param == null) {
            return "";
        } else if (param instanceof Date) {
            return formatDate((Date) param);
        } else if (param instanceof Collection) {
            StringBuilder b = new StringBuilder();
            for (Object o : (Collection) param) {
                if (b.length() > 0) {
                    b.append(',');
                }
                b.append(String.valueOf(o));
            }
            return b.toString();
        } else {
            return String.valueOf(param);
        }
    }

    /*
     * Format to {@code Pair} objects.
     * @param collectionFormat Collection format
     * @param name Name
     * @param value Value
     * @return List of pairs
     */
    public List<Pair> parameterToPairs(String collectionFormat, String name, Object value) {
        List<Pair> params = new ArrayList<Pair>();

        // preconditions
        if (name == null || name.isEmpty() || value == null) return params;

        Collection valueCollection;
        if (value instanceof Collection) {
            valueCollection = (Collection) value;
        } else {
            params.add(new Pair(name, parameterToString(value)));
            return params;
        }

        if (valueCollection.isEmpty()) {
            return params;
        }

        // get the collection format (default: csv)
        String format = (collectionFormat == null || collectionFormat.isEmpty() ? "csv" : collectionFormat);

        // create the params based on the collection format
        if ("multi".equals(format)) {
            for (Object item : valueCollection) {
                params.add(new Pair(name, parameterToString(item)));
            }

            return params;
        }

        String delimiter = ",";

        if ("csv".equals(format)) {
            delimiter = ",";
        } else if ("ssv".equals(format)) {
            delimiter = " ";
        } else if ("tsv".equals(format)) {
            delimiter = "\t";
        } else if ("pipes".equals(format)) {
            delimiter = "|";
        }

        StringBuilder sb = new StringBuilder();
        for (Object item : valueCollection) {
            sb.append(delimiter);
            sb.append(parameterToString(item));
        }

        params.add(new Pair(name, sb.substring(1)));

        return params;
    }


    /**
     * Escape the given string to be used as URL query value.
     *
     * @param str String
     * @return Escaped string
     */
    private String escapeString(String str) {
        try {
            return URLEncoder.encode(str, "utf8").replaceAll("\\+", "%20");
        } catch (UnsupportedEncodingException e) {
            return str;
        }
    }

    /**
     * Serialize the given Java object into string entity according the given
     * Content-Type (only JSON is supported for now).
     *
     * @param obj         Object
     * @param formParams  Form parameters
     * @param contentType Context type
     * @return Entity
     * @throws ApiException API exception
     */
    private HttpEntity serialize(Object obj, Map<String, Object> formParams, String contentType) throws ApiException {
        if (contentType.startsWith("multipart/form-data")) {
            MultipartEntityBuilder builder = MultipartEntityBuilder.create();
            for (Entry<String, Object> param : formParams.entrySet()) {
                if (param.getValue() instanceof File) {
                    File file = (File) param.getValue();
                    builder.addBinaryBody(param.getKey(), file);
                } else {
                    builder.addTextBody(param.getKey(), parameterToString(param.getValue()));
                }
            }
            return builder.build();
        } else if (contentType.startsWith("application/x-www-form-urlencoded")) {
            UrlEncodedFormEntity entity = new UrlEncodedFormEntity(
                    formParams.entrySet().stream()
                            .map(entry -> new BasicNameValuePair(entry.getKey(), parameterToString(entry.getValue())))
                            .collect(Collectors.toList()),
                    StandardCharsets.UTF_8
            );
            return entity;
        } else if (contentType.startsWith("application/json")) {
            try {
                String jsonString = json.getMapper().writeValueAsString(obj);
                return new StringEntity(jsonString, ContentType.APPLICATION_JSON);
            } catch (JsonProcessingException e) {
                throw new RuntimeException("Failed to serialize object " + obj);
            }
        } else {
            throw new RuntimeException("Unsupported content type " + contentType);
        }
    }

    /**
     * Deserialize response body to Java object according to the Content-Type.
     *
     * @param <T>        Type
     * @param response   Response
     * @param returnType Return type
     * @return Deserialize object
     * @throws ApiException API exception
     */
    @SuppressWarnings("unchecked")
    private <T> T deserialize(CloseableHttpResponse response, TypeReference<T> returnType) throws ApiException {
        try {
            if (response == null || returnType == null || response.getEntity() == null) {
                return null;
            }

            boolean isByteArray;
            if (returnType.getType() == byte[].class) {
                byte[] result;
                long contentLength = response.getEntity().getContentLength();
                if (contentLength >= 0) {
                    result = IOUtils.toByteArray(response.getEntity().getContent(), contentLength);
                } else {
                    result = IOUtils.toByteArray(response.getEntity().getContent());
                }
                return (T) result;
            } else if (returnType.getType() == File.class) {
                // Handle file downloading.
                T file = (T) downloadFileFromResponse(response);
                return file;
            } else {

                String contentType = Optional.ofNullable(response.getEntity().getContentType()).map(Header::getValue).orElse(null);
                if (OpenAPIUtility.isJsonMime(contentType)) {
                    return json.getMapper().readValue(response.getEntity().getContent(), returnType);
                } else {
                    throw new ApiException("Unknown mime type " + contentType + " of response");
                }
            }
        } catch (IOException e) {
            throw new ApiException(e);
        }
    }

    /**
     * Download file from the given response.
     *
     * @param response Response
     * @return File
     * @throws ApiException If fail to read file content from response and write to disk
     */
    private File downloadFileFromResponse(HttpResponse response) throws ApiException {
        try {
            File file = prepareDownloadFile(response);
            Files.copy(response.getEntity().getContent(), file.toPath(), StandardCopyOption.REPLACE_EXISTING);
            return file;
        } catch (IOException e) {
            throw new ApiException(e);
        }
    }

    private File prepareDownloadFile(HttpResponse response) throws IOException {
        String filename = null;
        String contentDisposition = Optional.ofNullable(response.getFirstHeader("Content-Disposition")).map(Header::getValue).orElse(null);
        if (StringUtils.isNotEmpty(contentDisposition)) {
            // Get filename from the Content-Disposition header.
            Pattern pattern = Pattern.compile("filename=['\"]?([^'\"\\s]+)['\"]?");
            Matcher matcher = pattern.matcher(contentDisposition);
            if (matcher.find())
                filename = matcher.group(1);
        }

        String prefix;
        String suffix = null;
        if (filename == null) {
            prefix = "download-";
            suffix = "";
        } else {
            int pos = filename.lastIndexOf('.');
            if (pos == -1) {
                prefix = filename + "-";
            } else {
                prefix = filename.substring(0, pos) + "-";
                suffix = filename.substring(pos);
            }
            // File.createTempFile requires the prefix to be at least three characters long
            if (prefix.length() < 3)
                prefix = "download-";
        }

        if (tempFolderPath == null)
            return File.createTempFile(prefix, suffix);
        else
            return File.createTempFile(prefix, suffix, new File(tempFolderPath));
    }

    /**
     * Invoke API by sending HTTP request with the given options.
     *
     * @param <T>          Type
     * @param path         The sub-path of the HTTP URL
     * @param method       The request method, one of "GET", "POST", "PUT", "HEAD" and "DELETE"
     * @param queryParams  The query parameters
     * @param body         The request body object
     * @param headerParams The header parameters
     * @param formParams   The form parameters
     * @param accept       The request's Accept header
     * @param contentType  The request's Content-Type header
     * @param authNames    The authentications to apply
     * @param returnType   The return type into which to deserialize the response
     * @return The response body in type of string
     * @throws ApiException API exception
     */
    public <T> ApiResponse<T> invokeAPI(String path, String method, List<Pair> queryParams, Object body, Map<String, String> headerParams, Map<String, Object> formParams, String accept, String contentType, String[] authNames, TypeReference<T> returnType) throws ApiException {
        updateParamsForAuth(authNames, queryParams, headerParams);

        String queryString = basePath + path;
        if (CollectionUtils.isNotEmpty(queryParams)) {
            queryString += "?" + queryParams.stream()
                    .map(p -> OpenAPIUtility.escapeString(p.getName()) + "=" + OpenAPIUtility.escapeString(p.getValue()))
                    .collect(Collectors.joining("&"));
        }
        HttpUriRequest request;
        switch (method) {
            case "GET":
                request = new HttpGet(queryString);
                break;
            case "POST":
                request = new HttpPost(queryString);
                break;
            case "PUT":
                request = new HttpPut(queryString);
                break;
            case "DELETE":
                request = new HttpDelete(queryString);
                break;
            case "PATCH":
                request = new HttpPatch(queryString);
                break;
            case "HEAD":
                request = new HttpHead(queryString);
                break;
            default:
                throw new RuntimeException("Unsupported http method " + method);
        }
        headerParams.forEach((name, value) -> {
            if (value != null) {
                request.addHeader(name, value);
            }
        });

        if (defaultHeaderMap != null) {
            defaultHeaderMap.forEach(request::addHeader);
        }

        if (request instanceof HttpEntityEnclosingRequest) {
            HttpEntity entity = serialize(body, formParams, contentType);
            if (entity != null) {
                ((HttpEntityEnclosingRequest) request).setEntity(entity);
            }
        }

        log.info("Querying {} {}", request.getMethod(), request.getURI());
        try (CloseableHttpResponse response = httpClient.execute(request)) {
            int statusCode = response.getStatusLine().getStatusCode();
            Map<String, List<String>> responseHeaders = buildResponseHeaders(response);

            if (statusCode >= 200 && statusCode < 300) {
                if (statusCode == HttpStatus.SC_NO_CONTENT || returnType == null) {
                    return new ApiResponse<>(statusCode, responseHeaders);
                } else {
                    return new ApiResponse<>(statusCode, responseHeaders, deserialize(response, returnType));
                }
            } else {
                String message = "error";
                String respBody = null;
                if (response.getEntity() != null) {
                    try {
                        respBody = IOUtils.toString(response.getEntity().getContent());
                        message = respBody;
                    } catch (RuntimeException e) {
                        //ignore
                    }
                }
                throw new ApiException(
                        statusCode,
                        message,
                        buildResponseHeaders(response),
                        respBody);
            }
        } catch (IOException e) {
            throw new ApiException(e);
        }
    }

    private Map<String, List<String>> buildResponseHeaders(HttpResponse response) {
        Map<String, List<String>> responseHeaders = new HashMap<String, List<String>>();
        for (Header header : response.getAllHeaders()) {
            responseHeaders.computeIfAbsent(header.getName(), ign -> new ArrayList<>(1))
                    .add(header.getValue());
        }
        return responseHeaders;
    }

    /**
     * Update query and header parameters based on authentication settings.
     *
     * @param authNames The authentications to apply
     */
    private void updateParamsForAuth(String[]
                                             authNames, List<Pair> queryParams, Map<String, String> headerParams) {
        if (authNames == null || authNames.length == 0 && StringUtils.isNotEmpty(defaultAuth)) {
            Authentication auth = authentications.get(defaultAuth);
            if (auth == null) {
                throw new RuntimeException("Authentication undefined: " + defaultAuth);
            }
            auth.applyToParams(queryParams, headerParams);
        }
        for (String authName : authNames) {
            Authentication auth = authentications.get(authName);
            if (auth == null) {
                throw new RuntimeException("Authentication undefined: " + authName);
            }
            auth.applyToParams(queryParams, headerParams);
        }
    }

    public void setDefaultHeaderMap(Map<String, String> defaultHeaderMap) {
        this.defaultHeaderMap = defaultHeaderMap;
    }

    @Required
    public void setBasePath(String basePath) {
        this.basePath = basePath;
    }

    public void setTempFolderPath(String tempFolderPath) {
        this.tempFolderPath = tempFolderPath;
    }

    public void setAuthentications(Map<String, Authentication> authentications) {
        this.authentications = authentications;
    }

    public void setConnectTimeout(int connectTimeout) {
        this.connectTimeout = connectTimeout;
    }

    public void setSocketTimeout(int socketTimeout) {
        this.socketTimeout = socketTimeout;
    }

    public void setMaxConnections(int maxConnections) {
        this.maxConnections = maxConnections;
    }

    public void setDefaultAuth(String defaultAuth) {
        this.defaultAuth = defaultAuth;
    }
}
