package com.travelaudience.nexus.proxy;

import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpClient;
import io.vertx.core.http.HttpClientRequest;
import io.vertx.core.http.HttpClientResponse;
import io.vertx.core.http.HttpHeaders;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.ext.web.RoutingContext;

/**
 * A basic class which proxies user requests to a Nexus instance, conveying authentication information.
 *
 * @see <a href="https://books.sonatype.com/nexus-book/reference3/security.html#remote-user-token">Authentication via Remote User Token</a>
 */
public final class NexusHttpProxy {
    private static final CharSequence X_FORWARDED_PROTO = HttpHeaders.createOptimized("X-Forwarded-Proto");
    private static final CharSequence X_FORWARDED_FOR = HttpHeaders.createOptimized("X-Forwarded-For");

    private final String host;
    private final HttpClient httpClient;
    private final String nexusRutHeader;
    private final int port;

    private NexusHttpProxy(final Vertx vertx,
                           final String host,
                           final int port,
                           final String nexusRutHeader) {
        this.host = host;
        this.httpClient = vertx.createHttpClient();
        this.nexusRutHeader = nexusRutHeader;
        this.port = port;
    }

    /**
     * Creates a new instance of {@link NexusHttpProxy}.
     *
     * @param vertx          the base {@link Vertx} instance.
     * @param host           the host we will be proxying to.
     * @param port           the port we will be proxying to.
     * @param nexusRutHeader the name of the RUT authentication header as configured in Nexus.
     * @return a new instance of {@link NexusHttpProxy}.
     */
    public static final NexusHttpProxy create(final Vertx vertx,
                                              final String host,
                                              final int port,
                                              final String nexusRutHeader) {
        return new NexusHttpProxy(vertx, host, port, nexusRutHeader);
    }

    /**
     * Proxies the specified HTTP request, enriching its headers with authentication information.
     *
     * @param userId  the ID of the user making the request.
     * @param origReq the original request (i.e., {@link RoutingContext#request()}.
     * @param origRes the original response (i.e., {@link RoutingContext#request()}.
     */
    public void proxyUserRequest(final String userId,
                                 final HttpServerRequest origReq,
                                 final HttpServerResponse origRes) {
        final Handler<HttpClientResponse> proxiedResHandler = proxiedRes -> {
            origRes.setChunked(true);
            origRes.setStatusCode(proxiedRes.statusCode());
            origRes.headers().setAll(proxiedRes.headers());
            proxiedRes.handler(origRes::write);
            proxiedRes.endHandler(v -> origRes.end());
        };

        final HttpClientRequest proxiedReq;
        proxiedReq = httpClient.request(origReq.method(), port, host, origReq.uri(), proxiedResHandler);
        if(origReq.method() == HttpMethod.OTHER) {
            proxiedReq.setRawMethod(origReq.rawMethod());
        }
        proxiedReq.setChunked(true);
        proxiedReq.headers().add(X_FORWARDED_PROTO, getHeader(origReq, X_FORWARDED_PROTO, origReq.scheme()));
        proxiedReq.headers().add(X_FORWARDED_FOR, getHeader(origReq, X_FORWARDED_FOR, origReq.remoteAddress().host()));
        proxiedReq.headers().addAll(origReq.headers());
        injectRutHeader(proxiedReq, userId);
        origReq.handler(proxiedReq::write);
        origReq.endHandler(v -> proxiedReq.end());
    }

    private final void injectRutHeader(final HttpClientRequest req,
                                       final String userId) {
        if (nexusRutHeader != null && nexusRutHeader.length() > 0 && userId != null && userId.length() > 0) {
            req.headers().add(nexusRutHeader, userId);
        }
    }

    private static final String getHeader(final HttpServerRequest req,
                                          final CharSequence name,
                                          final String defaultValue) {
        final String originalHeader = req.headers().get(name);

        if (originalHeader == null) {
            return defaultValue;
        } else {
            return originalHeader;
        }
    }
}