package org.apereo.cas.support.pac4j.web.flow;

import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;
import org.apereo.cas.CasProtocolConstants;
import org.apereo.cas.CentralAuthenticationService;
import org.apereo.cas.authentication.AuthenticationResult;
import org.apereo.cas.authentication.AuthenticationSystemSupport;
import org.apereo.cas.authentication.principal.ClientCredential;
import org.apereo.cas.authentication.principal.Service;
import org.apereo.cas.authentication.principal.WebApplicationService;
import org.apereo.cas.ticket.TicketGrantingTicket;
import org.apereo.cas.web.support.WebUtils;
import org.pac4j.core.client.BaseClient;
import org.pac4j.core.client.Clients;
import org.pac4j.core.client.IndirectClient;
import org.pac4j.core.context.WebContext;
import org.pac4j.core.credentials.Credentials;
import org.pac4j.core.exception.HttpAction;
import org.pac4j.core.profile.CommonProfile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.webflow.action.AbstractAction;
import org.springframework.webflow.context.ExternalContext;
import org.springframework.webflow.execution.Event;
import org.springframework.webflow.execution.RequestContext;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.Serializable;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;

/**
 * This class represents an action to put at the beginning of the webflow.
 * <p>
 * Before any authentication, redirection urls are computed for the different clients defined as well as the theme,
 * locale, method and service are saved into the web session.</p>
 * After authentication, appropriate information are expected on this callback url to finish the authentication
 * process with the provider.
 *
 * @author Jerome Leleu
 * @since 3.5.0
 */
public class DelegatedClientAuthenticationAction extends AbstractAction {
    /**
     * Stop the webflow for pac4j and route to view.
     */
    public static final String STOP_WEBFLOW = "stopWebflow";

    /**
     * Stop the webflow.
     */
    public static final String STOP = "stop";

    /**
     * Client action state id in the webflow.
     */
    public static final String CLIENT_ACTION = "clientAction";

    /**
     * All the urls and names of the pac4j clients.
     */
    public static final String PAC4J_URLS = "pac4jUrls";

    /**
     * View id that stops the webflow.
     */
    public static final String VIEW_ID_STOP_WEBFLOW = "casPac4jStopWebflow";

    private static final Logger LOGGER = LoggerFactory.getLogger(DelegatedClientAuthenticationAction.class);

    private static final Pattern PAC4J_CLIENT_SUFFIX_PATTERN = Pattern.compile("Client\\d*");

    private final Clients clients;
    private final AuthenticationSystemSupport authenticationSystemSupport;
    private final CentralAuthenticationService centralAuthenticationService;
    private final String themeParamName;
    private final String localParamName;
    private final boolean autoRedirect;

    public DelegatedClientAuthenticationAction(final Clients clients, final AuthenticationSystemSupport authenticationSystemSupport,
                                               final CentralAuthenticationService centralAuthenticationService, final String themeParamName,
                                               final String localParamName, final boolean autoRedirect) {
        this.clients = clients;
        this.authenticationSystemSupport = authenticationSystemSupport;
        this.centralAuthenticationService = centralAuthenticationService;
        this.themeParamName = themeParamName;
        this.localParamName = localParamName;
        this.autoRedirect = autoRedirect;
    }

    @Override
    protected Event doExecute(final RequestContext context) throws Exception {
        final HttpServletRequest request = WebUtils.getHttpServletRequest(context);
        final HttpServletResponse response = WebUtils.getHttpServletResponse(context);
        final HttpSession session = request.getSession();

        // web context
        final WebContext webContext = WebUtils.getPac4jJ2EContext(request, response);

        // get client
        final String clientName = request.getParameter(this.clients.getClientNameParameter());
        LOGGER.debug("clientName: [{}]", clientName);

        if (hasDelegationRequestFailed(request, response.getStatus()).isPresent()) {
            return stopWebflow();
        }
        // it's an authentication
        if (StringUtils.isNotBlank(clientName)) {
            // get client
            final BaseClient<Credentials, CommonProfile> client = (BaseClient<Credentials, CommonProfile>) this.clients.findClient(clientName);
            LOGGER.debug("Client: [{}]", client);

            // get credentials
            final Credentials credentials;
            try {
                credentials = client.getCredentials(webContext);
                LOGGER.debug("Retrieved credentials: [{}]", credentials);
            } catch (final Exception e) {
                LOGGER.debug("The request requires http action", e);
                return stopWebflow();
            }

            // retrieve parameters from web session
            final Service service = (Service) session.getAttribute(CasProtocolConstants.PARAMETER_SERVICE);
            context.getFlowScope().put(CasProtocolConstants.PARAMETER_SERVICE, service);
            LOGGER.debug("Retrieve service: [{}]", service);
            if (service != null) {
                request.setAttribute(CasProtocolConstants.PARAMETER_SERVICE, service.getId());
            }

            restoreRequestAttribute(request, session, this.themeParamName);
            restoreRequestAttribute(request, session, this.localParamName);
            restoreRequestAttribute(request, session, CasProtocolConstants.PARAMETER_METHOD);

            // credentials not null -> try to authenticate
            if (credentials != null) {
                final AuthenticationResult authenticationResult =
                        this.authenticationSystemSupport.handleAndFinalizeSingleAuthenticationTransaction(service, new ClientCredential(credentials));

                final TicketGrantingTicket tgt = this.centralAuthenticationService.createTicketGrantingTicket(authenticationResult);
                WebUtils.putTicketGrantingTicketInScopes(context, tgt);
                return success();
            }
        }

        // no or aborted authentication : go to login page
        prepareForLoginPage(context);
        if (response.getStatus() == HttpStatus.UNAUTHORIZED.value()) {
            return stopWebflow();
        }

        if (this.autoRedirect) {
            final Set<ProviderLoginPageConfiguration> urls = context.getFlowScope().get(PAC4J_URLS, Set.class);
            if (urls != null && urls.size() == 1) {
                final ProviderLoginPageConfiguration cfg = urls.stream().findFirst().get();
                LOGGER.debug("Auto-redirecting to client url [{}]", cfg.getRedirectUrl());
                response.sendRedirect(cfg.getRedirectUrl());
                final ExternalContext externalContext = context.getExternalContext();
                externalContext.recordResponseComplete();
                return stopWebflow();
            }
        }
        return error();
    }

    /**
     * Prepare the data for the login page.
     *
     * @param context The current webflow context
     * @throws HttpAction the http action
     */
    protected void prepareForLoginPage(final RequestContext context) throws HttpAction {
        final HttpServletRequest request = WebUtils.getHttpServletRequest(context);
        final HttpServletResponse response = WebUtils.getHttpServletResponse(context);
        final HttpSession session = request.getSession();

        // web context
        final WebContext webContext = WebUtils.getPac4jJ2EContext(request, response);

        // save parameters in web session
        final WebApplicationService service = WebUtils.getService(context);
        LOGGER.debug("save service: [{}]", service);
        session.setAttribute(CasProtocolConstants.PARAMETER_SERVICE, service);
        saveRequestParameter(request, session, this.themeParamName);
        saveRequestParameter(request, session, this.localParamName);
        saveRequestParameter(request, session, CasProtocolConstants.PARAMETER_METHOD);

        final Set<ProviderLoginPageConfiguration> urls = new LinkedHashSet<>();

        this.clients.findAllClients().stream().filter(c -> c instanceof IndirectClient).forEach(client -> {
            try {
                final IndirectClient indirectClient = (IndirectClient) client;

                final String name = client.getName();
                final String type = PAC4J_CLIENT_SUFFIX_PATTERN.matcher(client.getClass().getSimpleName()).replaceAll(StringUtils.EMPTY).toLowerCase();
                final String redirectionUrl = indirectClient.getRedirectAction(webContext).getLocation();
                LOGGER.debug("[{}] -> [{}]", name, redirectionUrl);
                urls.add(new ProviderLoginPageConfiguration(name, redirectionUrl, type));
            } catch (final HttpAction e) {
                if (e.getCode() == HttpStatus.UNAUTHORIZED.value()) {
                    LOGGER.debug("Authentication request was denied from the provider [{}]", client.getName());
                } else {
                    LOGGER.warn(e.getMessage(), e);
                }
            } catch (final Exception e) {
                LOGGER.error("Cannot process client [{}]", client, e);
            }
        });
        if (!urls.isEmpty()) {
            context.getFlowScope().put(PAC4J_URLS, urls);
        } else if (response.getStatus() != HttpStatus.UNAUTHORIZED.value()) {
            LOGGER.warn("No clients could be determined based on the provided configuration");
        }
    }

    /**
     * Restore an attribute in web session as an attribute in request.
     *
     * @param request The HTTP request
     * @param session The HTTP session
     * @param name    The name of the parameter
     */
    private static void restoreRequestAttribute(final HttpServletRequest request, final HttpSession session, final String name) {
        final String value = (String) session.getAttribute(name);
        request.setAttribute(name, value);
    }

    /**
     * Save a request parameter in the web session.
     *
     * @param request The HTTP request
     * @param session The HTTP session
     * @param name    The name of the parameter
     */
    private static void saveRequestParameter(final HttpServletRequest request, final HttpSession session, final String name) {
        final String value = request.getParameter(name);
        if (value != null) {
            session.setAttribute(name, value);
        }
    }

    private Event stopWebflow() {
        return new Event(this, STOP);
    }

    /**
     * Determine if request has errors.
     *
     * @param request the request
     * @param status  the status
     * @return the optional model and view, if request is an error.
     */
    public static Optional<ModelAndView> hasDelegationRequestFailed(final HttpServletRequest request, final int status) {
        final Map<String, String[]> params = request.getParameterMap();
        if (params.containsKey("error") || params.containsKey("error_code") || params.containsKey("error_description")
                || params.containsKey("error_message")) {
            final Map<String, Object> model = new HashMap<>();
            if (params.containsKey("error_code")) {
                model.put("code", StringEscapeUtils.escapeHtml4(request.getParameter("error_code")));
            } else {
                model.put("code", status);
            }
            model.put("error", StringEscapeUtils.escapeHtml4(request.getParameter("error")));
            model.put("reason", StringEscapeUtils.escapeHtml4(request.getParameter("error_reason")));

            if (params.containsKey("error_description")) {
                model.put("description", StringEscapeUtils.escapeHtml4(request.getParameter("error_description")));
            } else if (params.containsKey("error_message")) {
                model.put("description", StringEscapeUtils.escapeHtml4(request.getParameter("error_message")));
            }
            model.put(CasProtocolConstants.PARAMETER_SERVICE, request.getAttribute(CasProtocolConstants.PARAMETER_SERVICE));
            model.put("client", StringEscapeUtils.escapeHtml4(request.getParameter("client_name")));

            LOGGER.debug("Delegation request has failed. Details are [{}]", model);
            return Optional.of(new ModelAndView("casPac4jStopWebflow", model));
        }
        return Optional.empty();
    }

    /**
     * The Provider login page configuration.
     */
    public static class ProviderLoginPageConfiguration implements Serializable {
        private static final long serialVersionUID = 6216882278086699364L;
        private final String name;
        private final String redirectUrl;
        private final String type;

        /**
         * Instantiates a new Provider ui configuration.
         *
         * @param name        the name
         * @param redirectUrl the redirect url
         * @param type        the type
         */
        ProviderLoginPageConfiguration(final String name, final String redirectUrl, final String type) {
            this.name = name;
            this.redirectUrl = redirectUrl;
            this.type = type;
        }

        public String getName() {
            return name;
        }

        public String getRedirectUrl() {
            return redirectUrl;
        }

        public String getType() {
            return type;
        }
    }
}
