package com.github.ulisesbocchio.spring.boot.security.saml.configurer.builder;

import com.github.ulisesbocchio.spring.boot.security.saml.configurer.ServiceProviderEndpoints;
import com.github.ulisesbocchio.spring.boot.security.saml.configurer.ServiceProviderBuilder;
import com.github.ulisesbocchio.spring.boot.security.saml.properties.SAMLSSOProperties;
import com.github.ulisesbocchio.spring.boot.security.saml.properties.WebSSOProfileOptionProperties;
import org.assertj.core.util.VisibleForTesting;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.saml.SAMLDiscovery;
import org.springframework.security.saml.SAMLEntryPoint;
import org.springframework.security.saml.SAMLProcessingFilter;
import org.springframework.security.saml.SAMLWebSSOHoKProcessingFilter;
import org.springframework.security.saml.websso.WebSSOProfileOptions;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;

import java.util.Optional;

/**
 * <p>
 * Builder configurer that takes care of configuring/customizing the {@link SAMLProcessingFilter},
 * {@link SAMLWebSSOHoKProcessingFilter}, {@link SAMLDiscovery}, and {@link SAMLEntryPoint} bean.
 * </p>
 * <p>
 * This configurer always instantiates its own {@link SAMLProcessingFilter},
 * {@link SAMLWebSSOHoKProcessingFilter}, {@link SAMLDiscovery}, and {@link SAMLEntryPoint} based on the specified
 * configuration.
 * </p>
 * <p>
 * This configurer also reads the values from {@link SAMLSSOProperties} for some DSL methods if they are not used.
 * In other words, the user is able to configure the filters through the following properties:
 * <pre>
 *     saml.sso.default-success-url
 *     saml.sso.default-failure-url
 *     saml.sso.sso-processing-url
 *     saml.sso.enable-sso-hok
 *     saml.sso.discovery-processing-url
 *     saml.sso.idp-selection-page-url
 *     saml.sso.sso-login-url
 *     saml.sso.profile-options.binding
 *     saml.sso.profile-options.allowed-idps
 *     saml.sso.profile-options.provider-name
 *     saml.sso.profile-options.assertion-consumer-index
 *     saml.sso.profile-options.name-id
 *     saml.sso.profile-options.allow-create
 *     saml.sso.profile-options.passive
 *     saml.sso.profile-options.force-authn
 *     saml.sso.profile-options.include-scoping
 *     saml.sso.profile-options.proxy-count
 *     saml.sso.profile-options.relay-state
 *     saml.sso.profile-options.authn-contexts
 *     saml.sso.profile-options.authn-context-comparison
 *
 * </pre>
 * </p>
 *
 * @author Ulises Bocchio
 */
public class SSOConfigurer extends SecurityConfigurerAdapter<Void, ServiceProviderBuilder> {

    private String defaultSuccessURL;
    private AuthenticationSuccessHandler successHandler;
    private String defaultFailureURL;
    private AuthenticationFailureHandler failureHandler;
    private String ssoProcessingURL;
    private Boolean enableSsoHoK;
    private String discoveryProcessingURL;
    private String idpSelectionPageURL;
    private String ssoLoginURL;
    private WebSSOProfileOptions profileOptions;
    private AuthenticationManager authenticationManager;
    private SAMLSSOProperties config;
    private ServiceProviderEndpoints endpoints;
    private String ssoHoKProcessingURL;
    private SAMLEntryPoint samlEntryPointBean;
    private ApplicationEventPublisher eventPublisher;
    private SessionAuthenticationStrategy sessionAuthenticationStrategy;

    @Override
    public void init(ServiceProviderBuilder builder) throws Exception {
        authenticationManager = builder.getSharedObject(AuthenticationManager.class);
        config = builder.getSharedObject(SAMLSSOProperties.class);
        endpoints = builder.getSharedObject(ServiceProviderEndpoints.class);
        
        if ( config.isEnableEventPublisher() )
            eventPublisher = builder.getSharedObject(ApplicationEventPublisher.class);
        else eventPublisher = null;
    }

    @Override
    public void configure(ServiceProviderBuilder builder) throws Exception {
        if (successHandler == null) {
            SavedRequestAwareAuthenticationSuccessHandler successRedirectHandler = createDefaultSuccessHandler();
            successRedirectHandler.setDefaultTargetUrl(Optional.ofNullable(defaultSuccessURL).orElseGet(config::getDefaultSuccessUrl));
            successHandler = postProcess(successRedirectHandler);
        }

        defaultFailureURL = Optional.ofNullable(defaultFailureURL).orElseGet(config::getDefaultFailureUrl);
        if (failureHandler == null) {
            SimpleUrlAuthenticationFailureHandler authenticationFailureHandler = createDefaultFailureHandler();
            authenticationFailureHandler.setDefaultFailureUrl(defaultFailureURL);
            failureHandler = postProcess(authenticationFailureHandler);
        }
        endpoints.setDefaultFailureURL(defaultFailureURL);


        SAMLProcessingFilter ssoFilter = createDefaultSamlProcessingFilter();
        ssoFilter.setAuthenticationManager(authenticationManager);
        ssoFilter.setAuthenticationSuccessHandler(successHandler);
        ssoFilter.setAuthenticationFailureHandler(failureHandler);
        ssoProcessingURL = Optional.ofNullable(ssoProcessingURL).orElseGet(config::getSsoProcessingUrl);
        endpoints.setSsoProcessingURL(ssoProcessingURL);
        ssoFilter.setFilterProcessesUrl(ssoProcessingURL);
        if (sessionAuthenticationStrategy != null) {
            ssoFilter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy);
        }

        SAMLWebSSOHoKProcessingFilter ssoHoKFilter = null;
        if (Optional.ofNullable(enableSsoHoK).orElseGet(config::isEnableSsoHok)) {
            ssoHoKFilter = createDefaultSamlHoKProcessingFilter();
            ssoHoKFilter.setAuthenticationSuccessHandler(successHandler);
            ssoHoKFilter.setAuthenticationManager(authenticationManager);
            ssoHoKFilter.setAuthenticationFailureHandler(failureHandler);
            ssoHoKProcessingURL = Optional.ofNullable(ssoHoKProcessingURL).orElseGet(config::getSsoHokProcessingUrl);
            endpoints.setSsoHoKProcessingURL(ssoHoKProcessingURL);
            ssoHoKFilter.setFilterProcessesUrl(ssoHoKProcessingURL);
            if (sessionAuthenticationStrategy != null) {
                ssoHoKFilter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy);
            }
        }

        SAMLDiscovery discoveryFilter = createDefaultSamlDiscoveryFilter();
        discoveryProcessingURL = Optional.ofNullable(discoveryProcessingURL).orElseGet(config::getDiscoveryProcessingUrl);
        endpoints.setDiscoveryProcessingURL(discoveryProcessingURL);
        discoveryFilter.setFilterProcessesUrl(discoveryProcessingURL);
        idpSelectionPageURL = Optional.ofNullable(idpSelectionPageURL).orElseGet(config::getIdpSelectionPageUrl);
        endpoints.setIdpSelectionPageURL(idpSelectionPageURL);
        discoveryFilter.setIdpSelectionPath(idpSelectionPageURL);

        SAMLEntryPoint entryPoint = Optional.ofNullable(samlEntryPointBean).orElseGet(this::createDefaultSamlEntryPoint);
        entryPoint.setDefaultProfileOptions(Optional.ofNullable(profileOptions).orElseGet(this::getProfileOptions));
        ssoLoginURL = Optional.ofNullable(ssoLoginURL).orElseGet(config::getSsoLoginUrl);
        endpoints.setSsoLoginURL(ssoLoginURL);
        entryPoint.setFilterProcessesUrl(ssoLoginURL);

        builder.setSharedObject(SAMLProcessingFilter.class, ssoFilter);
        builder.setSharedObject(SAMLWebSSOHoKProcessingFilter.class, ssoHoKFilter);
        builder.setSharedObject(SAMLDiscovery.class, discoveryFilter);
        builder.setSharedObject(SAMLEntryPoint.class, entryPoint);
    }

    private WebSSOProfileOptions getProfileOptions() {
        WebSSOProfileOptionProperties properties = config.getProfileOptions();
        WebSSOProfileOptions options = new WebSSOProfileOptions();
        options.setAllowCreate(properties.getAllowCreate());
        options.setAllowedIDPs(properties.getAllowedIdps());
        options.setAssertionConsumerIndex(properties.getAssertionConsumerIndex());
        options.setAuthnContextComparison(properties.getAuthnContextComparison().getType());
        options.setAuthnContexts(properties.getAuthnContexts());
        options.setBinding(properties.getBinding());
        options.setForceAuthN(properties.getForceAuthn());
        options.setIncludeScoping(properties.getIncludeScoping());
        options.setNameID(properties.getNameId());
        options.setPassive(properties.getPassive());
        options.setProviderName(properties.getProviderName());
        options.setProxyCount(properties.getProxyCount());
        options.setRelayState(properties.getRelayState());
        return options;
    }

    @VisibleForTesting
    protected SAMLWebSSOHoKProcessingFilter createDefaultSamlHoKProcessingFilter() {
        SAMLWebSSOHoKProcessingFilter filter = new SAMLWebSSOHoKProcessingFilter();
        filter.setApplicationEventPublisher(eventPublisher);
        return filter;
    }

    @VisibleForTesting
    protected SAMLEntryPoint createDefaultSamlEntryPoint() {
        return new SAMLEntryPoint();
    }

    @VisibleForTesting
    protected SAMLDiscovery createDefaultSamlDiscoveryFilter() {
        return new SAMLDiscovery();
    }

    @VisibleForTesting
    protected SAMLProcessingFilter createDefaultSamlProcessingFilter() {
        SAMLProcessingFilter filter = new SAMLProcessingFilter();
        filter.setApplicationEventPublisher(eventPublisher);
        return filter;
    }

    @VisibleForTesting
    protected SimpleUrlAuthenticationFailureHandler createDefaultFailureHandler() {
        return new SimpleUrlAuthenticationFailureHandler();
    }

    @VisibleForTesting
    protected SavedRequestAwareAuthenticationSuccessHandler createDefaultSuccessHandler() {
        return new SavedRequestAwareAuthenticationSuccessHandler();
    }

    /**
     * Provide a specific {@link SAMLEntryPoint}.
     *
     * @param samlEntryPoint the actual entry point.
     * @return this configurer for further customization
     */
    public SSOConfigurer samlEntryPoint(SAMLEntryPoint samlEntryPoint) {
        this.samlEntryPointBean = samlEntryPoint;
        return this;
    }

    /**
     * Supplies the default target Url that will be used if no saved request is found in the session, or the
     * alwaysUseDefaultTargetUrl property is set to true. If not set, defaults to /. It will be treated as relative to
     * the web-app's context path, and should include the leading /. Alternatively, inclusion of a scheme name (such as
     * "http://" or "https://") as the prefix will denote a fully-qualified URL and this is also supported.
     * Not Relevant if {@link #successHandler(AuthenticationSuccessHandler)} is used.
     * <p>
     * Alternatively use property:
     * <pre>
     *      saml.sso.default-success-url
     * </pre>
     * </p>
     *
     * @param defaultSuccessURL the default target URL
     * @return this configurer for further customization
     */
    public SSOConfigurer defaultSuccessURL(String defaultSuccessURL) {
        this.defaultSuccessURL = defaultSuccessURL;
        return this;
    }

    /**
     * Provide a specific {@link AuthenticationSuccessHandler} to be invoked on successful authentication. Overrides
     * value set by {@link #defaultSuccessURL(String)}.
     *
     * @param successHandler the actual success handler.
     * @return this configurer for further customization
     */
    public SSOConfigurer successHandler(AuthenticationSuccessHandler successHandler) {
        this.successHandler = successHandler;
        return this;
    }

    /**
     * The URL which will be used as the failure destination. Not relevant if using {@link
     * #failureHandler(AuthenticationFailureHandler)}.
     * Default is {@code "/error"}.
     * <p>
     * Alternatively use property:
     * <pre>
     *      saml.sso.default-failure-url
     * </pre>
     * </p>
     *
     * @param defaultFailureURL the failure URL, for example "/loginFailed.jsp".
     * @return this configurer for further customization
     */
    public SSOConfigurer defaultFailureURL(String defaultFailureURL) {
        this.defaultFailureURL = defaultFailureURL;
        return this;
    }

    /**
     * Provide a specific {@link AuthenticationFailureHandler} to be invoked on unsuccessful authentication. Overrides
     * value set by {@link #defaultFailureURL(String)}.
     *
     * @param failureHandler the actual failure handler.
     * @return this configurer for further customization
     */
    public SSOConfigurer failureHandler(AuthenticationFailureHandler failureHandler) {
        this.failureHandler = failureHandler;
        return this;
    }

    /**
     * The URL that the {@link SAMLProcessingFilter} will be listening to.
     * Default is {@code "/saml/SSO"}.
     * <p>
     * Alternatively use property:
     * <pre>
     *      saml.sso.sso-processing-url
     * </pre>
     * </p>
     *
     * @param ssoProcessingURL the URL that the {@link SAMLProcessingFilter} will be listening to.
     * @return this configurer for further customization
     */
    public SSOConfigurer ssoProcessingURL(String ssoProcessingURL) {
        this.ssoProcessingURL = ssoProcessingURL;
        return this;
    }

    /**
     * The URL that the {@link SAMLWebSSOHoKProcessingFilter} will be listening to.
     * Default is {@code "/saml/HoKSSO"}.
     * <p>
     * Alternatively use property:
     * <pre>
     *      saml.sso.sso-hok-Processing-url
     * </pre>
     * </p>
     *
     * @param ssoHoKProcessingURL the URL that the {@link SAMLWebSSOHoKProcessingFilter} will be listening to.
     * @return this configurer for further customization
     */
    public SSOConfigurer ssoHoKProcessingURL(String ssoHoKProcessingURL) {
        this.ssoHoKProcessingURL = ssoHoKProcessingURL;
        return this;
    }

    /**
     * Whether to enable the {@link SAMLWebSSOHoKProcessingFilter} filter or not.
     * Default is {@code true}.
     * <p>
     * Alternatively use property:
     * <pre>
     *      saml.sso.enable-sso-hok
     * </pre>
     * </p>
     *
     * @param enableSsoHoK true if HoK Filter is enabled.
     * @return this configurer for further customization
     */
    public SSOConfigurer enableSsoHoK(boolean enableSsoHoK) {
        this.enableSsoHoK = enableSsoHoK;
        return this;
    }

    /**
     * The URL that the {@link SAMLDiscovery} filter will be listening to.
     * Default is {@code "/saml/discovery"}.
     * <p>
     * Alternatively use property:
     * <pre>
     *      saml.sso.discovery-processing-url
     * </pre>
     * </p>
     *
     * @param discoveryProcessingURL the URL that the {@link SAMLDiscovery} filter will be listening to.
     * @return this configurer for further customization
     */
    public SSOConfigurer discoveryProcessingURL(String discoveryProcessingURL) {
        this.discoveryProcessingURL = discoveryProcessingURL;
        return this;
    }

    /**
     * Sets path where request dispatcher will send user for IDP selection. In case it is null the default IDP will
     * always be used.
     * Default is {@code "/idpselection"}.
     * <p>
     * Alternatively use property:
     * <pre>
     *      saml.sso.idp-selection-page-url
     * </pre>
     * </p>
     *
     * @param idpSelectionPageURL selection path.
     * @return this configurer for further customization
     */
    public SSOConfigurer idpSelectionPageURL(String idpSelectionPageURL) {
        this.idpSelectionPageURL = idpSelectionPageURL;
        return this;
    }

    /**
     * The URL that the {@link SAMLEntryPoint} filter will be listening to.
     * Default is {@code "/saml/login"}.
     * <p>
     * Alternatively use property:
     * <pre>
     *      saml.sso.sso-login-url
     * </pre>
     * </p>
     *
     * @param ssoLoginURL the URL that the {@link SAMLEntryPoint} filter will be listening to.
     * @return this configurer for further customization
     */
    public SSOConfigurer ssoLoginURL(String ssoLoginURL) {
        this.ssoLoginURL = ssoLoginURL;
        return this;
    }

    /**
     * Provide a specific {@link WebSSOProfileOptions} options.
     * <p>
     * Alternatively use properties:
     * <pre>
     *      saml.sso.profile-options.binding
     *      saml.sso.profile-options.allowed-idps
     *      saml.sso.profile-options.provider-name
     *      saml.sso.profile-options.assertion-consumer-index
     *      saml.sso.profile-options.name-id
     *      saml.sso.profile-options.allow-create
     *      saml.sso.profile-options.passive
     *      saml.sso.profile-options.force-authn
     *      saml.sso.profile-options.include-scoping
     *      saml.sso.profile-options.proxy-count
     *      saml.sso.profile-options.relay-state
     *      saml.sso.profile-options.authn-contexts
     *      saml.sso.profile-options.authn-context-comparison
     * </pre>
     * </p>
     *
     * @param profileOptions the SSO Profile Options.
     * @return this configurer for further customization
     */
    public SSOConfigurer profileOptions(WebSSOProfileOptions profileOptions) {
        this.profileOptions = profileOptions;
        return this;
    }

    /**
     * Set the {@link SessionAuthenticationStrategy} for the {@link SAMLProcessingFilter}
     *
     * @param sessionAuthenticationStrategy to set
     * @return this configurer for further customization
     */
    public SSOConfigurer sessionAuthenticationStrategy(SessionAuthenticationStrategy sessionAuthenticationStrategy) {
        this.sessionAuthenticationStrategy = sessionAuthenticationStrategy;
        return this;
    }

}