package org.apereo.cas.mfa.accepto.web.flow;

import org.apereo.cas.authentication.AuthenticationException;
import org.apereo.cas.configuration.CasConfigurationProperties;
import org.apereo.cas.mfa.accepto.AccepttoApiUtils;
import org.apereo.cas.util.LoggingUtils;
import org.apereo.cas.web.support.WebUtils;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.utils.URIBuilder;
import org.pac4j.core.context.JEEContext;
import org.pac4j.core.context.session.SessionStore;
import org.springframework.webflow.action.AbstractAction;
import org.springframework.webflow.action.EventFactorySupport;
import org.springframework.webflow.execution.Event;
import org.springframework.webflow.execution.RequestContext;

import javax.servlet.http.HttpServletRequest;
import java.security.PublicKey;

/**
 * This is {@link AccepttoMultifactorFetchChannelAction}.
 *
 * @author Misagh Moayyed
 * @since 6.1.0
 */
@Slf4j
@RequiredArgsConstructor
public class AccepttoMultifactorFetchChannelAction extends AbstractAction {
    private final CasConfigurationProperties casProperties;
    private final SessionStore<JEEContext> sessionStore;
    private final PublicKey apiPublicKey;

    @Override
    public Event doExecute(final RequestContext requestContext) throws Exception {
        val request = WebUtils.getHttpServletRequestFromExternalWebflowContext(requestContext);
        val response = WebUtils.getHttpServletResponseFromExternalWebflowContext(requestContext);
        val webContext = new JEEContext(request, response, this.sessionStore);

        val channel = authenticateAndFetchChannel(requestContext);
        LOGGER.debug("Storing channel [{}] in session", channel);
        AccepttoWebflowUtils.storeChannelInSessionStore(channel, webContext);

        val authentication = WebUtils.getInProgressAuthentication();
        AccepttoWebflowUtils.storeAuthenticationInSessionStore(authentication, webContext);

        val accepttoRedirectUrl = buildAccepttoAuthenticationSelectionUrl(request, channel);
        LOGGER.debug("Redirecting to [{}]", accepttoRedirectUrl);
        requestContext.getRequestScope().put("accepttoRedirectUrl", accepttoRedirectUrl);
        return new EventFactorySupport().success(this);
    }

    /**
     * Build acceptto authentication selection url string.
     *
     * @param request the request
     * @param channel the channel
     * @return the string
     * @throws Exception the exception
     */
    protected String buildAccepttoAuthenticationSelectionUrl(final HttpServletRequest request, final String channel) throws Exception {
        val acceptto = casProperties.getAuthn().getMfa().getAcceptto();
        val callbackUrl = WebUtils.getHttpRequestFullUrl(request);
        return new URIBuilder(acceptto.getAuthnSelectionUrl() + "/mfa/index")
            .addParameter("channel", channel)
            .addParameter("callback_url", callbackUrl)
            .build()
            .toString();
    }

    /**
     * Authenticate and fetch channel.
     *
     * @param requestContext the request
     * @return the channel
     */
    protected String authenticateAndFetchChannel(final RequestContext requestContext) {
        try {
            val existingChannel = AccepttoWebflowUtils.getChannel(requestContext);
            if (existingChannel.isPresent()) {
                val channel = existingChannel.get();
                LOGGER.debug("Using existing channel retrieved as [{}}", channel);
                return channel;
            }

            val authentication = WebUtils.getInProgressAuthentication();
            val acceptto = casProperties.getAuthn().getMfa().getAcceptto();
            val results = AccepttoApiUtils.authenticate(authentication, acceptto, requestContext, this.apiPublicKey);
            LOGGER.debug("Received API results as [{}]", results);

            if (results.containsKey("channel")) {
                val channel = results.get("channel").toString();
                val channelStatus = (String) results.get("status");
                if (StringUtils.isNotBlank(channelStatus) && ("denied".equalsIgnoreCase(channelStatus) || "rejected".equalsIgnoreCase(channelStatus))) {
                    throw new AuthenticationException("Authentication attempt has been denied");
                }
                return channel;
            }
        } catch (final Exception e) {
            LoggingUtils.error(LOGGER, e);
        }
        throw new AuthenticationException("Unable to fetch channel for user");
    }
}
