package com.atlassian.bitbucket.internal.ssh.service;

import com.atlassian.activeobjects.spi.TransactionSynchronisationManager;
import com.atlassian.bitbucket.internal.ssh.dao.SshConfigurationDao;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.permission.PermissionValidationService;
import com.atlassian.bitbucket.ssh.SimpleSshConfiguration;
import com.atlassian.bitbucket.ssh.SshConfiguration;
import com.atlassian.bitbucket.ssh.SshConfigurationService;
import com.atlassian.bitbucket.ssh.event.SshConfigurationChangedEvent;
import com.atlassian.bitbucket.util.ValidationUtils;
import com.atlassian.cache.*;
import com.atlassian.event.api.EventPublisher;
import com.atlassian.sal.api.transaction.TransactionCallback;
import com.atlassian.sal.api.transaction.TransactionTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;

import javax.annotation.Nonnull;
import javax.validation.Validator;
import java.util.OptionalInt;

public class DefaultSshConfigurationService implements SshConfigurationService, DisposableBean {

    private static final Logger log = LoggerFactory.getLogger(SshConfigurationService.class);
    private static final OptionalInt overriddenPort = getOverriddenPort();
    private static final String SSH_CONFIGURATION_CACHE_KEY = SshConfiguration.class.getName();
    private static final String SYS_PROP_PORT_OVERRIDE = "plugin.ssh.port";

    private final EventPublisher eventPublisher;
    private final PermissionValidationService permissionValidationService;
    private final CachedReference<SshConfiguration> sshConfigurationReference;
    private final SshConfigurationDao sshConfigurationDao;
    private final TransactionTemplate transactionTemplate;
    private final TransactionSynchronisationManager transactionSynchronisationManager;
    private final Validator validator;
    private final CachedReferenceAdapter<SshConfiguration> sshConfigurationListener;

    public DefaultSshConfigurationService(EventPublisher eventPublisher,
                                          PermissionValidationService permissionValidationService,
                                          SshConfigurationDao sshConfigurationDao,
                                          TransactionTemplate transactionTemplate,
                                          TransactionSynchronisationManager transactionSynchronisationManager,
                                          Validator validator,
                                          CacheFactory cacheFactory) {
        this.eventPublisher = eventPublisher;
        this.permissionValidationService = permissionValidationService;
        this.sshConfigurationDao = sshConfigurationDao;
        this.transactionTemplate = transactionTemplate;
        this.transactionSynchronisationManager = transactionSynchronisationManager;
        this.validator = validator;

        this.sshConfigurationReference = cacheFactory.getCachedReference(SSH_CONFIGURATION_CACHE_KEY,
                new Supplier<SshConfiguration>() {
            @Override
            public SshConfiguration get() {
                return DefaultSshConfigurationService.this.sshConfigurationDao.get();
            }
        }, new CacheSettingsBuilder().replicateViaInvalidation().build());

        //If this plugin is reloading, the local SshConfiguration class will be from a different OSGI classloader so will
        //not be equivalent and we will get a ClassCastException. Force a reload (which is, unfortunately, cluster-wide)
        this.sshConfigurationReference.reset();

        this.sshConfigurationListener = new CachedReferenceAdapter<SshConfiguration>() {
            @Override
            public void onEvict(@Nonnull CachedReferenceEvent<SshConfiguration> event) {
                notifyConfigurationRefresh();
            }

            @Override
            public void onReset(@Nonnull CachedReferenceEvent<SshConfiguration> event) {
                notifyConfigurationRefresh();
            }
        };
        this.sshConfigurationReference.addListener(sshConfigurationListener, false);
    }

    @Nonnull
    @Override
    // @Unsecured as it is required for displaying the SSH url in the UI
    public SshConfiguration getConfiguration() {
        try (ClassLoaderSetter setter = ClassLoaderSetter.with(SshConfiguration.class)) {
            SshConfiguration result = sshConfigurationReference.get();
            return overriddenPort.isPresent() ? new SimpleSshConfiguration(result.isEnabled(), result.isAccessKeysEnabled(),
                    overriddenPort.getAsInt(), result.getBaseUrl()) : result;
        }
    }

    @Override
    // @Secured sys-admin only
    public void setConfiguration(@Nonnull final SshConfiguration newConfiguration) {
        permissionValidationService.validateForGlobal(Permission.SYS_ADMIN);

        ValidationUtils.validate(validator, newConfiguration);
        final SshConfiguration oldConfiguration = getConfiguration();

        transactionTemplate.execute(new TransactionCallback<Void>() {
            @Override
            public Void doInTransaction() {
                sshConfigurationDao.set(newConfiguration);

                transactionSynchronisationManager.runOnSuccessfulCommit(new Runnable() {
                    @Override
                    public void run() {
                        try (ClassLoaderSetter setter = ClassLoaderSetter.with(SshConfiguration.class)) {
                            sshConfigurationReference.reset();
                        }

                        notifyConfigurationChanged(oldConfiguration, newConfiguration);
                    }
                });
                return null;
            }
        });
    }

    @Override
    public void destroy() throws Exception {
        sshConfigurationReference.removeListener(sshConfigurationListener);
    }

    private static OptionalInt getOverriddenPort() {
        String overridePort = System.getProperty(SYS_PROP_PORT_OVERRIDE);
        if (overridePort != null) {
            try {
                return OptionalInt.of(Integer.parseInt(overridePort));
            } catch (NumberFormatException e) {
                log.warn("Override {} value [{}] is not valid, ignoring", SYS_PROP_PORT_OVERRIDE, overridePort, e);
            }
        }
        return OptionalInt.empty();
    }

    private void notifyConfigurationChanged(SshConfiguration oldConfiguration, SshConfiguration newConfiguration) {
        eventPublisher.publish(new SshConfigurationChangedEvent(this, oldConfiguration, newConfiguration));
    }

    private void notifyConfigurationRefresh() {
        eventPublisher.publish(new SshConfigurationRefreshEvent(this));
    }

    private static class ClassLoaderSetter implements AutoCloseable {
        public static ClassLoaderSetter with(Class<?> clazz) {
            return new ClassLoaderSetter(clazz.getClassLoader());
        }

        private final ClassLoader originalClassLoader;
        private final ClassLoader overrideClassLoader;

        public ClassLoaderSetter(ClassLoader overrideClassLoader) {
            this.originalClassLoader = Thread.currentThread().getContextClassLoader();
            this.overrideClassLoader = overrideClassLoader;

            Thread.currentThread().setContextClassLoader(overrideClassLoader);
        }

        @Override
        public void close() {
            Thread.currentThread().setContextClassLoader(originalClassLoader);
        }
    }
}
