/*
 * Licensed to the University Corporation for Advanced Internet Development, 
 * Inc. (UCAID) under one or more contributor license agreements. See the 
 * NOTICE file distributed with this work for additional information regarding
 * copyright ownership. The UCAID licenses this file to You under the Apache 
 * License, Version 2.0 (the "License"); you may not use this file except in 
 * compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.shibboleth.idp.oidc.flow;

import com.google.common.base.Strings;
import net.shibboleth.idp.authn.AuthenticationFlowDescriptor;
import net.shibboleth.idp.authn.context.AuthenticationContext;
import net.shibboleth.idp.authn.context.RequestedPrincipalContext;
import net.shibboleth.idp.oidc.config.OIDCConstants;
import net.shibboleth.idp.profile.AbstractProfileAction;
import net.shibboleth.idp.saml.authn.principal.AuthnContextClassRefPrincipal;
import org.mitre.oauth2.service.ClientDetailsEntityService;
import org.opensaml.profile.context.ProfileRequestContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.provider.AuthorizationRequest;
import org.springframework.webflow.execution.Event;
import org.springframework.webflow.execution.RequestContext;

import javax.annotation.Nonnull;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Map;

/**
 * Builds an authentication context message from an incoming request.
 */
public class BuildAuthenticationContextAction extends AbstractProfileAction {
    /**
     * The Log.
     */
    private final Logger log = LoggerFactory.getLogger(BuildAuthenticationContextAction.class);

    /**
     * The Available authentication flows.
     */
    private List<AuthenticationFlowDescriptor> availableAuthenticationFlows;

    /**
     * The Authentication principal weight map.
     */
    private Map<AuthnContextClassRefPrincipal, Integer> authenticationPrincipalWeightMap;

    /**
     * The Client service.
     */
    @Autowired
    private ClientDetailsEntityService clientService;

    /**
     * Instantiates a new authentication context action.
     */
    public BuildAuthenticationContextAction() {
    }

    /**
     * Sets available authentication flows.
     *
     * @param flows the flows
     */
    public void setAvailableAuthenticationFlows(final List<AuthenticationFlowDescriptor> flows) {
        this.availableAuthenticationFlows = flows;
    }

    /**
     * Sets authentication principal weight map.
     *
     * @param map the map
     */
    public void setAuthenticationPrincipalWeightMap(final Map<AuthnContextClassRefPrincipal, Integer> map) {
        this.authenticationPrincipalWeightMap = map;
    }


    @Nonnull
    @Override
    protected Event doExecute(@Nonnull final RequestContext springRequestContext,
                              @Nonnull final ProfileRequestContext profileRequestContext) {
        log.debug("{} Building authentication context", getLogPrefix());
        final AuthenticationContext ac = new AuthenticationContext();
        
        final OIDCAuthorizationRequestContext authZContext =
                profileRequestContext.getSubcontext(OIDCAuthorizationRequestContext.class);
        if (authZContext == null) {
            log.warn("No authorization request could be located in the profile request context");
            return Events.Failure.event(this);
        }

        final AuthorizationRequest authorizationRequest = authZContext.getAuthorizationRequest();
        if (authorizationRequest == null || Strings.isNullOrEmpty(authorizationRequest.getClientId())) {
            log.warn("Authorization request could not be loaded from session");
            return Events.Failure.event(this);
        }

        ac.setForceAuthn(authZContext.isForceAuthentication());
        if (ac.isForceAuthn()) {
            log.debug("Authentication context requires force authN for {}",
                    authorizationRequest.getClientId());
        } else {
            log.debug("Authentication context does not require force authN for {}",
                    authorizationRequest.getClientId());
        }

        final List<Principal> principals = new ArrayList<>();
        processRequestedAcrValuesIfAny(authorizationRequest, principals);
        processAcrValuesBasedOnPrincipalWeightMap(principals);
        addRequestedPrincipalIntoContext(ac, principals);
        
        profileRequestContext.addSubcontext(ac, true);
        profileRequestContext.setBrowserProfile(true);
        return Events.Success.event(this);
    }

    /**
     * Add requested principal into context.
     *
     * @param ac         the ac
     * @param principals the principals
     */
    private void addRequestedPrincipalIntoContext(final AuthenticationContext ac, final List<Principal> principals) {
        final RequestedPrincipalContext rpc = new RequestedPrincipalContext();
        rpc.setOperator("exact");
        rpc.setRequestedPrincipals(principals);
        ac.addSubcontext(rpc, true);
    }

    /**
     * Process acr values based on principal weight map.
     *
     * @param principals the principals
     */
    private void processAcrValuesBasedOnPrincipalWeightMap(final List<Principal> principals) {
        if (principals.isEmpty()) {
            final AuthnContextClassRefPrincipal[] principalArray =
                    this.authenticationPrincipalWeightMap.keySet()
                            .toArray(new AuthnContextClassRefPrincipal[]{});
            Arrays.sort(principalArray, new WeightedComparator());
            principals.add(principalArray[principalArray.length - 1]);
        }
    }

    /**
     * Process requested acr values if any.
     *
     * @param authorizationRequest the authorization request
     * @param principals           the principals
     */
    private void processRequestedAcrValuesIfAny(final AuthorizationRequest authorizationRequest, 
                                                final List<Principal> principals) {
        if (authorizationRequest.getExtensions().containsKey(OIDCConstants.ACR_VALUES)) {
            final String[] acrValues = authorizationRequest.getExtensions()
                    .get(OIDCConstants.ACR_VALUES).toString().split(" ");
            for (final String acrValue : acrValues) {
                final AuthnContextClassRefPrincipal requestedPrincipal =
                        new AuthnContextClassRefPrincipal(acrValue.trim());
                for (final AuthenticationFlowDescriptor flow : this.availableAuthenticationFlows) {
                    if (!principals.contains(requestedPrincipal)
                            && flow.getSupportedPrincipals().contains(requestedPrincipal)) {
                        principals.add(requestedPrincipal);
                    }
                }
            }

        }
    }


    /**
     * A {@link Comparator} that compares the mapped weights of the two operands, using a weight of zero
     * for any unmapped values.
     */
    private class WeightedComparator implements Comparator {

        @Override
        public int compare(final Object o1, final Object o2) {
            final int weight1 =
                    authenticationPrincipalWeightMap.containsKey(o1) ? authenticationPrincipalWeightMap.get(o1) : 0;
            final int weight2 =
                    authenticationPrincipalWeightMap.containsKey(o2) ? authenticationPrincipalWeightMap.get(o2) : 0;
            if (weight1 < weight2) {
                return -1;
            } else if (weight1 > weight2) {
                return 1;
            }

            return 0;
        }

    }
}