package com.atlassian.stash.internal.scm.git.upgrade.sal;

import com.atlassian.bitbucket.util.*;
import com.atlassian.bitbucket.util.Timer;
import com.atlassian.sal.api.lifecycle.LifecycleAware;
import com.atlassian.sal.api.message.Message;
import com.atlassian.sal.api.pluginsettings.PluginSettings;
import com.atlassian.sal.api.pluginsettings.PluginSettingsFactory;
import com.atlassian.sal.api.transaction.TransactionCallback;
import com.atlassian.sal.api.transaction.TransactionTemplate;
import com.atlassian.sal.api.upgrade.PluginUpgradeTask;
import com.atlassian.bitbucket.concurrent.LockService;
import com.atlassian.stash.internal.scm.git.upgrade.AsynchronousUpgradeTask;
import com.atlassian.stash.internal.scm.git.upgrade.GitUpgradeManager;
import com.atlassian.stash.internal.scm.git.upgrade.SynchronousUpgradeTask;
import com.atlassian.stash.internal.scm.git.upgrade.UpgradeTaskSupport;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.repository.RepositoryService;
import com.atlassian.bitbucket.scm.git.GitScm;
import com.atlassian.bitbucket.server.ApplicationPropertiesService;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.user.SecurityService;
import com.atlassian.bitbucket.util.concurrent.LockGuard;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.io.Files;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.Ordered;

import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * Leverages SAL's {@code PluginUpgradeTask} functionality to run {@link SynchronousUpgradeTask synchronous upgrade
 * tasks} during startup, based on the application lifecycle. If all synchronous upgrade tasks apply successfully,
 * or have already run, a background thread is started to run {@link AsynchronousUpgradeTask asynchronous upgrade
 * tasks}.
 * <p>
 * This upgrade manager implements {@code LifecycleAware} to be notified of application startup, and it implements
 * {@code Ordered} to ensure, when {@code LifecycleAware} components are being notified, it is notified <i>first</i>.
 * That ensures Git upgrade tasks are performed before any other lifecycle processing is done. An important example
 * of other functionality also started in response to the application lifecycle is the SSH server. To ensure the Git
 * repositories are not available over SSH while synchronous upgrade tasks are run, they need to be <i>reliably</i>
 * notified of application startup before the SSH plugin is.
 *
 * @since 2.9
 */
public class SalGitUpgradeManager implements GitUpgradeManager, LifecycleAware, Ordered {

    /**
     * The name of the {@link LockService#getLock(String) lock} to acquire before applying upgrade tasks. The same
     * lock is used for both synchronous and asynchronous upgrade tasks, and it is acquired once for each type. It
     * must be reacquired for asynchronous upgrade tasks because they run on a different thread, and the lock can't
     * be acquired for synchronous upgrade tasks and then released on the {@link #upgradeThread upgrade thread}.
     *
     * @since 3.5
     */
    public static final String LOCK_NAME = "git:upgrade-manager";
    /**
     * The subdirectory under {@link ApplicationPropertiesService#getConfigDir() config} where upgrade marker files
     * are created.
     *
     * @since 3.5
     */
    public static final String UPGRADES_DIR = "upgrades";
    /**
     * The prefix applied to {@link AsynchronousUpgradeTask#getKey() upgrade keys} when creating marker files.
     *
     * @since 3.5
     */
    public static final String UPGRADE_PREFIX = "git-";

    private static final Comparator<? super UpgradeTaskSupport> ORDER = new Comparator<UpgradeTaskSupport>() {

        @Override
        public int compare(UpgradeTaskSupport left, UpgradeTaskSupport right) {
            int a = left.getOrder();
            int b = right.getOrder();

            return (a < b) ? -1 : ((a == b) ? 0 : 1);
        }
    };

    private static final Logger log = LoggerFactory.getLogger(SalGitUpgradeManager.class);

    private final List<AsynchronousUpgradeTask> asynchronousTasks;
    private final LockService lockService;
    private final PluginSettingsFactory pluginSettingsFactory;
    private final ApplicationPropertiesService propertiesService;
    private final RepositoryService repositoryService;
    private final SecurityService securityService;
    private final List<SynchronousUpgradeTask> synchronousTasks;
    private final TransactionTemplate transactionTemplate;

    private volatile Thread upgradeThread;

    public SalGitUpgradeManager(LockService lockService, PluginSettingsFactory pluginSettingsFactory,
                                ApplicationPropertiesService propertiesService, RepositoryService repositoryService,
                                SecurityService securityService, TransactionTemplate transactionTemplate,
                                List<AsynchronousUpgradeTask> asynchronousTasks,
                                List<SynchronousUpgradeTask> synchronousTasks) {
        this.asynchronousTasks = Lists.newArrayList(asynchronousTasks);
        this.lockService = lockService;
        this.pluginSettingsFactory = pluginSettingsFactory;
        this.propertiesService = propertiesService;
        this.repositoryService = repositoryService;
        this.securityService = securityService;
        this.synchronousTasks = ImmutableList.copyOf(synchronousTasks);
        this.transactionTemplate = transactionTemplate;

        Collections.sort(this.asynchronousTasks, ORDER);
    }

    /**
     * Ensures Git repositories are upgraded before <i>any other processing</i>, including other plugin upgrades
     * (order 5) and starting the SSH server (order 10).
     *
     * @return {@code 1}
     */
    @Override
    public int getOrder() {
        return 1;
    }

    /**
     * Applies any pending updates when the lifecycle indicates the application has started. This means they are
     * applied <i>after</i> the Spring {@code ApplicationContext} has started. {@code ApplicationContext} startup
     * has a <i>one minute timeout</i>, which can easily be blown by {@link SynchronousUpgradeTask synchronous
     * upgrade tasks}, so upgrades must be performed after.
     */
    @Override
    public void onStart() {
        if (transactionTemplate.execute(new SynchronousUpgrader())) {
            upgradeThread = new Thread(new AsynchronousUpgrader(), "git-upgrade");
            upgradeThread.setDaemon(true);
            upgradeThread.start();
        }
    }

    @Override
    public void onStop() {
        // Interrupts the background upgrade thread, if it's running, and attempts to wait for it to complete.
        if (upgradeThread != null && upgradeThread.isAlive()) {
            log.warn("Interrupting asynchronous upgrade tasks for SCM shutdown");
            upgradeThread.interrupt();

            try {
                upgradeThread.join(TimeUnit.SECONDS.toMillis(5L));
                log.info("Asynchronous upgrade tasks were interrupted");
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                log.error("Asynchronous upgrade tasks could not be interrupted");
            }
        }
    }

    /**
     * Acquires a lock (cluster-wide, if the system is clustered) to ensure upgrade tasks are not applied
     * concurrently, even if multiple servers are starting at the same time.
     * <p>
     * The lock is <i>acquired</i> before this returns, and a guard wrapping the lock is returned. Callers
     * should use a try-with-resources block to release the lock when their processing completes.
     *
     * @return a {@link LockGuard} wrapping the acquired lock
     * @since 3.5
     */
    private LockGuard lock() {
        return new LockGuard(lockService.getLock(LOCK_NAME));
    }

    private class AsynchronousUpgrader implements Runnable, TransactionCallback<Void> {

        private final File markerDir;
        private final List<MarkedUpgradeTask> pending;

        private AsynchronousUpgrader() {
            markerDir = new File(propertiesService.getConfigDir(), UPGRADES_DIR);
            pending = Lists.newArrayList(
                    Lists.transform(asynchronousTasks, new Function<AsynchronousUpgradeTask, MarkedUpgradeTask>() {

                        @Override
                        public MarkedUpgradeTask apply(AsynchronousUpgradeTask task) {
                            return new MarkedUpgradeTask(task, new File(markerDir, UPGRADE_PREFIX + task.getKey()));
                        }
                    })); //This list needs to be mutable for filtering later
        }

        @Override
        public Void doInTransaction() {
            FileUtils.mkdir(markerDir); //Ensure the marker directory exists while holding the lock

            for (MarkedUpgradeTask task : pending) {
                if (Thread.currentThread().isInterrupted()) { //Try to be sensitive to interruption
                    log.warn("Asynchronous upgrade tasks were interrupted. At least one task was not applied");
                    return null;
                }

                log.debug("Running asynchronous upgrade task {}", task);
                try {
                    task.apply();
                } catch (Exception e) {
                    log.error("Asynchronous upgrade task {} has failed. No further upgrade tasks will be attempted. " +
                            "This is likely to cause unexpected behavior", task, e);
                    return null;
                }
            }

            log.info("All asynchronous upgrade tasks have completed");
            return null;
        }

        @Override
        public void run() {
            if (filterTasks()) { //After all tasks have completed, avoid acquiring the lock completely
                try (LockGuard ignored = lock()) {
                    if (filterTasks()) { //Check for tasks completed while we were waiting for the lock
                        transactionTemplate.execute(this);

                        return;
                    }
                }
            }

            log.debug("All asynchronous upgrade tasks have already been completed");
        }

        /**
         * Removes all {@link #pending} tasks which have already been completed.
         *
         * @return {@code true} if at least one task is still pending; otherwise, {@code false} if all tasks have run
         * @since 3.5
         */
        private boolean filterTasks() {
            Iterator<MarkedUpgradeTask> iterator = pending.iterator();
            while (iterator.hasNext()) {
                MarkedUpgradeTask task = iterator.next();

                if (task.isComplete()) {
                    log.debug("Asynchronous upgrade task {} has already been completed", task);
                    iterator.remove();
                }
            }

            return !pending.isEmpty();
        }
    }

    private abstract class DelegatingUpgradeTask<T extends UpgradeTaskSupport> implements Operation<Void, Exception> {

        protected final T task;

        protected DelegatingUpgradeTask(T task) {
            this.task = task;
        }

        public void apply() throws Exception {
            try (Timer ignored = TimerUtils.start("git: apply " + task.getClass().getSimpleName())) {
                securityService.withPermission(Permission.REPO_WRITE, task.getDescription()).call(this);
            }
        }

        /**
         * Processes <i>all</i> the Git repositories under this instance, applying the wrapped
         * {@link com.atlassian.stash.internal.scm.git.upgrade.UpgradeTaskSupport upgrade task} to each.
         */
        @Override
        public Void perform() throws Exception {
            PagedIterable<Repository> repositories = new PagedIterable<>(new PageProvider<Repository>() {

                @Override
                public Page<Repository> get(PageRequest request) {
                    return PageUtils.asPageOf(Repository.class, repositoryService.findAll(request));
                }
            }, 100);

            for (Repository repository : repositories) {
                if (GitScm.ID.equals(repository.getScmId())) {
                    task.upgrade(repository);
                }
            }

            return null;
        }
    }

    private class MarkedUpgradeTask extends DelegatingUpgradeTask<AsynchronousUpgradeTask> {

        private final File marker;

        private MarkedUpgradeTask(AsynchronousUpgradeTask task, File marker) {
            super(task);

            this.marker = marker;
        }

        @Override
        public void apply() throws Exception {
            super.apply();
            try {
                Files.touch(marker);
            } catch (IOException e) {
                log.error("Asynchronous upgrade task {} could not be marked complete. It will run again on restart",
                        toString(), e);
            }
        }

        public boolean isComplete() {
            return marker.isFile();
        }

        @Override
        public String toString() {
            return task.getClass().getSimpleName() + " (" + task.getKey() + ")";
        }
    }

    private class SalUpgradeTask extends DelegatingUpgradeTask<SynchronousUpgradeTask> implements PluginUpgradeTask {

        private SalUpgradeTask(SynchronousUpgradeTask task) {
            super(task);
        }

        @Override
        public Collection<Message> doUpgrade() throws Exception {
            apply();

            return Collections.emptyList();
        }

        @Override
        public int getBuildNumber() {
            return task.getOrder();
        }

        @Override
        public String getPluginKey() {
            return SalPlugin.PLUGIN_KEY;
        }

        @Override
        public String getShortDescription() {
            return task.getDescription();
        }
    }

    private class SynchronousUpgrader
            implements Function<SynchronousUpgradeTask, PluginUpgradeTask>, TransactionCallback<Boolean> {

        @Override
        public PluginUpgradeTask apply(SynchronousUpgradeTask task) {
            return new SalUpgradeTask(task);
        }

        @Override
        public Boolean doInTransaction() {
            PluginSettings settings = pluginSettingsFactory.createGlobalSettings();

            SalPluginUpgrader synchronous = new SalPluginUpgrader(settings, getSalUpgradeTasks());
            if (synchronous.isNeeded()) {
                try (LockGuard ignored = lock()) {
                    if (synchronous.apply()) {
                        log.info("All synchronous upgrade tasks have completed");
                    }
                }
            }

            return synchronous.isSuccessful();
        }

        private List<PluginUpgradeTask> getSalUpgradeTasks() {
            //A mutable ArrayList is used here because PluginUpgrader sorts the provided list, not a copy of it
            return Lists.newArrayList(Iterables.transform(synchronousTasks, this));
        }
    }
}
