
package io.gravitee.am.gateway.handler.oauth2.service.request;

import io.gravitee.am.gateway.handler.oauth2.exception.InvalidScopeException;
import io.gravitee.am.model.oidc.Client;
import io.gravitee.am.model.Role;
import io.gravitee.am.model.User;
import io.reactivex.Single;

import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import static io.gravitee.am.common.oidc.Scope.SCOPE_DELIMITER;

/**
 * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
 * @author GraviteeSource Team
 */
public abstract class AbstractRequestResolver<R extends OAuth2Request> {

    /**
     * If the client omits the scope parameter when requesting authorization, the authorization server MUST either process the
     * request using a pre-defined default value or fail the request indicating an invalid scope.
     * See <a href="https://tools.ietf.org/html/rfc6749#section-3.3">3.3. Access Token Scope</a>
     *
     * @param request the request to resolve
     * @param client the client which trigger the request
     * @return the oauth 2.0 request
     */
    protected Single<R> resolveAuthorizedScopes(R request, Client client, User endUser) {
        final List<String> clientScopes = client.getScopes();
        final Set<String> requestScopes = request.getScopes();
        Set<String> clientResolvedScopes = new HashSet<>();
        Set<String> resolvedScopes = new HashSet<>();
        Set<String> invalidScopes = new HashSet<>();
        // client scopes
        if (clientScopes != null && !clientScopes.isEmpty()) {
            // no requested scope, set default client scopes to the request
            if (requestScopes == null || requestScopes.isEmpty()) {
                resolvedScopes.addAll(new HashSet<>(clientScopes));
            } else {
                // filter the actual scopes granted by the client
                for (String scope : requestScopes) {
                    if (clientScopes.contains(scope)) {
                        resolvedScopes.add(scope);
                        clientResolvedScopes.add(scope);
                    } else {
                        invalidScopes.add(scope);
                    }
                }
            }
        }

        // user scopes
        if (endUser != null && client.isEnhanceScopesWithUserPermissions()) {
            Set<Role> roles = endUser.getRolesPermissions();
            if (roles != null && !roles.isEmpty()) {
                Set<String> permissions = roles.stream()
                        .map(role -> role.getOauthScopes() != null ? role.getOauthScopes() : Collections.<String>emptyList())
                        .flatMap(List::stream)
                        .collect(Collectors.toSet());
                
                if (requestScopes != null) {
                	// filter the actual scopes granted by the resource owner
                    requestScopes.forEach(scope -> {
                        if (!permissions.contains(scope) && !clientResolvedScopes.contains(scope)) {
                        	invalidScopes.add(scope);
                        }
                    });
                }
                
                // The request must be enhanced with all of user's permissions
                invalidScopes.removeAll(permissions);
                resolvedScopes.addAll(permissions);
            }
        }

        if (!invalidScopes.isEmpty()) {
            return Single.error(new InvalidScopeException("Invalid scope(s): " + invalidScopes.stream().collect(Collectors.joining(SCOPE_DELIMITER))));
        }

        if (resolvedScopes.isEmpty() && (requestScopes != null && !requestScopes.isEmpty())) {
            return Single.error(new InvalidScopeException("Invalid scope(s): " + requestScopes.stream().collect(Collectors.joining(SCOPE_DELIMITER))));
        }

        // only put default values if there is no requested scopes
        if (requestScopes == null || requestScopes.isEmpty()) {
            request.setScopes(resolvedScopes);
        }

        return Single.just(request);
    }
}
