/*
 * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for additional information regarding
 * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the License. You may obtain a
 * copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */

package org.apache.geode.management.internal.web.http.support;

import static org.apache.geode.internal.util.ProductVersionUtil.getDistributionVersion;

import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import org.apache.geode.internal.version.DistributionVersion;
import org.apache.geode.management.internal.web.http.converter.SerializableObjectHttpMessageConverter;
import org.apache.geode.security.AuthenticationFailedException;
import org.apache.geode.security.NotAuthorizedException;


/**
 * The HttpRequester class is a Adapter/facade for the Spring RestTemplate class for abstracting
 * HTTP requests and operations.
 * <p>
 *
 * @see org.springframework.http.client.SimpleClientHttpRequestFactory
 * @see org.springframework.web.client.RestTemplate
 * @since GemFire 8.0
 */
@SuppressWarnings("unused")
public class HttpRequester {
  private final RestTemplate restTemplate;
  private final Properties securityProperties;

  protected static final String USER_AGENT_HTTP_REQUEST_HEADER_VALUE;

  static {
    final DistributionVersion distributionVersion = getDistributionVersion();
    USER_AGENT_HTTP_REQUEST_HEADER_VALUE =
        "gfsh (pronounced " + distributionVersion.getName() + " shell)/v"
            + distributionVersion.getVersion();
  }

  // a list of acceptable content/media types supported by Gfsh
  private final List<MediaType> acceptableMediaTypes = Arrays.asList(MediaType.APPLICATION_JSON,
      MediaType.TEXT_PLAIN, MediaType.APPLICATION_OCTET_STREAM);

  public HttpRequester() {
    this(null, null);
  }

  public HttpRequester(Properties securityProperties) {
    this(securityProperties, null);
  }

  HttpRequester(Properties securityProperties, RestTemplate restTemplate) {
    final SimpleClientHttpRequestFactory clientHttpRequestFactory =
        new SimpleClientHttpRequestFactory();
    this.securityProperties = securityProperties;
    if (restTemplate == null) {
      this.restTemplate = new RestTemplate(clientHttpRequestFactory);
    } else {
      this.restTemplate = restTemplate;
    }

    // add our custom HttpMessageConverter for serializing DTO Objects into the HTTP request message
    // body and de-serializing HTTP response message body content back into DTO Objects
    List<HttpMessageConverter<?>> converters = this.restTemplate.getMessageConverters();
    // remove the MappingJacksonHttpConverter
    for (int i = converters.size() - 1; i >= 0; i--) {
      HttpMessageConverter converter = converters.get(i);
      if (converter instanceof MappingJackson2HttpMessageConverter) {
        converters.remove(converter);
      }
    }
    converters.add(new SerializableObjectHttpMessageConverter());

    this.restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
      @Override
      public void handleError(final ClientHttpResponse response) throws IOException {
        String body = IOUtils.toString(response.getBody(), StandardCharsets.UTF_8);
        final String message = String.format("The HTTP request failed with: %1$d - %2$s.",
            response.getStatusCode().value(), body);

        if (response.getStatusCode().value() == 401) {
          throw new AuthenticationFailedException(message);
        } else if (response.getStatusCode().value() == 403) {
          throw new NotAuthorizedException(message);
        } else {
          throw new RuntimeException(message);
        }
      }
    });
  }

  /**
   * Gets an instance of the Spring RestTemplate to perform the HTTP operations.
   * <p>
   *
   * @return an instance of the Spring RestTemplate for performing HTTP operations.
   * @see org.springframework.web.client.RestTemplate
   */
  public RestTemplate getRestTemplate() {
    return restTemplate;
  }

  public <T> T get(URI url, Class<T> responseType) {
    return exchange(url, HttpMethod.GET, null, responseType);
  }

  public <T> T post(URI url, Object content, Class<T> responseType) {
    return exchange(url, HttpMethod.POST, content, responseType);
  }

  <T> T exchange(URI url, HttpMethod method, Object content,
      Class<T> responseType) {
    HttpHeaders headers = new HttpHeaders();
    addHeaderValues(headers);

    HttpEntity<Object> httpEntity = new HttpEntity<>(content, headers);

    final ResponseEntity<T> response = restTemplate.exchange(url, method, httpEntity, responseType);
    return response.getBody();
  }

  /**
   * @return either a json representation of ResultModel or a Path
   */
  public Object executeWithResponseExtractor(URI url) {
    return restTemplate.execute(url, HttpMethod.POST, this::addHeaderValues, this::extractResponse);
  }

  void addHeaderValues(ClientHttpRequest request) {
    addHeaderValues(request.getHeaders());
  }

  /**
   * @return either a json representation of ResultModel or a Path
   */
  Object extractResponse(ClientHttpResponse response) throws IOException {
    MediaType mediaType = response.getHeaders().getContentType();
    if (mediaType.equals(MediaType.APPLICATION_JSON)) {
      return org.apache.commons.io.IOUtils.toString(response.getBody(), StandardCharsets.UTF_8);
    } else {
      Path tempFile = Files.createTempFile("fileDownload", "");
      if (tempFile.toFile().exists()) {
        FileUtils.deleteQuietly(tempFile.toFile());
      }
      Files.copy(response.getBody(), tempFile);
      return tempFile;
    }
  }

  void addHeaderValues(HttpHeaders headers) {
    // update the headers
    headers.add(HttpHeaders.USER_AGENT, USER_AGENT_HTTP_REQUEST_HEADER_VALUE);
    headers.setAccept(acceptableMediaTypes);

    if (securityProperties != null) {
      for (String key : securityProperties.stringPropertyNames()) {
        headers.add(key, securityProperties.getProperty(key));
      }
    }
  }

  /**
   * build the url using the path and query params
   *
   * @param path : the part after the baseUrl
   * @param queryParams this needs to be an even number of strings in the form of paramName,
   *        paramValue....
   */
  public static URI createURI(String baseUrl, String path, String... queryParams) {
    UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(baseUrl).path(path);

    if (queryParams != null) {
      if (queryParams.length % 2 != 0) {
        throw new IllegalArgumentException("invalid queryParams count");
      }
      for (int i = 0; i < queryParams.length; i += 2) {
        builder.queryParam(queryParams[i], queryParams[i + 1]);
      }
    }
    return builder.build().encode().toUri();
  }
}
