/*
 * 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.deltaspike.core.impl.config;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.spi.CreationalContext;
import jakarta.enterprise.event.Observes;
import jakarta.enterprise.inject.Any;
import jakarta.enterprise.inject.Default;
import jakarta.enterprise.inject.Produces;
import jakarta.enterprise.inject.Typed;
import jakarta.enterprise.inject.Vetoed;
import jakarta.enterprise.inject.spi.AfterBeanDiscovery;
import jakarta.enterprise.inject.spi.AfterDeploymentValidation;
import jakarta.enterprise.inject.spi.Bean;
import jakarta.enterprise.inject.spi.BeanManager;
import jakarta.enterprise.inject.spi.BeforeBeanDiscovery;
import jakarta.enterprise.inject.spi.BeforeShutdown;
import jakarta.enterprise.inject.spi.Extension;
import jakarta.enterprise.inject.spi.InjectionPoint;
import jakarta.enterprise.inject.spi.ProcessAnnotatedType;
import jakarta.enterprise.inject.spi.ProcessBean;
import jakarta.enterprise.inject.spi.ProcessProducerMethod;
import javax.management.InstanceAlreadyExistsException;
import javax.management.InstanceNotFoundException;
import javax.management.MBeanServer;
import javax.management.ObjectName;

import java.lang.annotation.Annotation;
import java.lang.management.ManagementFactory;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.deltaspike.core.api.config.ConfigProperty;
import org.apache.deltaspike.core.api.config.ConfigResolver;
import org.apache.deltaspike.core.api.config.Configuration;
import org.apache.deltaspike.core.api.config.Filter;
import org.apache.deltaspike.core.api.config.PropertyFileConfig;
import org.apache.deltaspike.core.api.config.Source;
import org.apache.deltaspike.core.api.exclude.Exclude;
import org.apache.deltaspike.core.api.provider.BeanProvider;
import org.apache.deltaspike.core.spi.activation.Deactivatable;
import org.apache.deltaspike.core.spi.config.BaseConfigPropertyProducer;
import org.apache.deltaspike.core.spi.config.ConfigFilter;
import org.apache.deltaspike.core.spi.config.ConfigSource;
import org.apache.deltaspike.core.spi.config.ConfigValidator;
import org.apache.deltaspike.core.util.ClassDeactivationUtils;
import org.apache.deltaspike.core.util.ClassUtils;
import org.apache.deltaspike.core.util.ServiceUtils;
import org.apache.deltaspike.core.util.StringUtils;

/**
 * This extension handles {@link org.apache.deltaspike.core.api.config.PropertyFileConfig}s
 * provided by users.
 */
public class ConfigurationExtension implements Extension, Deactivatable
{
    private static final Logger LOG = Logger.getLogger(ConfigurationExtension.class.getName());

    private static final String CANNOT_CREATE_CONFIG_SOURCE_FOR_CUSTOM_PROPERTY_FILE_CONFIG =
        "Cannot create ConfigSource for custom property-file config ";

    /**
     * This is a trick for EAR scenarios in some containers.
     * They e.g. boot up the shared EAR lib with the ear ClassLoader.
     * Thus any {@link org.apache.deltaspike.core.api.config.PropertyFileConfig} configuration will just get
     * activated for this very single EAR ClassLoader but <em>not</em> for all the webapps.
     * But if I have a property file in a jar in the shared EAR lib then I most likely also like to get it
     * if I call this from my webapp (TCCL).
     * So we also automatically register all the PropertyFileConfigs we found in the 'parent BeanManager'
     * as well.
     */
    private static Map<ClassLoader, List<Class<? extends PropertyFileConfig>>> detectedParentPropertyFileConfigs
        = new ConcurrentHashMap<ClassLoader, List<Class<? extends PropertyFileConfig>>>();

    private boolean isActivated = true;

    private List<Class<? extends PropertyFileConfig>> propertyFileConfigClasses
        = new ArrayList<Class<?  extends PropertyFileConfig>>();

    private final Set<Type> dynamicConfigTypes = new HashSet<Type>();
    private Bean<DynamicBeanProducer> dynamicProducer;

    private final List<Bean<? extends ConfigSource>> cdiSources = new ArrayList<Bean<? extends ConfigSource>>();
    private final List<Bean<? extends ConfigFilter>> cdiFilters = new ArrayList<Bean<? extends ConfigFilter>>();
    private final List<Class<?>> dynamicConfigurationBeanClasses = new ArrayList<Class<?>>();

    @SuppressWarnings("UnusedDeclaration")
    protected void init(@Observes BeforeBeanDiscovery beforeBeanDiscovery)
    {
        isActivated = ClassDeactivationUtils.isActivated(getClass());
    }

    public static void registerConfigMBean()
    {
        String appName = ConfigResolver.getPropertyValue(ConfigResolver.DELTASPIKE_APP_NAME_CONFIG);
        if (appName != null && appName.length() > 0)
        {
            try
            {
                MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();

                ClassLoader tccl = ClassUtils.getClassLoader(ConfigurationExtension.class);
                DeltaSpikeConfigInfo cfgMBean = new DeltaSpikeConfigInfo(tccl);

                ObjectName name = new ObjectName("deltaspike.config." + appName + ":type=DeltaSpikeConfig");
                mBeanServer.registerMBean(cfgMBean, name);
            }
            catch (InstanceAlreadyExistsException iae)
            {
                // all fine, the CfgBean got already registered.
                // Most probably by the ServletConfigListener
            }
            catch (Exception e)
            {
                throw new RuntimeException(e);
            }
        }
    }

    public static void unRegisterConfigMBean(String appName)
    {
        if (appName != null && appName.length() > 0)
        {
            try
            {
                MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();

                ObjectName name = new ObjectName("deltaspike.config." + appName + ":type=DeltaSpikeConfig");

                mBeanServer.unregisterMBean(name);
            }
            catch (InstanceNotFoundException infe)
            {
                // all ok, nothing to de-register it seems
            }
            catch (Exception e)
            {
                throw new RuntimeException(e);
            }
        }
    }

    @SuppressWarnings("UnusedDeclaration")
    public void collectUserConfigSources(@Observes ProcessAnnotatedType<? extends PropertyFileConfig> pat)
    {
        if (!isActivated)
        {
            return;
        }

        Class<? extends PropertyFileConfig> pcsClass = pat.getAnnotatedType().getJavaClass();
        if (pcsClass.isAnnotation() ||
            pcsClass.isInterface()  ||
            pcsClass.isSynthetic()  ||
            pcsClass.isArray()      ||
            pcsClass.isEnum()         )
        {
            // we only like to add real classes
            return;
        }

        if (pat.getAnnotatedType().isAnnotationPresent(Exclude.class))
        {
            // We only pick up PropertyFileConfigs if they are not excluded
            // This can be the case for PropertyFileConfigs which are registered via java.util.ServiceLoader
            return;
        }

        propertyFileConfigClasses.add(pcsClass);
    }

    public void findDynamicConfigurationBeans(@Observes ProcessAnnotatedType<?> pat)
    {
        if (!pat.getAnnotatedType().isAnnotationPresent(Configuration.class))
        {
            return;
        }
        final Class<?> javaClass = pat.getAnnotatedType().getJavaClass();
        if (!javaClass.isInterface())
        {
            return;
        }
        dynamicConfigurationBeanClasses.add(javaClass);
    }

    public void findSources(@Observes ProcessBean<? extends ConfigSource> source)
    {
        if (!source.getAnnotated().isAnnotationPresent(Source.class))
        {
            return;
        }
        cdiSources.add(source.getBean());
    }

    public void findFilters(@Observes ProcessBean<? extends ConfigFilter> filter)
    {
        if (!filter.getAnnotated().isAnnotationPresent(Filter.class))
        {
            return;
        }
        cdiFilters.add(filter.getBean());
    }

    public void findDynamicProducer(@Observes ProcessProducerMethod<?, DynamicBeanProducer> processBean)
    {
        dynamicProducer = processBean.getBean();
    }

    public void collectDynamicTypes(@Observes ProcessBean<?> processBean)
    {
        for (final InjectionPoint ip : processBean.getBean().getInjectionPoints())
        {
            final ConfigProperty annotation = ip.getAnnotated().getAnnotation(ConfigProperty.class);
            if (annotation == null || annotation.converter() == ConfigResolver.Converter.class)
            {
                continue;
            }

            dynamicConfigTypes.add(ip.getType());
        }
    }

    public void addDynamicBeans(@Observes AfterBeanDiscovery afterBeanDiscovery, BeanManager bm)
    {
        if (dynamicProducer != null && !dynamicConfigTypes.isEmpty())
        {
            afterBeanDiscovery.addBean(new DynamicBean(dynamicProducer, dynamicConfigTypes));
        }
        for (final Class<?> proxyType : dynamicConfigurationBeanClasses)
        {
            afterBeanDiscovery.addBean()
                .types(proxyType, Object.class)
                .qualifiers(Default.Literal.INSTANCE, Any.Literal.INSTANCE)
                .scope(ApplicationScoped.class)
                .id("DeltaSpikeConfiguration#" + proxyType.getName())
                .createWith(cc -> {
                        // TODO: support partialbean binding? can make sense for virtual properties +
                        //  would integrate with jcache
                        // we'd need to add @PartialBeanBinding on a bean created from ConfigurationHandler
                        // detection can just be a loadClass of this API
                        // for now: waiting for user request for it
                        final Class<?>[] api = new Class<?>[]{proxyType};
                        final Configuration configuration = api[0].getAnnotation(Configuration.class);
                        final long cacheFor = configuration.cacheFor();
                        return Proxy.newProxyInstance(
                            Thread.currentThread().getContextClassLoader(), api,
                            new ProxyConfigurationLifecycle.ConfigurationHandler(
                                cacheFor <= 0 ? -1 : configuration.cacheUnit().toMillis(cacheFor), configuration.prefix())
                        );

                    })
                .beanClass(proxyType);
        }
    }

    @SuppressWarnings("UnusedDeclaration")
    public void registerUserConfigSources(@Observes AfterBeanDiscovery abd)
    {
        if (!isActivated)
        {
            return;
        }

        // create a local copy with all the collected PropertyFileConfig
        Set<Class<? extends PropertyFileConfig>> allPropertyFileConfigClasses
            = new HashSet<Class<? extends PropertyFileConfig>>(this.propertyFileConfigClasses);

        // now add any PropertyFileConfigs from a 'parent BeanManager'
        // we start with the current TCCL
        ClassLoader currentClassLoader = ClassUtils.getClassLoader(null);
        addParentPropertyFileConfigs(currentClassLoader, allPropertyFileConfigClasses);

        // now let's add our own PropertyFileConfigs to the detected ones.
        // because maybe WE are a parent BeanManager ourselves!
        if (!this.propertyFileConfigClasses.isEmpty())
        {
            detectedParentPropertyFileConfigs.put(currentClassLoader, this.propertyFileConfigClasses);
        }

        // collect all the ConfigSources from our PropertyFileConfigs
        List<ConfigSource> configSources = new ArrayList<ConfigSource>();
        for (Class<? extends PropertyFileConfig> propertyFileConfigClass : allPropertyFileConfigClasses)
        {
            configSources.addAll(createPropertyConfigSource(propertyFileConfigClass));
        }
        ConfigResolver.addConfigSources(configSources);

        registerConfigMBean();

        logConfiguration();
    }

    public void validateConfiguration(@Observes AfterDeploymentValidation adv)
    {
        List<ConfigSource> configSources = new ArrayList<ConfigSource>(cdiSources.size());
        for (final Bean bean : cdiSources)
        {
            configSources.add(BeanProvider.getContextualReference(ConfigSource.class, bean));
        }
        ConfigResolver.addConfigSources(configSources);

        for (final Bean bean : cdiFilters)
        {
            ConfigResolver.addConfigFilter(BeanProvider.getContextualReference(ConfigFilter.class, bean));
        }

        processConfigurationValidation(adv);
    }

    private void logConfiguration()
    {
        Boolean logConfig = ConfigResolver.resolve(ConfigResolver.DELTASPIKE_LOG_CONFIG).as(Boolean.class).getValue();
        if (logConfig != null && logConfig && LOG.isLoggable(Level.INFO))
        {
            StringBuilder sb = new StringBuilder(1 << 16);

            // first log out the config sources in descendent ordinal order
            sb.append("ConfigSources: ");
            ConfigSource[] configSources = ConfigResolver.getConfigSources();
            for (int i = 0; i < configSources.length; i++)
            {
                ConfigSource configSource = configSources[i];
                sb.append("\n\t").append(configSource.getOrdinal()).append(" - ").append(configSource.getConfigName())
                    .append(" (CS_").append(i + 1).append(")");
            }

            // and all the entries in no guaranteed order
            Map<String, String> allProperties = ConfigResolver.getAllProperties();
            sb.append("\n\nConfigured Values:");
            for (Map.Entry<String, String> entry : allProperties.entrySet())
            {
                int fromConfigSource;

                sb.append("\n\t")
                    .append(entry.getKey())
                    .append(" = ")
                    .append(ConfigResolver.filterConfigValueForLog(entry.getKey(), entry.getValue()))
                    .append(" (")
                    .append(configuredIn(configSources, entry.getKey()))
                    .append(")");
            }

            LOG.info(sb.toString());
        }
    }

    private static String configuredIn(ConfigSource[] configSources, String key)
    {
        int foundInOrdinal = -1;
        int foundInConfigSource = -1;

        for (int i = 0; i < configSources.length; i++)
        {
            ConfigSource configSource = configSources[i];
            if ((configSource.isScannable() && configSource.getProperties().containsKey(key) ||
                 !configSource.isScannable() && StringUtils.isNotEmpty(configSource.getPropertyValue(key)))
                && configSource.getOrdinal() > foundInOrdinal)
            {
                foundInConfigSource = i;
                foundInOrdinal = configSource.getOrdinal();
            }
        }
        return "CS_" + (foundInConfigSource + 1);
    }

    /**
     * Add all registered PropertyFileConfigs which got picked up in a parent ClassLoader already
     */
    private void addParentPropertyFileConfigs(ClassLoader currentClassLoader,
                                              Set<Class<? extends PropertyFileConfig>> propertyFileConfigClasses)
    {
        if (currentClassLoader.getParent() == null)
        {
            return;
        }

        for (Map.Entry<ClassLoader, List<Class<? extends PropertyFileConfig>>> classLoaderListEntry :
                detectedParentPropertyFileConfigs.entrySet())
        {
            if (currentClassLoader.getParent().equals(classLoaderListEntry.getKey()))
            {
                // if this is the direct parent ClassLoader then lets add those PropertyFileConfigs.
                propertyFileConfigClasses.addAll(classLoaderListEntry.getValue());

                // even check further parents
                addParentPropertyFileConfigs(classLoaderListEntry.getKey(), propertyFileConfigClasses);

                // and be done. There can only be a single parent CL...
                return;
            }
        }
    }

    /**
     * This method triggers freeing of the ConfigSources.
     */
    @SuppressWarnings("UnusedDeclaration")
    public void freeConfigSources(@Observes BeforeShutdown bs)
    {
        String appName = ConfigResolver.getPropertyValue(ConfigResolver.DELTASPIKE_APP_NAME_CONFIG);
        unRegisterConfigMBean(appName);

        ConfigResolver.freeConfigSources();
        detectedParentPropertyFileConfigs.remove(ClassUtils.getClassLoader(null));

        // we also free the ClassDeactivationUtils cache
        ClassDeactivationUtils.clearCache();
    }

    /**
     * @return create an instance of the given {@link PropertyFileConfig} and return all it's ConfigSources.
     */
    private List<ConfigSource> createPropertyConfigSource(Class<? extends PropertyFileConfig> propertyFileConfigClass)
    {
        String fileName = "";
        try
        {
            PropertyFileConfig propertyFileConfig = propertyFileConfigClass.newInstance();
            fileName = propertyFileConfig.getPropertyFileName();
            EnvironmentPropertyConfigSourceProvider environmentPropertyConfigSourceProvider
                = new EnvironmentPropertyConfigSourceProvider(fileName, propertyFileConfig.isOptional());

            return environmentPropertyConfigSourceProvider.getConfigSources();
        }
        catch (InstantiationException e)
        {
            throw new RuntimeException(CANNOT_CREATE_CONFIG_SOURCE_FOR_CUSTOM_PROPERTY_FILE_CONFIG +
                propertyFileConfigClass.getName(), e);
        }
        catch (IllegalAccessException e)
        {
            throw new RuntimeException(CANNOT_CREATE_CONFIG_SOURCE_FOR_CUSTOM_PROPERTY_FILE_CONFIG +
                    propertyFileConfigClass.getName(), e);
        }
        catch (IllegalStateException e)
        {
            throw new IllegalStateException(
                propertyFileConfigClass.getName() + " points to an invalid file: '" + fileName + "'", e);
        }
    }

    protected void processConfigurationValidation(AfterDeploymentValidation adv)
    {
        for (ConfigValidator configValidator : ServiceUtils.loadServiceImplementations(ConfigValidator.class))
        {
            Set<String> violations = configValidator.processValidation();

            if (violations == null)
            {
                continue;
            }

            for (String violation : violations)
            {
                adv.addDeploymentProblem(new IllegalStateException(violation));
            }
        }
    }

    @ApplicationScoped
    @Typed(DynamicBeanProducer.class) // used as an internal bean
    static class DynamicBeanProducer extends BaseConfigPropertyProducer
    {
        @Produces
        @ConfigProperty(name = "ignored")
        public Object create(final InjectionPoint ip)
        {
            return super.getUntypedPropertyValue(ip, ip.getType());
        }
    }

    @Vetoed
    private static final class DynamicBean<T> implements Bean<T>
    {
        private final Bean<T> producer;
        private final Set<Type> types;

        private DynamicBean(final Bean<T> producer, final Set<Type> types)
        {
            this.producer = producer;
            this.types = types;
        }

        @Override
        public Set<Type> getTypes()
        {
            return types;
        }

        /**
         * this method got dumped in EE10, but still exists in EE9
         */
        public boolean isNullable()
        {
            return true;
        }

        @Override
        public Set<Annotation> getQualifiers()
        {
            return producer.getQualifiers();
        }

        @Override
        public Class<? extends Annotation> getScope()
        {
            return producer.getScope();
        }

        @Override
        public String getName()
        {
            return producer.getName();
        }

        @Override
        public Set<InjectionPoint> getInjectionPoints()
        {
            return producer.getInjectionPoints();
        }

        @Override
        public Class<?> getBeanClass()
        {
            return producer.getBeanClass();
        }

        @Override
        public Set<Class<? extends Annotation>> getStereotypes()
        {
            return producer.getStereotypes();
        }

        @Override
        public boolean isAlternative()
        {
            return producer.isAlternative();
        }

        @Override
        public T create(final CreationalContext<T> creationalContext)
        {
            return producer.create(creationalContext);
        }

        @Override
        public void destroy(final T t, final CreationalContext<T> creationalContext)
        {
            producer.destroy(t, creationalContext);
        }
    }
}
