package com.atlassian.stash.internal.plugin;

import com.atlassian.plugin.spring.AvailableToPlugins;
import com.atlassian.sal.api.pluginsettings.PluginSettings;
import com.atlassian.sal.api.pluginsettings.PluginSettingsFactory;
import com.atlassian.stash.internal.spring.SpringTransactionUtils;
import com.google.common.base.Throwables;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.util.concurrent.UncheckedExecutionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;

import javax.annotation.Nonnull;
import java.lang.reflect.Proxy;

@Component("pluginSettingsFactory")
@AvailableToPlugins(PluginSettingsFactory.class)
public class DefaultPluginSettingsFactory implements PluginSettingsFactory {

    public static final String GLOBAL_SETTING_NAMESPACE = "bitbucket.global.settings";

    private final ApplicationContext applicationContext;
    private final PluginSettingDao pluginSettingDao;
    private final TransactionTemplate readOnlyTransaction;
    private final TransactionTemplate readWriteTransaction;
    // Cache for the PluginSettings instances. These are cached because they are requested a lot and creating them is
    // cheap, but not free
    private final LoadingCache<String, PluginSettings> settingsForKey;

    private PluginSettings globalSettings;

    @Autowired
    public DefaultPluginSettingsFactory(ApplicationContext applicationContext, PluginSettingDao pluginSettingDao,
                                        PlatformTransactionManager transactionManager) {

        this.applicationContext = applicationContext;
        this.pluginSettingDao = pluginSettingDao;
        this.readOnlyTransaction = new TransactionTemplate(transactionManager, SpringTransactionUtils.definitionFor(
                TransactionDefinition.PROPAGATION_REQUIRED, true));
        this.readWriteTransaction = new TransactionTemplate(transactionManager, SpringTransactionUtils.REQUIRES_NEW);

        settingsForKey = CacheBuilder.newBuilder()
                .maximumSize(100L) // we want some kind of limit even if the PluginSettings instances are not heavy
                .build(new CacheLoader<String, PluginSettings>() {
                    @Override
                    public PluginSettings load(@Nonnull String namespace) throws Exception {
                        // PluginSettings only supports persisting simple types and java collections, but because of
                        // the jsr303 validation annotations the hibernate validator needs to be on the classpath. As
                        // a result, the PluginSettings instances need to be wrapped in a proxy that sets the correct
                        // context class loader.
                        return (PluginSettings) Proxy.newProxyInstance(
                                PluginSettings.class.getClassLoader(),
                                new Class<?>[] {PluginSettings.class},
                                new ContextClassLoaderSettingInvocationHandler(
                                        DefaultPluginSettingsFactory.this.applicationContext.getBean(
                                                "pluginSettingsPrototype",
                                                DefaultPluginSettingsFactory.this.pluginSettingDao,
                                                namespace,
                                                DefaultPluginSettingsFactory.this.readOnlyTransaction,
                                                DefaultPluginSettingsFactory.this.readWriteTransaction))
                        );
                    }
                });
    }

    @Override
    public PluginSettings createGlobalSettings() {
        if (globalSettings == null) {
            globalSettings = settingsForKey.apply(GLOBAL_SETTING_NAMESPACE);
        }
        return globalSettings;
    }

    @Override
    public PluginSettings createSettingsForKey(String key) {
        try {
            return settingsForKey.getUnchecked(key);
        } catch (UncheckedExecutionException e) {
            throw Throwables.propagate(e.getCause());
        }
    }
}
