/**
 * Copyright 2018 The Feign Authors
 *
 * Licensed 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 feign.reactor.resttemplate.client;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import feign.MethodMetadata;
import feign.reactor.client.ReactiveHttpClient;
import feign.reactor.client.ReactiveHttpRequest;
import feign.reactor.client.ReactiveHttpResponse;
import feign.reactor.client.ReadTimeoutException;
import org.reactivestreams.Publisher;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestTemplate;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.SocketTimeoutException;
import java.util.List;
import java.util.Map;

import static feign.Util.resolveLastTypeParameter;
import static org.springframework.core.ParameterizedTypeReference.forType;

public class RestTemplateFakeReactiveHttpClient implements ReactiveHttpClient {

  private final RestTemplate restTemplate;
  private final boolean acceptGzip;
  private final Type returnPublisherType;
  private final ParameterizedTypeReference<Object> returnActualType;

  RestTemplateFakeReactiveHttpClient(MethodMetadata methodMetadata,
      RestTemplate restTemplate,
      boolean acceptGzip) {
    this.restTemplate = restTemplate;
    this.acceptGzip = acceptGzip;

    ObjectMapper mapper = new ObjectMapper();
    mapper.registerModule(new JavaTimeModule());

    MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
    converter.setObjectMapper(mapper);
    restTemplate.getMessageConverters().add(0, converter);


    final Type returnType = methodMetadata.returnType();
    returnPublisherType = ((ParameterizedType) returnType).getRawType();
    returnActualType = forType(
        resolveLastTypeParameter(returnType, (Class<?>) returnPublisherType));
  }

  @Override
  public Mono<ReactiveHttpResponse> executeRequest(ReactiveHttpRequest request) {

    Mono<Object> bodyMono;
    if (request.body() instanceof Mono) {
      bodyMono = ((Mono<Object>) request.body());
    } else if (request.body() instanceof Flux) {
      bodyMono = ((Flux) request.body()).collectList();
    } else {
      bodyMono = Mono.just(request.body());
    }
    bodyMono = bodyMono.switchIfEmpty(Mono.just(new byte[0]));

    return bodyMono.<ReactiveHttpResponse>flatMap(body -> {
      MultiValueMap<String, String> headers = new LinkedMultiValueMap<>(request.headers());
      if (acceptGzip) {
        headers.add("Accept-Encoding", "gzip");
      }

      ResponseEntity response =
              restTemplate.exchange(request.uri().toString(), HttpMethod.valueOf(request.method()),
                      new HttpEntity<>(body, headers), responseType());

      return Mono.just(new FakeReactiveHttpResponse(response, returnPublisherType));
    })
            .onErrorMap(ex -> ex instanceof ResourceAccessException
                                && ex.getCause() instanceof SocketTimeoutException,
                    ReadTimeoutException::new)
            .onErrorResume(HttpStatusCodeException.class,
                    ex -> Mono.just(new ErrorReactiveHttpResponse(ex)));
  }

  private ParameterizedTypeReference<Object> responseType(){
    if (returnPublisherType == Mono.class) {
      return returnActualType;
    } else {
      return forType(new ParameterizedType() {
        @Override
        public Type[] getActualTypeArguments() {
          return new Type[] {returnActualType.getType()};
        }

        @Override
        public Type getRawType() {
          return List.class;
        }

        @Override
        public Type getOwnerType() {
          return null;
        }
      });
    }
  }

  private static class FakeReactiveHttpResponse implements ReactiveHttpResponse{

    private final ResponseEntity response;
    private final Type returnPublisherType;

    private FakeReactiveHttpResponse(ResponseEntity response, Type returnPublisherType) {
      this.response = response;
      this.returnPublisherType = returnPublisherType;
    }

    @Override
    public int status() {
      return response.getStatusCodeValue();
    }

    @Override
    public Map<String, List<String>> headers() {
      return response.getHeaders();
    }

    @Override
    public Publisher<Object> body() {
      if (returnPublisherType == Mono.class) {
        return Mono.just(response.getBody());
      } else {
        return Flux.fromIterable((List)response.getBody());
      }
    }

    @Override
    public Mono<byte[]> bodyData() {
      return Mono.just(new byte[0]);
    }
  }

  private static class ErrorReactiveHttpResponse implements ReactiveHttpResponse{

    private final HttpStatusCodeException ex;

    private ErrorReactiveHttpResponse(HttpStatusCodeException ex) {
      this.ex = ex;
    }

    @Override
    public int status() {
      return ex.getStatusCode().value();
    }

    @Override
    public Map<String, List<String>> headers() {
      return ex.getResponseHeaders();
    }

    @Override
    public Publisher<Object> body() {
      return Mono.empty();
    }

    @Override
    public Mono<byte[]> bodyData() {
      return Mono.just(ex.getResponseBodyAsByteArray());
    }
  }
}