/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.camel.spring.boot.security;

import java.util.Collections;
import java.util.Map;
import org.apache.camel.CamelContext;
import org.apache.camel.spring.boot.CamelAutoConfiguration;
import org.apache.camel.support.jsse.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.type.AnnotatedTypeMetadata;

@Configuration(proxyBeanMethods = false)
@AutoConfigureAfter(CamelAutoConfiguration.class)
@EnableConfigurationProperties(CamelSSLConfigurationProperties.class)
public class CamelSSLAutoConfiguration {

    private static final Logger LOG = LoggerFactory.getLogger(CamelSSLAutoConfiguration.class);

    @Bean
    @ConditionalOnMissingBean
    @Conditional(CamelSSLAutoConfiguration.SSLCondition.class)
    public SSLContextParameters sslContextParameters(CamelContext camelContext,
            CamelSSLConfigurationProperties properties) {
        // use any camel.ssl.config props
        SSLContextParameters sslContext = camelContext.getSSLContextParameters() != null
                ? copyParams(camelContext.getSSLContextParameters()) : new SSLContextParameters();

        // override with any camel.ssl props
        SSLContextParameters config = new SSLContextBuilder(sslContext).certAlias(properties.getCertAlias())
                .cipherSuites(properties.getCipherSuites()).cipherSuitesFilter(properties.getCipherSuitesFilter())
                .clientParameters(properties.getClientParameters()).keyManagers(properties.getKeyManagers())
                .provider(properties.getProvider()).secureRandom(properties.getSecureRandom())
                .secureSocketProtocol(properties.getSecureSocketProtocol())
                .secureSocketProtocols(properties.getSecureSocketProtocols())
                .secureSocketProtocolsFilter(properties.getSecureSocketProtocolsFilter())
                .serverParameters(properties.getServerParameters()).sessionTimeout(properties.getSessionTimeout())
                .trustAllCertificates(properties.isTrustAllCertificates())
                .trustManager(properties.getTrustManagers()).build();

        return config;
    }

    @Bean
    @ConditionalOnMissingBean
    @Conditional(CamelSSLAutoConfiguration.SSLConfigCondition.class)
    public GlobalSSLContextParametersSupplier sslContextParametersSupplier(CamelSSLConfigurationProperties properties) {
        final SSLContextParameters config = properties.getConfig() != null ? properties.getConfig()
                : new SSLContextParameters();
        return () -> config;
    }

    public static class SSLCondition extends SpringBootCondition {
        @Override
        public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata annotatedTypeMetadata) {
            Binder binder = Binder.get(context.getEnvironment());
            Map<String, Object> sslProperties = binder.bind("camel.ssl", Bindable.mapOf(String.class, Object.class))
                    .orElse(Collections.emptyMap());
            sslProperties.remove("config");
            ConditionMessage.Builder message = ConditionMessage.forCondition("camel.ssl");
            if (!sslProperties.isEmpty()) {
                return ConditionOutcome.match(message.because("enabled"));
            }

            return ConditionOutcome.noMatch(message.because("not enabled"));
        }
    }

    public static class SSLConfigCondition extends SpringBootCondition {
        @Override
        public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata annotatedTypeMetadata) {
            Binder binder = Binder.get(context.getEnvironment());
            Map<String, Object> sslProperties = binder
                    .bind("camel.ssl.config", Bindable.mapOf(String.class, Object.class))
                    .orElse(Collections.emptyMap());
            ConditionMessage.Builder message = ConditionMessage.forCondition("camel.ssl.config");
            if (!sslProperties.isEmpty()) {
                return ConditionOutcome.match(message.because("enabled"));
            }

            return ConditionOutcome.noMatch(message.because("not enabled"));
        }
    }

    private SSLContextParameters copyParams(SSLContextParameters sslContextParameters) {
        SSLContextParameters copy = new SSLContextParameters();
        copy.setCertAlias(sslContextParameters.getCertAlias());
        copy.setCipherSuites(sslContextParameters.getCipherSuites());
        copy.setCipherSuitesFilter(sslContextParameters.getCipherSuitesFilter());
        copy.setClientParameters(sslContextParameters.getClientParameters());
        copy.setKeyManagers(sslContextParameters.getKeyManagers());
        copy.setProvider(sslContextParameters.getProvider());
        copy.setSecureRandom(sslContextParameters.getSecureRandom());
        copy.setSecureSocketProtocol(sslContextParameters.getSecureSocketProtocol());
        copy.setSecureSocketProtocols(sslContextParameters.getSecureSocketProtocols());
        copy.setSecureSocketProtocolsFilter(sslContextParameters.getSecureSocketProtocolsFilter());
        copy.setServerParameters(sslContextParameters.getServerParameters());
        copy.setSessionTimeout(sslContextParameters.getSessionTimeout());
        copy.setTrustManagers(sslContextParameters.getTrustManagers());
        return copy;
    }

    private class SSLContextBuilder {

        private SSLContextParameters sslContextParameters;

        public SSLContextBuilder(SSLContextParameters sslContextParameters) {
            this.sslContextParameters = sslContextParameters;
        }

        public SSLContextBuilder certAlias(String certAlias) {
            if (certAlias != null) {
                sslContextParameters.setCertAlias(certAlias);
            }
            return this;
        }

        public SSLContextBuilder cipherSuites(CipherSuitesParameters cipherSuites) {
            if (cipherSuites != null) {
                sslContextParameters.setCipherSuites(cipherSuites);
            }
            return this;
        }

        public SSLContextBuilder cipherSuitesFilter(FilterParameters cipherSuitesFilter) {
            if (cipherSuitesFilter != null) {
                sslContextParameters.setCipherSuitesFilter(cipherSuitesFilter);
            }
            return this;
        }

        public SSLContextBuilder clientParameters(SSLContextClientParameters clientParameters) {
            if (clientParameters != null) {
                sslContextParameters.setClientParameters(clientParameters);
            }
            return this;
        }

        public SSLContextBuilder keyManagers(KeyManagersParameters keyManagers) {
            if (keyManagers != null) {
                sslContextParameters.setKeyManagers(keyManagers);
            }
            return this;
        }

        public SSLContextBuilder provider(String provider) {
            if (provider != null) {
                sslContextParameters.setProvider(provider);
            }
            return this;
        }

        public SSLContextBuilder secureRandom(SecureRandomParameters secureRandom) {
            if (secureRandom != null) {
                sslContextParameters.setSecureRandom(secureRandom);
            }
            return this;
        }

        public SSLContextBuilder secureSocketProtocol(String secureSocketProtocol) {
            if (secureSocketProtocol != null) {
                sslContextParameters.setSecureSocketProtocol(secureSocketProtocol);
            }
            return this;
        }

        public SSLContextBuilder secureSocketProtocols(SecureSocketProtocolsParameters secureSocketProtocols) {
            if (secureSocketProtocols != null) {
                sslContextParameters.setSecureSocketProtocols(secureSocketProtocols);
            }
            return this;
        }

        public SSLContextBuilder secureSocketProtocolsFilter(FilterParameters secureSocketProtocolsFilter) {
            if (secureSocketProtocolsFilter != null) {
                sslContextParameters.setSecureSocketProtocolsFilter(secureSocketProtocolsFilter);
            }
            return this;
        }

        public SSLContextBuilder serverParameters(SSLContextServerParameters serverParameters) {
            if (serverParameters != null) {
                sslContextParameters.setServerParameters(serverParameters);
            }
            return this;
        }

        public SSLContextBuilder sessionTimeout(String sessionTimeout) {
            if (sessionTimeout != null) {
                sslContextParameters.setSessionTimeout(sessionTimeout);
            }
            return this;
        }

        public SSLContextBuilder trustManager(TrustManagersParameters trustManager) {
            if (trustManager != null) {
                sslContextParameters.setTrustManagers(trustManager);
            }
            return this;
        }

        public SSLContextBuilder trustAllCertificates(boolean trustAllCertificates) {
            if (trustAllCertificates) {
                TrustManagersParameters tmp = sslContextParameters.getTrustManagers();
                if (tmp == null) {
                    tmp = new TrustManagersParameters();
                    sslContextParameters.setTrustManagers(tmp);
                }
                tmp.setTrustManager(TrustAllTrustManager.INSTANCE);
                LOG.warn(
                        "Trust all certificates enabled. Using this in production can expose the application to man-in-the-middle attacks");
            }
            return this;
        }

        public SSLContextParameters build() {
            return this.sslContextParameters;
        }
    }

}
