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.properties.SAMLSSOProperties;
import com.github.ulisesbocchio.spring.boot.security.saml.properties.TLSProperties;
import org.assertj.core.util.VisibleForTesting;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.saml.key.KeyManager;
import org.springframework.security.saml.trust.httpclient.TLSProtocolConfigurer;

import java.util.Arrays;
import java.util.Optional;
import java.util.Set;

import static java.util.stream.Collectors.toSet;

/**
 * <p>
 * Builder configurer that takes care of configuring/customizing the {@link TLSProtocolConfigurer} bean.
 * </p>
 * <p>
 * This configurer always instantiates its own {@link TLSProtocolConfigurer}.
 * </p>
 * <p>
 * This configurer also reads the values from {@link SAMLSSOProperties#getTls()} for some the DSL methods if they are
 * not used. In other words, the user is able to configure the TLSProtocolConfigurer through the following properties:
 * <pre>
 *     saml.sso.tls.protocol-name
 *     saml.sso.tls.protocol-port
 *     saml.sso.tls.key-manager
 *     saml.sso.tls.ssl-hostname-verification
 *     saml.sso.tls.trusted-keys
 * </pre>
 * </p>
 *
 * @author Ulises Bocchio
 */
public class TLSConfigurer extends SecurityConfigurerAdapter<Void, ServiceProviderBuilder> {

    private String protocolName;
    private Integer protocolPort;
    private String sslHostnameVerification;
    private Set<String> trustedKeys;
    private TLSProperties config;

    @Override
    public void init(ServiceProviderBuilder builder) throws Exception {
        config = builder.getSharedObject(SAMLSSOProperties.class).getTls();
    }

    @Override
    public void configure(ServiceProviderBuilder builder) throws Exception {
        KeyManager keyManager = builder.getSharedObject(KeyManager.class);
        TLSProtocolConfigurer configurer = createDefaultTlsProtocolConfigurer();
        configurer.setProtocolName(Optional.ofNullable(protocolName).orElseGet(config::getProtocolName));
        configurer.setProtocolPort(Optional.ofNullable(protocolPort).orElseGet(config::getProtocolPort));
        configurer.setSslHostnameVerification(Optional.ofNullable(sslHostnameVerification).orElseGet(config::getSslHostnameVerification));
        configurer.setTrustedKeys(Optional.ofNullable(trustedKeys).orElseGet(config::getTrustedKeys));
        configurer.setKeyManager(keyManager);
        configurer.afterPropertiesSet();
        builder.setSharedObject(TLSProtocolConfigurer.class, configurer);
    }

    @VisibleForTesting
    protected TLSProtocolConfigurer createDefaultTlsProtocolConfigurer() {
        return new TLSProtocolConfigurer();
    }

    /**
     * Name of protocol (ID) to register to HTTP Client, https by default.
     * Default is {@code "https"}.
     * <p>
     * Alternatively use property:
     * <pre>
     *      saml.sso.tls.protocol-name
     * </pre>
     * </p>
     *
     * @param protocolName the protocol
     * @return this configurer for further customization
     */
    public TLSConfigurer protocolName(String protocolName) {
        this.protocolName = protocolName;
        return this;
    }

    /**
     * Default port for protocol.
     * Default is {@code 443}.
     * <p>
     * Alternatively use property:
     * <pre>
     *      saml.sso.tls.protocol-port
     * </pre>
     * </p>
     *
     * @param protocolPort the protocol port
     * @return this configurer for further customization
     */
    public TLSConfigurer protocolPort(int protocolPort) {
        this.protocolPort = protocolPort;
        return this;
    }

    /**
     * Hostname verifier to use for verification of SSL connections. Default value is "default", other supported
     * options
     * are "defaultAndLocalhost", "strict" and "allowAll".
     * Default is {@code "default"}.
     * <p>
     * Alternatively use property:
     * <pre>
     *      saml.sso.tls.ssl-hostname-verification
     * </pre>
     * </p>
     *
     * @param sslHostnameVerification hostname verification mode.
     * @return this configurer for further customization
     */
    public TLSConfigurer sslHostnameVerification(String sslHostnameVerification) {
        this.sslHostnameVerification = sslHostnameVerification;
        return this;
    }

    /**
     * When not set all certificates included in the keystore will be used as trusted certificate authorities. When
     * specified,
     * only keys with the defined aliases will be used for trust evaluation.
     * <p>
     * Alternatively use property:
     * <pre>
     *      saml.sso.tls.trusted-keys
     * </pre>
     * </p>
     *
     * @param trustedKeys trusted keys.
     * @return this configurer for further customization
     */
    public TLSConfigurer trustedKeys(String... trustedKeys) {
        this.trustedKeys = Arrays.stream(trustedKeys).collect(toSet());
        return this;
    }
}