package org.apereo.cas.web.flow.resolver.impl;

import org.apache.commons.lang3.tuple.Pair;
import org.apereo.cas.CentralAuthenticationService;
import org.apereo.cas.authentication.Authentication;
import org.apereo.cas.authentication.AuthenticationServiceSelectionPlan;
import org.apereo.cas.authentication.AuthenticationSystemSupport;
import org.apereo.cas.services.MultifactorAuthenticationProvider;
import org.apereo.cas.services.MultifactorAuthenticationProviderSelector;
import org.apereo.cas.services.RegisteredService;
import org.apereo.cas.services.ServicesManager;
import org.apereo.cas.ticket.registry.TicketRegistrySupport;
import org.apereo.cas.web.flow.authentication.BaseMultifactorAuthenticationProviderEventResolver;
import org.apereo.cas.web.support.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.util.CookieGenerator;
import org.springframework.webflow.execution.Event;
import org.springframework.webflow.execution.RequestContext;

import javax.servlet.http.HttpServletRequest;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Set;

/**
 * This is {@link SelectiveAuthenticationProviderWebflowEventEventResolver}
 * that acts as a stub resolver, specifically designed for extensions.
 * Deployers can extend this class to perform additional processes on the final set
 * of resolved events, to select one vs another based on the nature of the event attributes.
 *
 * @author Misagh Moayyed
 * @since 5.0.0
 */
public class SelectiveAuthenticationProviderWebflowEventEventResolver extends BaseMultifactorAuthenticationProviderEventResolver {
    private static final Logger LOGGER = LoggerFactory.getLogger(SelectiveAuthenticationProviderWebflowEventEventResolver.class);
    
    public SelectiveAuthenticationProviderWebflowEventEventResolver(final AuthenticationSystemSupport authenticationSystemSupport,
                                                                    final CentralAuthenticationService centralAuthenticationService,
                                                                    final ServicesManager servicesManager,
                                                                    final TicketRegistrySupport ticketRegistrySupport,
                                                                    final CookieGenerator warnCookieGenerator,
                                                                    final AuthenticationServiceSelectionPlan authenticationSelectionStrategies,
                                                                    final MultifactorAuthenticationProviderSelector selector) {
        super(authenticationSystemSupport, centralAuthenticationService, servicesManager, ticketRegistrySupport, warnCookieGenerator,
                authenticationSelectionStrategies, selector);
    }

    @Override
    public Set<Event> resolveInternal(final RequestContext context) {
        final Set<Event> resolvedEvents = getResolvedEventsAsAttribute(context);
        final Authentication authentication = WebUtils.getAuthentication(context);
        final RegisteredService registeredService = resolveRegisteredServiceInRequestContext(context);
        final HttpServletRequest request = WebUtils.getHttpServletRequest(context);
        return resolveEventsInternal(resolvedEvents, authentication, registeredService, request, context);
    }

    /**
     * Resolve events internal set. Implementation may filter events from the collection
     * to only return the one that is appropriate for this request. The default
     * implementation returns the entire collection.
     *
     * @param resolveEvents     the resolve events
     * @param authentication    the authentication
     * @param registeredService the registered service
     * @param request           the request
     * @param context           the request context
     * @return the set of resolved events
     */
    protected Set<Event> resolveEventsInternal(final Set<Event> resolveEvents, final Authentication authentication, final RegisteredService registeredService,
                                               final HttpServletRequest request, final RequestContext context) {
        LOGGER.debug("Collection of resolved events for this authentication sequence are:");
        resolveEvents.forEach(e -> LOGGER.debug("Event id [{}] resolved from [{}]", e.getId(), e.getSource().getClass().getName()));
        final Pair<Set<Event>, Collection<MultifactorAuthenticationProvider>> pair =
                filterEventsByMultifactorAuthenticationProvider(resolveEvents, authentication, registeredService);
        WebUtils.putResolvedMultifactorAuthenticationProviders(context, pair.getValue());
        return pair.getKey();
    }

    /**
     * Filter events by multifactor authentication providers.
     *
     * @param resolveEvents     the resolve events
     * @param authentication    the authentication
     * @param registeredService the registered service
     * @return the set of events
     */
    protected Pair<Set<Event>, Collection<MultifactorAuthenticationProvider>> filterEventsByMultifactorAuthenticationProvider(
            final Set<Event> resolveEvents, final Authentication authentication, final RegisteredService registeredService) {
        LOGGER.debug("Locating multifactor providers to determine support for this authentication sequence");
        final Map<String, MultifactorAuthenticationProvider> providers =
                WebUtils.getAvailableMultifactorAuthenticationProviders(applicationContext);

        if (providers == null || providers.isEmpty()) {
            LOGGER.debug("No providers are available to honor this request. Moving on...");
            return Pair.of(resolveEvents, Collections.emptySet());
        }

        final Collection<MultifactorAuthenticationProvider> flattenedProviders = flattenProviders(providers.values());

        // remove providers that don't support the event
        flattenedProviders.removeIf(p -> resolveEvents.stream().filter(e -> p.supports(e, authentication, registeredService)).count() == 0);

        // remove events that are not supported by providers.
        resolveEvents.removeIf(e -> flattenedProviders.stream().filter(p -> p.supports(e, authentication, registeredService)).count() == 0);

        LOGGER.debug("Finalized set of resolved events are [{}]", resolveEvents);
        return Pair.of(resolveEvents, flattenedProviders);
    }
}
