package com.atlassian.stash.internal.maintenance.backup;

import com.atlassian.event.api.EventPublisher;
import com.atlassian.johnson.event.EventLevel;
import com.atlassian.johnson.event.EventType;
import com.atlassian.bitbucket.Product;
import com.atlassian.bitbucket.event.backup.BackupCanceledEvent;
import com.atlassian.bitbucket.event.backup.BackupFailedEvent;
import com.atlassian.bitbucket.event.backup.BackupStartedEvent;
import com.atlassian.bitbucket.event.backup.BackupSucceededEvent;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.stash.internal.backup.BackupException;
import com.atlassian.stash.internal.backup.CanceledBackupException;
import com.atlassian.stash.internal.maintenance.latch.Latch;
import com.atlassian.stash.internal.maintenance.latch.LatchableService;
import com.atlassian.stash.internal.db.DatabaseManager;
import com.atlassian.stash.internal.johnson.JohnsonUtils;
import com.atlassian.stash.internal.maintenance.MaintenanceTask;
import com.atlassian.stash.internal.maintenance.MaintenanceTaskFactory;
import com.atlassian.stash.internal.maintenance.MaintenanceType;
import com.atlassian.stash.internal.maintenance.TaskMaintenanceEvent;
import com.atlassian.bitbucket.util.Progress;
import com.google.common.collect.Lists;
import org.springframework.core.OrderComparator;

import javax.annotation.Nonnull;
import java.util.Collections;
import java.util.List;

public abstract class AbstractBackupTask implements MaintenanceTask {

    private final BackupClientPlaceholderStep backupClientPlaceholderStep;
    private final MaintenanceTask delegateTask;
    private final EventPublisher eventPublisher;
    private final I18nService i18nService;
    private final List<LatchableService> latchableServices;

    private volatile boolean canceled;

    public AbstractBackupTask(DatabaseManager databaseManager, EventPublisher eventPublisher, I18nService i18nService,
                              MaintenanceTaskFactory taskFactory, LatchableService[] latchableServices) {
        this.backupClientPlaceholderStep = taskFactory.backupClientBackupStep();
        this.eventPublisher = eventPublisher;
        this.i18nService = i18nService;
        this.latchableServices = Lists.newArrayList(latchableServices);

        //Order the services and then reverse them (because this is used to unlatch the services, not latch them)
        //
        //Note: PluginScmService is proxied, so even if it implements Ordered it won't be picked up (unless the
        //      InternalScmService interface extended it, which is pretty vile). DefaultDatabaseManager is not a
        //      proxy, though, so this sorting will correctly move that ahead of PluginScmService. In other words,
        //      the final order here is still reliably correct with this code.
        OrderComparator.sort(this.latchableServices);
        Collections.reverse(this.latchableServices);

        delegateTask = createDelegateBackupTask(backupClientPlaceholderStep, taskFactory,
                new SimpleBackupState(databaseManager.getHandle()),
                new TaskMaintenanceEvent(EventType.get("performing-maintenance"),
                        Product.NAME + " is unavailable while it is being backed up",
                        EventLevel.get(JohnsonUtils.LEVEL_MAINTENANCE), MaintenanceType.BACKUP));
    }

    @Override
    public void cancel() {
        canceled = true;
        delegateTask.cancel();
    }

    public BackupClientProgressCallback getClientProgressCallback() {
        return backupClientPlaceholderStep;
    }

    @Nonnull
    @Override
    public Progress getProgress() {
        return delegateTask.getProgress();
    }

    @Override
    public void run() {
        BackupException exception;
        try {
            eventPublisher.publish(new BackupStartedEvent(this));

            delegateTask.run();

            if (canceled) {
                throw new CanceledBackupException(
                        i18nService.createKeyedMessage("bitbucket.backup.canceled"));
            }

            eventPublisher.publish(new BackupSucceededEvent(this));
        } catch (Throwable t) {
            unlatchAll();

            //We set the exception to a variable so we can fire the correct migration ended event
            if (t instanceof BackupException) {
                exception = (BackupException) t;
            } else {
                exception = new BackupException(i18nService.createKeyedMessage("bitbucket.backup.failed", Product.NAME), t);
            }

            if (exception instanceof CanceledBackupException) {
                eventPublisher.publish(new BackupCanceledEvent(this));
            } else {
                eventPublisher.publish(new BackupFailedEvent(this));
            }

            throw exception;
        }
    }

    protected abstract MaintenanceTask createDelegateBackupTask(BackupClientPlaceholderStep backupClientPlaceholderStep,
                                                                MaintenanceTaskFactory taskFactory,
                                                                BackupState backupState,
                                                                TaskMaintenanceEvent maintenanceEvent);

    private void unlatchAll() {
        for (LatchableService<?> latchableService : latchableServices) {
            Latch latch = latchableService.getCurrentLatch();
            if (latch != null) {
                latch.unlatch();
            }
        }
    }
}
