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

import com.github.ulisesbocchio.spring.boot.security.saml.configurer.ServiceProviderBuilder;
import com.github.ulisesbocchio.spring.boot.security.saml.configurer.ServiceProviderEndpoints;
import com.github.ulisesbocchio.spring.boot.security.saml.properties.MetadataGeneratorProperties;
import com.github.ulisesbocchio.spring.boot.security.saml.properties.SAMLSSOProperties;
import org.opensaml.saml2.metadata.EntityDescriptor;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.saml.metadata.ExtendedMetadata;
import org.springframework.security.saml.metadata.MetadataDisplayFilter;
import org.springframework.security.saml.metadata.MetadataGenerator;
import org.springframework.security.saml.metadata.MetadataGeneratorFilter;

import java.util.Arrays;
import java.util.Collection;
import java.util.Optional;

/**
 * <p>
 * Builder configurer that takes care of configuring/customizing the {@link MetadataGenerator},
 * {@link MetadataDisplayFilter}, and {@link MetadataGeneratorFilter} bean.
 * </p>
 * <p>
 * This configurer always instantiates its own {@link MetadataGenerator},
 * {@link MetadataDisplayFilter}, and {@link MetadataGeneratorFilter}  based on the specified configuration.
 * </p>
 * <p>
 * This configurer also reads the values from {@link SAMLSSOProperties#getMetadataGenerator()} 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.metadata-generator.metadata-url
 *     saml.sso.metadata-generator.entity-id
 *     saml.sso.metadata-generator.want-assertion-signed
 *     saml.sso.metadata-generator.request-signed
 *     saml.sso.metadata-generator.name-id
 *     saml.sso.metadata-generator.entity-base-url
 *     saml.sso.metadata-generator.bindings-sso
 *     saml.sso.metadata-generator.bindings-hok-sso
 *     saml.sso.metadata-generator.bindings-slo
 *     saml.sso.metadata-generator.assertion-consumer-index
 *     saml.sso.metadata-generator.include-discovery-extension
 * </pre>
 * </p>
 *
 * @author Ulises Bocchio
 */
public class MetadataGeneratorConfigurer extends SecurityConfigurerAdapter<Void, ServiceProviderBuilder> {

    private String metadataURL;
    private String entityId;
    private String id;
    private Boolean wantAssertionSigned;
    private Boolean requestSigned;
    private Collection<String> nameId;
    private String entityBaseURL;
    private Collection<String> bindingsHoKSSO;
    private Collection<String> bindingsSLO;
    private Collection<String> bindingsSSO;
    private Integer assertionConsumerIndex;
    private Boolean includeDiscoveryExtension;
    private MetadataGeneratorProperties config;
    private ServiceProviderEndpoints endpoints;
    private ExtendedMetadata extendedMetadata;
    private MetadataGenerator metadataGenerator;
    private MetadataGenerator metadataGeneratorBean;

    public MetadataGeneratorConfigurer() {
    }

    public MetadataGeneratorConfigurer(MetadataGenerator metadataGenerator) {
        this.metadataGenerator = metadataGenerator;
    }

    @Override
    public void init(ServiceProviderBuilder builder) throws Exception {
        config = builder.getSharedObject(SAMLSSOProperties.class).getMetadataGenerator();
        endpoints = builder.getSharedObject(ServiceProviderEndpoints.class);
        metadataGeneratorBean = builder.getSharedObject(MetadataGenerator.class);
    }

    @Override
    public void configure(ServiceProviderBuilder builder) throws Exception {
        extendedMetadata = builder.getSharedObject(ExtendedMetadata.class);
        MetadataDisplayFilter metadataDisplayFilter = new MetadataDisplayFilter();
        metadataURL = Optional.ofNullable(metadataURL).orElseGet(config::getMetadataUrl);
        endpoints.setMetadataURL(metadataURL);
        metadataDisplayFilter.setFilterProcessesUrl(metadataURL);

        MetadataGenerator actualMetadataGenerator = metadataGeneratorBean;
        if(actualMetadataGenerator == null) {
            if(this.metadataGenerator != null) {
                actualMetadataGenerator = this.metadataGenerator;
            } else {
                actualMetadataGenerator = new MetadataGenerator();
            }
            actualMetadataGenerator.setEntityId(Optional.ofNullable(entityId).orElseGet(config::getEntityId));
            actualMetadataGenerator.setId(Optional.ofNullable(id).orElseGet(config::getId));
            actualMetadataGenerator.setExtendedMetadata(extendedMetadata);
            actualMetadataGenerator.setWantAssertionSigned(Optional.ofNullable(wantAssertionSigned).orElseGet(config::isWantAssertionSigned));
            actualMetadataGenerator.setRequestSigned(Optional.ofNullable(requestSigned).orElseGet(config::isRequestSigned));
            actualMetadataGenerator.setNameID(Optional.ofNullable(nameId).orElseGet(config::getNameId));
            actualMetadataGenerator.setEntityBaseURL(Optional.ofNullable(entityBaseURL).orElseGet(config::getEntityBaseUrl));
            actualMetadataGenerator.setBindingsHoKSSO(Optional.ofNullable(bindingsHoKSSO).orElseGet(config::getBindingsHokSso));
            actualMetadataGenerator.setBindingsSLO(Optional.ofNullable(bindingsSLO).orElseGet(config::getBindingsSlo));
            actualMetadataGenerator.setBindingsSSO(Optional.ofNullable(bindingsSSO).orElseGet(config::getBindingsSso));
            actualMetadataGenerator.setAssertionConsumerIndex(Optional.ofNullable(assertionConsumerIndex).orElseGet(config::getAssertionConsumerIndex));
            actualMetadataGenerator.setIncludeDiscoveryExtension(Optional.ofNullable(includeDiscoveryExtension).orElseGet(config::isIncludeDiscoveryExtension));
        }

        MetadataGeneratorFilter metadataGeneratorFilter = new MetadataGeneratorFilter(actualMetadataGenerator);

        builder.setSharedObject(MetadataGenerator.class, actualMetadataGenerator);
        builder.setSharedObject(MetadataDisplayFilter.class, metadataDisplayFilter);
        builder.setSharedObject(MetadataGeneratorFilter.class, metadataGeneratorFilter);
    }

    /**
     * {@link MetadataDisplayFilter} processing URL. Defines which URL will display the Service Provider Metadata.
     * Default is {@code "/saml/metadata"}.
     * <p>
     * Alternatively use property:
     * <pre>
     *      saml.sso.metadata-generator.metadata-url
     * </pre>
     * </p>
     *
     * @param metadataURL the metadata display filter processing URL.
     * @return this configurer for further customization
     */
    public MetadataGeneratorConfigurer metadataURL(String metadataURL) {
        this.metadataURL = metadataURL;
        return this;
    }

    /**
     * This Service Provider's SAML Entity ID. Used as entity id for generated requests from this Service Provider.
     * Default is {@code "localhost"}.
     * <p>
     * Alternatively use property:
     * <pre>
     *      saml.sso.metadata-generator.entity-id
     * </pre>
     * </p>
     *
     * @param entityId the entity id of this Service Provider.
     * @return this configurer for further customization
     */
    public MetadataGeneratorConfigurer entityId(String entityId) {
        this.entityId = entityId;
        return this;
    }

    /**
     * This Service Provider's SAML ID. Used as ID of {@link EntityDescriptor} managed by {@link MetadataGenerator}.
     * Default is {@code null}.
     * <p>
     * Alternatively use property:
     * <pre>
     *      saml.sso.metadata-generator.id
     * </pre>
     * </p>
     *
     * @param id the id.
     * @return this configurer for further customization
     */
    public MetadataGeneratorConfigurer id(String id) {
        this.id = id;
        return this;
    }

    /**
     * Whether incoming SAML assertions should be signed or not.
     * Default is {@code true}.
     * <p>
     * Alternatively use property:
     * <pre>
     *      saml.sso.metadata-generator.want-assertion-signed
     * </pre>
     * </p>
     *
     * @param wantAssertionSigned true if assertions are wanted signed.
     * @return this configurer for further customization
     */
    public MetadataGeneratorConfigurer wantAssertionSigned(Boolean wantAssertionSigned) {
        this.wantAssertionSigned = wantAssertionSigned;
        return this;
    }

    /**
     * Whether Authentication Requests should be signed by this Service Provider or not.
     * <p>
     * Alternatively use property:
     * <pre>
     *      saml.sso.metadata-generator.request-signed
     * </pre>
     * </p>
     *
     * @param requestSigned true if authentication requests should be signed.
     * @return this configurer for further customization
     */
    public MetadataGeneratorConfigurer requestSigned(Boolean requestSigned) {
        this.requestSigned = requestSigned;
        return this;
    }

    /**
     * NameIDs to be included in generated metadata.
     * <p>
     * Alternatively use property:
     * <pre>
     *      saml.sso.metadata-generator.name-id
     * </pre>
     * </p>
     *
     * @param nameId the name IDs to be included in generated metadata.
     * @return this configurer for further customization
     */
    public MetadataGeneratorConfigurer nameId(String... nameId) {
        this.nameId = Arrays.asList(nameId);
        return this;
    }

    /**
     * This Service Provider's entity base URL. Provide if base URL cannot be inferred by using the hostname where
     * the Service Provider will be running. I.E. if running on the cloud behind a load balancer.
     * <p>
     * Alternatively use property:
     * <pre>
     *      saml.sso.metadata-generator.entity-base-url
     * </pre>
     * </p>
     *
     * @param entityBaseURL the Service Provider base URL.
     * @return this configurer for further customization
     */
    public MetadataGeneratorConfigurer entityBaseURL(String entityBaseURL) {
        this.entityBaseURL = entityBaseURL;
        return this;
    }

    /**
     * List of bindings to be included in the generated metadata for Web Single Sign-On Holder of Key. Ordering of
     * bindings affects inclusion in the generated metadata. Supported values are: "artifact" (or
     * "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact") and "post" (or "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST").
     * By default there are no included bindings for the profile.
     * <p>
     * Alternatively use property:
     * <pre>
     *      saml.sso.metadata-generator.bindings-hok-sso
     * </pre>
     * </p>
     *
     * @param bindingsHoKSSO bindings for web single sign-on holder-of-key
     * @return this configurer for further customization
     */
    public MetadataGeneratorConfigurer bindingsHoKSSO(String... bindingsHoKSSO) {
        this.bindingsHoKSSO = Arrays.asList(bindingsHoKSSO);
        return this;
    }

    /**
     * List of bindings to be included in the generated metadata for Single Logout. Ordering of bindings affects
     * inclusion in the generated metadata. Supported values are: "post" (or "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST")
     * and "redirect" (or "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"). The following bindings are included by
     * default: "post", "redirect".
     * <p>
     * Alternatively use property:
     * <pre>
     *      saml.sso.metadata-generator.bindings-slo
     * </pre>
     * </p>
     *
     * @param bindingsSLO bindings for single logout
     * @return this configurer for further customization
     */
    public MetadataGeneratorConfigurer bindingsSLO(String... bindingsSLO) {
        this.bindingsSLO = Arrays.asList(bindingsSLO);
        return this;
    }

    /**
     * List of bindings to be included in the generated metadata for Web Single Sign-On. Ordering of bindings affects
     * inclusion in the generated metadata. Supported values are: "artifact" (or "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact"),
     * "post" (or "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST") and "paos" (or "urn:oasis:names:tc:SAML:2.0:bindings:PAOS").
     * The following bindings are included by default: "artifact", "post".
     * <p>
     * Alternatively use property:
     * <pre>
     *      saml.sso.metadata-generator.bindings-sso
     * </pre>
     * </p>
     *
     * @param bindingsSSO bindings for web single sign-on
     * @return this configurer for further customization
     */
    public MetadataGeneratorConfigurer bindingsSSO(String... bindingsSSO) {
        this.bindingsSSO = Arrays.asList(bindingsSSO);
        return this;
    }

    /**
     * Generated assertion consumer service with the index equaling set value will be marked as default. Use negative
     * value to skip the default attribute altogether.
     * Default is {@code 0}.
     * <p>
     * Alternatively use property:
     * <pre>
     *      saml.sso.metadata-generator.assertion-consumer-index
     * </pre>
     * </p>
     *
     * @param assertionConsumerIndex assertion consumer index of service to mark as default
     * @return this configurer for further customization
     */
    public MetadataGeneratorConfigurer assertionConsumerIndex(Integer assertionConsumerIndex) {
        this.assertionConsumerIndex = assertionConsumerIndex;
        return this;
    }

    /**
     * When true discovery profile extension metadata pointing to the default SAMLEntryPoint will be generated and
     * stored in the generated metadata document.
     * <p>
     * Alternatively use property:
     * <pre>
     *      saml.sso.metadata-generator.include-discovery-extension
     * </pre>
     * </p>
     *
     * @param includeDiscoveryExtension flag indicating whether IDP discovery should be enabled
     * @return this configurer for further customization
     */
    public MetadataGeneratorConfigurer includeDiscoveryExtension(Boolean includeDiscoveryExtension) {
        this.includeDiscoveryExtension = includeDiscoveryExtension;
        return this;
    }
}