package feign;

import static feign.FeignException.errorExecuting;
import static feign.FeignException.errorReading;
import static feign.Util.ensureClosed;

import feign.InvocationHandlerFactory.MethodHandler;
import feign.codec.DecodeException;
import feign.codec.Decoder;
import feign.codec.ErrorDecoder;
import feign.vertx.VertxHttpClient;
import io.vertx.core.AsyncResult;
import io.vertx.core.Future;
import io.vertx.core.Handler;

import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.util.List;

/**
 * Method handler for asynchronous HTTP requests via {@link VertxHttpClient}.
 * Inspired by {@link SynchronousMethodHandler}.
 *
 * @author Alexei KLENIN
 * @author Gordon McKinney
 */
final class AsynchronousMethodHandler implements MethodHandler {
  private static final long MAX_RESPONSE_BUFFER_SIZE = 8192L;

  private final MethodMetadata metadata;
  private final Target<?> target;
  private final VertxHttpClient client;
  private final Retryer retryer;
  private final List<RequestInterceptor> requestInterceptors;
  private final Logger logger;
  private final Logger.Level logLevel;
  private final RequestTemplate.Factory buildTemplateFromArgs;
  private final Decoder decoder;
  private final ErrorDecoder errorDecoder;
  private final boolean decode404;

  private AsynchronousMethodHandler(
      final Target<?> target,
      final VertxHttpClient client,
      final Retryer retryer,
      final List<RequestInterceptor> requestInterceptors,
      final Logger logger,
      final Logger.Level logLevel,
      final MethodMetadata metadata,
      final RequestTemplate.Factory buildTemplateFromArgs,
      final Decoder decoder,
      final ErrorDecoder errorDecoder,
      final boolean decode404) {
    this.target = target;
    this.client = client;
    this.retryer = retryer;
    this.requestInterceptors = requestInterceptors;
    this.logger = logger;
    this.logLevel = logLevel;
    this.metadata = metadata;
    this.buildTemplateFromArgs = buildTemplateFromArgs;
    this.errorDecoder = errorDecoder;
    this.decoder = decoder;
    this.decode404 = decode404;
  }

  @Override
  @SuppressWarnings("unchecked")
  public Future invoke(final Object[] argv) {
    final RequestTemplate template = buildTemplateFromArgs.create(argv);
    final Retryer retryer = this.retryer.clone();

    final ResultHandlerWithRetryer handler = new ResultHandlerWithRetryer(template, retryer);
    executeAndDecode(template).setHandler(handler);

    return handler.getResultFuture();
  }

  /**
   * Executes request from {@code template} with {@code this.client} and decodes the response.
   * Result or occurred error wrapped in returned Future.
   *
   * @param template  request template
   *
   * @return future with decoded result or occurred error
   */
  private Future<Object> executeAndDecode(final RequestTemplate template) {
    final Request request = targetRequest(template);
    final Future<Object> decodedResultFuture = Future.future();

    logRequest(request);

    final Instant start = Instant.now();

    client.execute(request).setHandler(res -> {
      boolean shouldClose = true;

      final long elapsedTime = Duration.between(start, Instant.now()).toMillis();

      if (res.succeeded()) {

        /* Just as executeAndDecode in SynchronousMethodHandler but wrapped in Future */
        Response response = res.result();

        try {
          // TODO: check why this buffering is needed
          if (logLevel != Logger.Level.NONE) {
            response = logger.logAndRebufferResponse(
                metadata.configKey(),
                logLevel,
                response,
                elapsedTime);
          }

          if (Response.class == metadata.returnType()) {
            if (response.body() == null) {
              decodedResultFuture.complete(response);
            } else if (response.body().length() == null
                || response.body().length() > MAX_RESPONSE_BUFFER_SIZE) {
              shouldClose = false;
              decodedResultFuture.complete(response);
            } else {
              final byte[] bodyData = Util.toByteArray(response.body().asInputStream());
              decodedResultFuture.complete(Response.create(
                  response.status(),
                  response.reason(),
                  response.headers(),
                  bodyData));
            }
          } else if (response.status() >= 200 && response.status() < 300) {
            if (Void.class == metadata.returnType()) {
              decodedResultFuture.complete();
            } else {
              decodedResultFuture.complete(decode(response));
            }
          } else if (decode404 && response.status() == 404) {
            decodedResultFuture.complete(decoder.decode(response, metadata.returnType()));
          } else {
            decodedResultFuture.fail(errorDecoder.decode(metadata.configKey(), response));
          }
        } catch (final IOException ioException) {
          logIoException(ioException, elapsedTime);
          decodedResultFuture.fail(errorReading(request, response, ioException));
        } catch (FeignException exception) {
          decodedResultFuture.fail(exception);
        } finally {
          if (shouldClose) {
            ensureClosed(response.body());
          }
        }
      } else {
        if (res.cause() instanceof IOException) {
          logIoException((IOException) res.cause(), elapsedTime);
          decodedResultFuture.fail(errorExecuting(request, (IOException) res.cause()));
        } else {
          decodedResultFuture.fail(res.cause());
        }
      }
    });

    return decodedResultFuture;
  }

  /**
   * Associates request to defined target.
   *
   * @param template  request template
   *
   * @return fully formed request
   */
  private Request targetRequest(final RequestTemplate template) {
    for (final RequestInterceptor interceptor : requestInterceptors) {
      interceptor.apply(template);
    }

    return target.apply(new RequestTemplate(template));
  }

  /**
   * Transforms HTTP response body into object using decoder.
   *
   * @param response  HTTP response
   *
   * @return decoded result
   *
   * @throws IOException IO exception during the reading of InputStream of response
   * @throws DecodeException when decoding failed due to a checked or unchecked exception besides
   *     IOException
   * @throws FeignException when decoding succeeds, but conveys the operation failed
   */
  private Object decode(final Response response) throws IOException, FeignException {
    try {
      return decoder.decode(response, metadata.returnType());
    } catch (final FeignException feignException) {
      /* All feign exception including decode exceptions */
      throw feignException;
    } catch (final RuntimeException unexpectedException) {
      /* Any unexpected exception */
      throw new DecodeException(unexpectedException.getMessage(), unexpectedException);
    }
  }

  /**
   * Logs request.
   *
   * @param request  HTTP request
   */
  private void logRequest(final Request request) {
    if (logLevel != Logger.Level.NONE) {
      logger.logRequest(metadata.configKey(), logLevel, request);
    }
  }

  /**
   * Logs IO exception.
   *
   * @param exception  IO exception
   * @param elapsedTime  time spent to execute request
   */
  private void logIoException(final IOException exception, final long elapsedTime) {
    if (logLevel != Logger.Level.NONE) {
      logger.logIOException(metadata.configKey(), logLevel, exception, elapsedTime);
    }
  }

  /**
   * Logs retry.
   */
  private void logRetry() {
    if (logLevel != Logger.Level.NONE) {
      logger.logRetry(metadata.configKey(), logLevel);
    }
  }

  static final class Factory {
    private final VertxHttpClient client;
    private final Retryer retryer;
    private final List<RequestInterceptor> requestInterceptors;
    private final Logger logger;
    private final Logger.Level logLevel;
    private final boolean decode404;

    Factory(
        final VertxHttpClient client,
        final Retryer retryer,
        final List<RequestInterceptor> requestInterceptors,
        final Logger logger,
        final Logger.Level logLevel,
        final boolean decode404) {
      this.client = client;
      this.retryer = retryer;
      this.requestInterceptors = requestInterceptors;
      this.logger = logger;
      this.logLevel = logLevel;
      this.decode404 = decode404;
    }

    MethodHandler create(
        final Target<?> target,
        final MethodMetadata metadata,
        final RequestTemplate.Factory buildTemplateFromArgs,
        final Decoder decoder,
        final ErrorDecoder errorDecoder) {
      return new AsynchronousMethodHandler(
          target,
          client,
          retryer,
          requestInterceptors,
          logger,
          logLevel,
          metadata,
          buildTemplateFromArgs,
          decoder,
          errorDecoder,
          decode404);
    }
  }

  /**
   * Handler for {@link AsyncResult} able to retry execution of request. In this case handler passed
   * to new request.
   *
   * @param <T>  type of response
   */
  private final class ResultHandlerWithRetryer<T> implements Handler<AsyncResult<T>> {
    private final RequestTemplate template;
    private final Retryer retryer;
    private final Future<T> resultFuture = Future.future();

    private ResultHandlerWithRetryer(final RequestTemplate template, final Retryer retryer) {
      this.template = template;
      this.retryer = retryer;
    }

    /**
     * In case of failure retries HTTP request passing itself as handler.
     *
     * @param result  result of asynchronous HTTP request execution
     */
    @Override
    @SuppressWarnings("unchecked")
    public void handle(AsyncResult<T> result) {
      if (result.succeeded()) {
        this.resultFuture.complete(result.result());
      } else {
        try {
          throw result.cause();
        } catch (final RetryableException retryableException) {
          try {
            this.retryer.continueOrPropagate(retryableException);
            logRetry();
            ((Future<T>) executeAndDecode(this.template)).setHandler(this);
          } catch (final RetryableException noMoreRetryAttempts) {
            this.resultFuture.fail(noMoreRetryAttempts);
          }
        } catch (final Throwable otherException) {
          this.resultFuture.fail(otherException);
        }
      }
    }

    /**
     * Returns a future that will be completed after successful execution or after all attempts
     * finished by fail.
     *
     * @return future with result of attempts
     */
    private Future<?> getResultFuture() {
      return this.resultFuture;
    }
  }
}