package com.github.kennedyoliveira.hystrix.contrib.standalone.dashboard;

import io.vertx.core.Handler;
import io.vertx.core.MultiMap;
import io.vertx.core.Vertx;
import io.vertx.core.VertxException;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.*;
import io.vertx.core.streams.Pump;
import io.vertx.ext.web.RoutingContext;
import lombok.extern.slf4j.Slf4j;

import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Optional;

/**
 * This handler will proxy connection to a Hystrix Event Metrics stream using Basic Auth if present, and compressing the response.
 *
 * @author Kennedy Oliveira
 */
@Slf4j
public class HystrixDashboardProxyConnectionHandler implements Handler<RoutingContext> {

  /**
   * Creates a new {@link HystrixDashboardProxyConnectionHandler}, that will handle proxying connection to a Hystrix Event Metrics stream using Basic Auth if present
   *
   * @return The new {@link HystrixDashboardProxyConnectionHandler}
   */
  static HystrixDashboardProxyConnectionHandler create() {
    return new HystrixDashboardProxyConnectionHandler();
  }

  @Override
  public void handle(RoutingContext requestCtx) {
    getProxyUrl(requestCtx)
        .flatMap(proxiedUrl -> createUriFromUrl(proxiedUrl, requestCtx))
        .ifPresent(uri -> proxyRequest(uri, requestCtx));
  }

  /**
   * Extract the url to Proxy.
   *
   * @param requestCtx Context of the current request.
   * @return The url to proxy or null if it wasn't found.
   */
  Optional<String> getProxyUrl(RoutingContext requestCtx) {
    final HttpServerRequest serverRequest = requestCtx.request();
    final HttpServerResponse serverResponse = requestCtx.response();

    // origin = metrics stream endpoint
    String origin = serverRequest.getParam("origin");
    if (origin == null || origin.isEmpty()) {
      log.warn("Request without origin");
      serverResponse.setStatusCode(500)
                    .end(Buffer.buffer("Required parameter 'origin' missing. Example: 107.20.175.135:7001".getBytes(StandardCharsets.UTF_8)));
      return Optional.empty();
    }
    origin = origin.trim();

    boolean hasFirstParameter = false;
    StringBuilder url = new StringBuilder();
    // if there is no http, i add
    if (!origin.startsWith("http")) {
      url.append("http://");
    }
    url.append(origin);

    // if contains any query parameter
    if (origin.contains("?")) {
      hasFirstParameter = true;
    }

    // add the request params to the url to proxy, because need to forward Delay and maybe another future param
    MultiMap params = serverRequest.params();
    for (String key : params.names()) {
      if (!"origin".equals(key) && !"authorization".equals(key)) {
        String value = params.get(key);

        if (hasFirstParameter) {
          url.append("&");
        } else {
          url.append("?");
          hasFirstParameter = true;
        }
        url.append(key).append("=").append(value);
      }
    }

    return Optional.of(url.toString());
  }

  /**
   * Tries to transform the {@code proxiedUrl} in a URI, if fails, return a response to the caller and end the request.
   *
   * @param proxiedUrl Url to convert
   * @param requestCtx Context of the current request.
   * @return If succeed, a {@link URI}, otherwise {@code null}.
   */
  Optional<URI> createUriFromUrl(String proxiedUrl, RoutingContext requestCtx) {
    try {
      return Optional.of(URI.create(proxiedUrl));
    } catch (Exception e) {
      final String errorMsg = String.format("Failed to parse the url [%s] to proxy.", proxiedUrl);
      log.error(errorMsg, e);
      requestCtx.response().setStatusCode(500).end(errorMsg);
      return Optional.empty();
    }
  }

  /**
   * Proxy the request to url {@code proxiedUrl}.
   *
   * @param proxiedUrl The url to proxy.
   * @param requestCtx Context of the current request.
   */
  void proxyRequest(URI proxiedUrl, RoutingContext requestCtx) {
    final HttpServerRequest serverRequest = requestCtx.request();
    final HttpServerResponse serverResponse = requestCtx.response();

    final String host = proxiedUrl.getHost();
    final String scheme = proxiedUrl.getScheme();
    final String path = proxiedUrl.getPath();
    int port = proxiedUrl.getPort();

    if (port == -1) {
      // if there are no port, and the scheme is HTTPS, set as 443, default HTTPS port, else set as 80, default HTTP port
      if ("https".equalsIgnoreCase(scheme)) {
        log.warn("No port specified in the url to proxy [{}], using 443 since it's a HTTPS request.", proxiedUrl);
        port = 443;
      } else {
        log.warn("No port specified in the url to proxy [{}], using 80", proxiedUrl);
        port = 80;
      }
    }

    log.info("Proxing request to {}", proxiedUrl);

    // create a request
    final HttpClient httpClient = createHttpClient(requestCtx.vertx());
    final HttpClientRequest httpClientRequest = httpClient.get(port, host, path + (proxiedUrl.getQuery() != null ? "?" + proxiedUrl.getQuery() : ""));

    // setup basic auth if present
    configureBasicAuth(serverRequest, httpClientRequest);

    // TODO Implement the connection close that is available on vert.x 3.3 instead of closing the client

    // set the serverResponse handler
    httpClientRequest.handler(clientResponse -> {
      // response success
      if (clientResponse.statusCode() == 200) {
        serverResponse.setChunked(true);
        serverResponse.setStatusCode(200);

        // setup the headers from proxied request, ignoring the transfer encoding
        clientResponse.headers().forEach(headerEntry -> {
          if (!HttpHeaders.TRANSFER_ENCODING.equals(headerEntry.getKey()))
            serverResponse.putHeader(headerEntry.getKey(), headerEntry.getValue());
        });

        // transfer response from the proxied  to the server response
        final Pump pump = Pump.pump(clientResponse, serverResponse);

        // if there are any errors, usually connection closed, stop the pump and close the connection if it still open
        clientResponse.exceptionHandler(t -> {
          if (t instanceof VertxException && t.getMessage().equalsIgnoreCase("Connection was closed")) {
            log.info("Proxied connection stopped.");
          } else {
            log.error("Proxy response", t);
          }
          closeQuietlyHttpClient(httpClient);
        });

        // start the transferring
        pump.start();
      } else {
        log.error("Connecting to the proxied url: Status Code: {}, Status Message: {}", clientResponse.statusCode(), clientResponse.statusMessage());
        serverResponse.setStatusCode(500).end("Fail to connect to client, response code: " + clientResponse.statusCode());
        closeQuietlyHttpClient(httpClient);
      }
    });

    // handle errors on the client side (hystrix-dashboard client)
    requestCtx.response().closeHandler(ignored -> {
      log.warn("[Client disconnected] Connection closed on client side");
      log.info("Stopping the proxying...");
      closeQuietlyHttpClient(httpClient);
    });

    // handle exception on request
    serverRequest.exceptionHandler(t -> {
      log.error("On server request", t);
      closeQuietlyHttpClient(httpClient);
    });

    // handle exceptions on proxied server side
    httpClientRequest.exceptionHandler(t -> {
      log.error("Proxying request", t);
      serverResponse.setStatusCode(500);

      if (t.getMessage() != null)
        serverResponse.end(t.getMessage());
      else
        serverResponse.end();

      closeQuietlyHttpClient(httpClient);
    });

    // request timeout
    httpClientRequest.setTimeout(5000L);

    // do the request
    httpClientRequest.end();
  }

  /**
   * Configure basic auth for proxied stream
   *
   * @param serverRequest     request
   * @param httpClientRequest client that will proxy the request
   */
  void configureBasicAuth(HttpServerRequest serverRequest, HttpClientRequest httpClientRequest) {
    final String authorization = serverRequest.getParam("authorization");
    if (authorization != null) {
      httpClientRequest.putHeader(HttpHeaders.AUTHORIZATION, authorization);
    }
  }

  /**
   * If the HttpClient is already closed when you try to close it again it throws {@link IllegalStateException}, this method swallow it.
   *
   * @param client Client to close.
   */
  @SuppressWarnings({"PMD.EmptyCatchBlock"})
  private void closeQuietlyHttpClient(HttpClient client) {
    try {
      client.close();
    } catch (Exception ignored) { }
  }

  /**
   * Initialize if need and returns the {@link HttpClient} for proxying requests.
   *
   * @return The {@link HttpClient} for proxying requests.
   */
  private HttpClient createHttpClient(Vertx vertx) {
    final HttpClientOptions httpClientOptions = new HttpClientOptions().setKeepAlive(false)
                                                                       .setTryUseCompression(true)
                                                                       .setMaxPoolSize(1); // just 1 because the client will be closed when the request end

    return vertx.createHttpClient(httpClientOptions);
  }
}