/*******************************************************************************
 * Copyright (c) 2019, 2020 Contributors to the Eclipse Foundation
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 *******************************************************************************/

package org.eclipse.hono.service.http;

import java.util.Base64;
import java.util.Objects;

import javax.net.ssl.SSLSession;

import org.eclipse.hono.client.TenantClientFactory;
import org.eclipse.hono.config.ProtocolAdapterProperties;
import org.eclipse.hono.service.BaseExecutionContextTenantAndAuthIdProvider;
import org.eclipse.hono.util.TenantObjectWithAuthId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.opentracing.SpanContext;
import io.vertx.core.Future;
import io.vertx.core.http.HttpHeaders;
import io.vertx.ext.web.RoutingContext;

/**
 * Provides a method to determine the tenant and auth-id of a HTTP request from the given HttpContext.
 */
public class HttpContextTenantAndAuthIdProvider extends BaseExecutionContextTenantAndAuthIdProvider<HttpContext> {

    private static final Logger LOG = LoggerFactory.getLogger(HttpContextTenantAndAuthIdProvider.class);

    private final String tenantIdContextParamName;
    private final String deviceIdContextParamName;

    /**
     * Creates a new HttpContextTenantAndAuthIdProvider.
     *
     * @param config The configuration.
     * @param tenantClientFactory The factory to use for creating a Tenant service client.
     * @throws NullPointerException if any of the parameters is {@code null}.
     */
    public HttpContextTenantAndAuthIdProvider(final ProtocolAdapterProperties config,
            final TenantClientFactory tenantClientFactory) {
        super(config, tenantClientFactory);
        this.tenantIdContextParamName = null;
        this.deviceIdContextParamName = null;
    }

    /**
     * Creates a new HttpContextTenantAndAuthIdProvider.
     *
     * @param config The configuration.
     * @param tenantClientFactory The factory to use for creating a Tenant service client.
     * @param tenantIdContextParamName The name of the HttpContext parameter name that provides the tenant id in case of
     *            an unauthenticated request.
     * @param deviceIdContextParamName The name of the HttpContext parameter name that provides the device id in case of
     *            an unauthenticated request. The device id will be used as auth-id for the created
     *            TenantObjectWithAuthId objects.
     * @throws NullPointerException if any of the parameters is {@code null}.
     */
    public HttpContextTenantAndAuthIdProvider(final ProtocolAdapterProperties config,
            final TenantClientFactory tenantClientFactory,
            final String tenantIdContextParamName,
            final String deviceIdContextParamName) {
        super(config, tenantClientFactory);
        this.tenantIdContextParamName = Objects.requireNonNull(tenantIdContextParamName);
        this.deviceIdContextParamName = Objects.requireNonNull(deviceIdContextParamName);
    }

    @Override
    public Future<TenantObjectWithAuthId> get(final HttpContext context, final SpanContext spanContext) {
        if (config.isAuthenticationRequired()) {
            return getFromClientCertificate(context.getRoutingContext(), spanContext)
                    .recover(thr -> getFromAuthHeader(context.getRoutingContext(), spanContext));
        }
        if (tenantIdContextParamName != null && deviceIdContextParamName != null) {
            final String tenantId = context.get(tenantIdContextParamName);
            final String deviceId = context.get(deviceIdContextParamName);
            if (tenantId != null && deviceId != null) {
                // unauthenticated request
                return tenantClientFactory.getOrCreateTenantClient()
                        .compose(tenantClient -> tenantClient.get(tenantId, spanContext))
                        .map(tenantObject -> new TenantObjectWithAuthId(tenantObject, deviceId));
            }
        }
        return Future.failedFuture("tenant could not be determined");
    }

    /**
     * Get the tenant and auth-id from the client certificate of the SSL session in the given routing context request.
     *
     * @param ctx The execution context.
     * @param spanContext The OpenTracing context to use for tracking the operation (may be {@code null}).
     * @return A future indicating the outcome of the operation.
     *         <p>
     *         The future will fail if tenant and auth-id information could not be retrieved from the given request
     *         or if there was an error obtaining the tenant object. In the latter case the future will be failed with a
     *         {@link org.eclipse.hono.client.ServiceInvocationException}.
     *         <p>
     *         Otherwise the future will contain the created <em>TenantObjectWithAuthId</em>.
     */
    protected final Future<TenantObjectWithAuthId> getFromClientCertificate(final RoutingContext ctx,
            final SpanContext spanContext) {
        if (!ctx.request().isSSL()) {
            return Future.failedFuture("no cert found (not SSL/TLS encrypted)");
        }
        final SSLSession sslSession = ctx.request().sslSession();
        return getFromClientCertificate(sslSession, spanContext);
    }

    /**
     * Get the tenant and auth-id from the <em>authorization</em> header of the given routing context request.
     *
     * @param ctx The execution context.
     * @param spanContext The OpenTracing context to use for tracking the operation (may be {@code null}).
     * @return A future indicating the outcome of the operation.
     *         <p>
     *         The future will fail if tenant and auth-id information could not be retrieved from the header
     *         or if there was an error obtaining the tenant object. In the latter case the future will be failed with a
     *         {@link org.eclipse.hono.client.ServiceInvocationException}.
     *         <p>
     *         Otherwise the future will contain the created <em>TenantObjectWithAuthId</em>.
     */
    protected final Future<TenantObjectWithAuthId> getFromAuthHeader(final RoutingContext ctx,
            final SpanContext spanContext) {
        final String authorizationHeader = ctx.request().headers().get(HttpHeaders.AUTHORIZATION);
        if (authorizationHeader == null) {
            return Future.failedFuture("no auth header found");
        }
        String userName = null;
        try {
            final int idx = authorizationHeader.indexOf(' ');
            if (idx > 0 && "Basic".equalsIgnoreCase(authorizationHeader.substring(0, idx))) {
                final String authorization = authorizationHeader.substring(idx + 1);
                final String decoded = new String(Base64.getDecoder().decode(authorization));
                final int colonIdx = decoded.indexOf(":");
                userName = colonIdx != -1 ? decoded.substring(0, colonIdx) : decoded;
            }
        } catch (final RuntimeException e) {
            LOG.debug("error parsing auth header: {}", e.getMessage());
        }
        if (userName == null) {
            return Future.failedFuture("unsupported auth header value");
        }
        return getFromUserName(userName, spanContext);
    }
}