/* ******************************************************************************
 * Copyright (c) 2019 Stephane Bastian
 *
 * This program and the accompanying materials are made available under the 2
 * 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 3
 *
 * Contributors: 1
 *   Stephane Bastian - initial API and implementation
 * ******************************************************************************/
package io.vertx.ext.web.handler.impl;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.Objects;
import java.util.function.BiConsumer;

import io.vertx.core.http.HttpHeaders;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.impl.logging.Logger;
import io.vertx.core.impl.logging.LoggerFactory;
import io.vertx.ext.auth.User;
import io.vertx.ext.auth.authorization.Authorization;
import io.vertx.ext.auth.authorization.AuthorizationContext;
import io.vertx.ext.auth.authorization.AuthorizationProvider;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.AuthorizationHandler;
import io.vertx.ext.web.handler.HttpException;

/**
 * Implementation of the {@link io.vertx.ext.web.handler.AuthorizationHandler}
 *
 * @author <a href="mail://stephane.bastian.dev@gmail.com">Stephane Bastian</a>
 */
public class AuthorizationHandlerImpl implements AuthorizationHandler {

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

  private final static int FORBIDDEN_CODE = 403;
  private final static HttpException FORBIDDEN_EXCEPTION = new HttpException(FORBIDDEN_CODE);

  private final Authorization authorization;
  private final Collection<AuthorizationProvider> authorizationProviders;
  private BiConsumer<RoutingContext, AuthorizationContext> variableHandler;

  public AuthorizationHandlerImpl(Authorization authorization) {
    this.authorization = Objects.requireNonNull(authorization);
    this.authorizationProviders = new ArrayList<>();
  }

  @Override
  public void handle(RoutingContext ctx) {
    final User user = ctx.user();
    if (user == null) {
      ctx.fail(FORBIDDEN_CODE, FORBIDDEN_EXCEPTION);
    } else {
      // before starting any potential async operation here
      // pause parsing the request body. The reason is that
      // we don't want to loose the body or protocol upgrades
      // for async operations
      final boolean parseEnded = ctx.request().isEnded();
      if (!parseEnded) {
        ctx.request().pause();
      }
      try {
        // create the authorization context
        final AuthorizationContext authorizationContext = AuthorizationContext.create(user);
        if (variableHandler != null) {
          variableHandler.accept(ctx, authorizationContext);
        }
        // check or fetch authorizations
        checkOrFetchAuthorizations(ctx, parseEnded, authorizationContext, authorizationProviders.iterator());
      } catch (RuntimeException e) {
        // resume as the error handler may allow this request to become valid again
        resume(ctx.request(), parseEnded);
        ctx.fail(e);
      }
    }
  }

  @Override
  public AuthorizationHandler variableConsumer(BiConsumer<RoutingContext, AuthorizationContext> handler) {
    this.variableHandler = handler;
    return this;
  }

  /**
   * this method checks that the specified authorization match the current content.
   * It doesn't fetch all providers at once in order to do early-out, but rather tries to be smart and fetch authorizations one provider at a time
   *
   * @param ctx the current routing context
   * @param authorizationContext the current authorization context
   * @param providers the providers iterator
   */
  private void checkOrFetchAuthorizations(RoutingContext ctx, boolean parseEnded, AuthorizationContext authorizationContext, Iterator<AuthorizationProvider> providers) {
    if (authorization.match(authorizationContext)) {
      // resume the processing of the request
      resume(ctx.request(), parseEnded);
      ctx.next();
      return;
    }

    final User user = ctx.user();

    if (user == null || !providers.hasNext()) {
      // resume as the error handler may allow this request to become valid again
      resume(ctx.request(), parseEnded);
      ctx.fail(FORBIDDEN_CODE, FORBIDDEN_EXCEPTION);
      return;
    }

    // there was no match, in this case we do the following:
    // 1) contact the next provider we haven't contacted yet
    // 2) if there is a match, get out right away otherwise repeat 1)
    while (providers.hasNext()) {
      AuthorizationProvider provider = providers.next();
      // we haven't fetched authorization from this provider yet
      if (!user.authorizations().getProviderIds().contains(provider.getId())) {
        provider.getAuthorizations(ctx.user(), authorizationResult -> {
          if (authorizationResult.failed()) {
            LOG.warn("An error occured getting authorization - providerId: " + provider.getId(), authorizationResult.cause());
            // note that we don't 'record' the fact that we tried to fetch the authorization provider. therefore, it will be re-fetched later-on
          }
          checkOrFetchAuthorizations(ctx, parseEnded, authorizationContext, providers);
        });
        // get out right now as the callback will decide what to do next
        return;
      }
    }
  }

  @Override
  public AuthorizationHandler addAuthorizationProvider(AuthorizationProvider authorizationProvider) {
    Objects.requireNonNull(authorizationProvider);

    this.authorizationProviders.add(authorizationProvider);
    return this;
  }

  private void resume(HttpServerRequest request, final boolean parseEnded) {
    if (!parseEnded && !request.headers().contains(HttpHeaders.UPGRADE, HttpHeaders.WEBSOCKET, true)) {
      request.resume();
    }
  }
}

@@ -1812,16 +1812,77 @@
 viders;%0A
+  private final Collection%3CString%3E authorizationProviderIds;%0A
   privat
@@ -1949,16 +1949,16 @@
 andler;%0A
-
 %0A  publi
@@ -2132,16 +2132,71 @@
 st%3C%3E();%0A
+    this.authorizationProviderIds = new ArrayList%3C%3E();%0A
   %7D%0A%0A  @
@@ -4320,32 +4320,66 @@
 viders.hasNext()
+ %7C%7C allAuthorizationsFetched(user)
 ) %7B%0A      // res
@@ -5766,17 +5766,16 @@
 vider);%0A
-%0A
     this
@@ -5822,24 +5822,94 @@
 nProvider);%0A
+    this.authorizationProviderIds.add(authorizationProvider.getId());%0A
     return t
@@ -6119,24 +6119,24 @@
 t.resume();%0A
-
     %7D%0A  %7D%0A%7D%0A
@@ -6132,11 +6132,531 @@
    %7D%0A  %7D
+%0A%0A  /**%0A   * Returns %3Ccode%3Etrue%3C/code%3E when the authorizations of all available%0A   * providers have been loaded for the given user which may have happened in a%0A   * previous request of the current user session.%0A   *%0A   * @param user%0A   * @return %3Ccode%3Etrue%3C/code%3E when the authorizations of all available%0A   *         providers have been loaded for the user%0A   */%0A  private boolean allAuthorizationsFetched(final User user) %7B%0A      return user.authorizations().getProviderIds().containsAll(authorizationProviderIds);%0A  %7D
 %0A%7D%0A

fix denied requests hanging with active session