package net.unicon.cas.mfa.web.flow;

import net.unicon.cas.mfa.authentication.AuthenticationSupport;
import net.unicon.cas.mfa.authentication.MultiFactorAuthenticationTransactionContext;
import net.unicon.cas.mfa.authentication.RequestedAuthenticationMethodRankingStrategy;
import net.unicon.cas.mfa.util.MultiFactorUtils;
import net.unicon.cas.mfa.web.flow.event.MultiFactorAuthenticationSpringWebflowEventBuilder;
import net.unicon.cas.mfa.web.flow.util.MultiFactorRequestContextUtils;
import net.unicon.cas.mfa.web.support.MultiFactorAuthenticationSupportingWebApplicationService;
import org.apache.commons.lang3.StringUtils;
import org.jasig.cas.CasProtocolConstants;
import org.jasig.cas.authentication.Authentication;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.webflow.action.AbstractAction;
import org.springframework.webflow.execution.Event;
import org.springframework.webflow.execution.RequestContext;

import java.util.Set;

/**
 * Determines whether the login flow needs to branch *now* to honor the authentication method requirements of
 *
 *
 * If the Service expresses a requirement for how the user must authenticate,
 * and there's an existing single sign-on session, and there is not a record in the user's
 * single sign-on session of having already fulfilled that requirement, then fires the `requireMfa` event indicating
 * that exceptional handling is required.  Otherwise (i.e., if no exceptional authentication method is required,
 * or there is no existing single sign-on session,
 * or the required exceptional authentication method is already fulfilled) then fire the `requireTgt` event indicating
 * that the login flow should proceed as per normal.
 *
 * More explicitly:
 *
 *
 * <ol>
 * <li>If an authentication context does not exist
 * (i.e., the user does not have an existing single sign-on session with a record of a prior authentication),
 * continue the login flow as usual by firing the `requireTgt` event.</li>
 * <li>If an authentication context exists without any authentication method decoration, fire the
 * `requireMfa` event indicating that an exceptional flow is required to fulfill the service's authentication
 * requirements</li>
 * <li>If an authentication context exists with an authentication method decoration indicating an authentication
 * method other than that required by the service, fire the `requireMfa` event indicating that an exceptional flow
 * is required to fulfill the service's authentication requirements.</li>
 * <li>Otherwise, fire the `requireTgt` event to continue the login flow as per usual.</li>
 * </ol>
 *
 * This means that in the case where there is not an existing single sign-on session, this Action will continue
 * the login flow as per normal <strong>even though additional authentication will be required
 * later in the flow to fulfill the authentication requirements of the CAS-using service</strong>.
 *
 * @author Misagh Moayyed
 */
public final class ValidateInitialMultiFactorAuthenticationRequestAction extends AbstractAction {

    private final Logger logger = LoggerFactory.getLogger(ValidateInitialMultiFactorAuthenticationRequestAction.class);

    /**
     * The Constant EVENT_ID_REQUIRE_TGT.
     */
    public static final String EVENT_ID_REQUIRE_TGT = "requireTgt";

    /**
     * The authentication support.
     */
    private final AuthenticationSupport authenticationSupport;

    /**
     * Authentication method ranking strategy.
     */
    private final RequestedAuthenticationMethodRankingStrategy authnMethodRankingStrategy;

    /**
     * Instantiates a new validate initial multifactor authentication request action.
     *
     * @param authSupport the authN support
     * @param authenticationMethodRankingStrategy authenticationMethodRankingStrategy
     */
    public ValidateInitialMultiFactorAuthenticationRequestAction(final AuthenticationSupport authSupport,
                           final RequestedAuthenticationMethodRankingStrategy authenticationMethodRankingStrategy) {
        this.authenticationSupport = authSupport;
        this.authnMethodRankingStrategy = authenticationMethodRankingStrategy;
    }

    /* (non-Javadoc)
     * @see org.springframework.webflow.action.AbstractAction#doExecute(org.springframework.webflow.execution.RequestContext)
     */
    @Override
    protected Event doExecute(final RequestContext context) throws Exception {
        final MultiFactorAuthenticationTransactionContext mfaTx = MultiFactorRequestContextUtils.getMfaTransaction(context);
        if (mfaTx == null) {
            return new Event(this, EVENT_ID_REQUIRE_TGT);
        }
        final MultiFactorAuthenticationSupportingWebApplicationService mfaService =
                this.authnMethodRankingStrategy.computeHighestRankingAuthenticationMethod(mfaTx);

        final String requestedAuthenticationMethod = mfaService != null ? mfaService.getAuthenticationMethod() : null;
        final String tgt = MultiFactorRequestContextUtils.getTicketGrantingTicketId(context);

        /*
         * If the TGT is blank i.e. there is no existing SSO session, proceed with normal login flow
         * (Note that flow may need interrupted later if the CAS-using service requires an authentication method
         *  not fulfilled by the normal login flow)
         */
        if (StringUtils.isBlank(tgt)) {
            logger.trace("TGT is blank; proceed flow normally.");
            return new Event(this, EVENT_ID_REQUIRE_TGT);
        }

        /*
         * If the authentication method the CAS-using service has specified is blank,
         * proceed with the normal login flow.
         */
        if (StringUtils.isBlank(requestedAuthenticationMethod)) {
            logger.trace("Since required authentication method is blank, proceed flow normally.");
            return new Event(this, EVENT_ID_REQUIRE_TGT);
        }

        logger.trace("Service [{}] requires authentication method [{}]", mfaTx.getTargetServiceId(), requestedAuthenticationMethod);
        final Authentication authentication = this.authenticationSupport.getAuthenticationFrom(tgt);

        /*
         * If somehow the TGT were to have no authentication, then interpret as an existing SSO session insufficient
         * to fulfill the requirements of this service, and branch to fulfill the authentication requirement.
         */
        if (authentication == null) {
            logger.warn("TGT had no Authentication, which is odd. "
                    + "Proceeding as if additional authentication required.");

            //Place the ranked mfa service into the flow scope to be available in the actual mfa subflows
            MultiFactorRequestContextUtils.setMultifactorWebApplicationService(context, mfaService);
            return new Event(this, getMultiFactorEventIdByAuthenticationMethod(requestedAuthenticationMethod));
        }

        final Set<String> previouslyAchievedAuthenticationMethods =
                MultiFactorUtils.getSatisfiedAuthenticationMethods(authentication);

        /*
         * If any of the recorded authentication methods from the prior Authentication are 'stronger'
         * than the authentication method requested to access the CAS-using service, proceed with the normal authentication flow.
         */
        if (this.authnMethodRankingStrategy
                .anyPreviouslyAchievedAuthenticationMethodsStrongerThanRequestedOne(previouslyAchievedAuthenticationMethods,
                        requestedAuthenticationMethod)) {
            logger.trace("Authentication method [{}] is EQUAL -- OR -- WEAKER than any previously fulfilled methods [{}]; "
                    + "proceeding with flow normally...", requestedAuthenticationMethod, previouslyAchievedAuthenticationMethods);
            return new Event(this, EVENT_ID_REQUIRE_TGT);
        }

        if (context.getRequestParameters().get(CasProtocolConstants.PARAMETER_RENEW) != null) {
            return new Event(this, EVENT_ID_REQUIRE_TGT);
        }

        logger.trace("Authentication method [{}] is STRONGER than any previously fulfilled methods [{}]; "
                + "branching to prompt for required authentication method.",
                requestedAuthenticationMethod, previouslyAchievedAuthenticationMethods);

        //Place the ranked mfa service into the flow scope to be available in the actual mfa subflows
        MultiFactorRequestContextUtils.setMultifactorWebApplicationService(context, mfaService);
        return new Event(this, getMultiFactorEventIdByAuthenticationMethod(requestedAuthenticationMethod));
    }

    /**
     * Construct the next MFA event id based on the given authentication method.
     *
     * @param authnMethod the authentication method provided
     *
     * @return the next event in the flow, that is effectively the value of
     * {@link MultiFactorAuthenticationSpringWebflowEventBuilder#MFA_EVENT_ID_PREFIX}
     * prepended to the authentication method.
     */
    private static String getMultiFactorEventIdByAuthenticationMethod(final String authnMethod) {
        return MultiFactorAuthenticationSpringWebflowEventBuilder.MFA_EVENT_ID_PREFIX + authnMethod;
    }
}