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

import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.stash.internal.db.DatabaseHandle;
import com.atlassian.stash.internal.db.DatabaseLatch;
import com.atlassian.stash.internal.db.DatabaseManager;
import com.atlassian.stash.internal.jdbc.DataSourceConfiguration;
import com.atlassian.stash.internal.maintenance.CompositeMaintenanceTask;
import com.atlassian.stash.internal.maintenance.MaintenanceTask;
import com.atlassian.stash.internal.maintenance.MaintenanceTaskFactory;
import com.atlassian.stash.internal.maintenance.latch.LatchMode;
import com.atlassian.stash.internal.migration.CanceledMigrationException;
import com.atlassian.bitbucket.util.Progress;
import com.google.common.base.Throwables;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;

import static com.atlassian.stash.internal.maintenance.DrainHelper.drain;

public abstract class BaseMigrationTask implements MaintenanceTask {

    public static final String QUALIFIER_TARGET_DATABASE = "targetDatabase";

    protected static final Logger log = LoggerFactory.getLogger(BaseMigrationTask.class);

    protected final I18nService i18nService;
    protected final MigrationState state;

    protected volatile boolean canceled;

    private final DatabaseManager databaseManager;
    private final MaintenanceTask delegateTask;
    private final int drainTimeoutSeconds;
    private final int forceDrainTimeoutSeconds;

    protected BaseMigrationTask(DatabaseManager databaseManager, MaintenanceTaskFactory factory,
                                I18nService i18nService, DatabaseHandle targetDatabase,
                                int drainTimeoutSeconds, int forceDrainTimeoutSeconds) {
        this.i18nService = i18nService;
        this.databaseManager = databaseManager;
        this.drainTimeoutSeconds = drainTimeoutSeconds;
        this.forceDrainTimeoutSeconds = forceDrainTimeoutSeconds;

        // create a migrationState bean and register it in the beanFactory
        state = new SimpleMigrationState(databaseManager.getHandle(), targetDatabase);

        // create the delegate task
        delegateTask = createMigrationTask(factory, state);
    }

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

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

    @Override
    public void run() {
        try {
            delegateTask.run();

            if (canceled) {
                throw new CanceledMigrationException(
                        i18nService.createKeyedMessage("bitbucket.migration.canceled"));
            }
        } catch (Throwable t) {
            //The migration failed or was canceled. Revert the configuration/data source/session factory and then
            //propagate the exception.
            if (canceled) {
                log.info("Reverting database configuration after migration was canceled");
            } else {
                log.error("Reverting database configuration after a failed migration attempt", t);
            }
            revert();

            throw Throwables.propagate(t);
        }
    }

    protected MaintenanceTask createMigrationTask(MaintenanceTaskFactory factory, MigrationState state) {
        return new CompositeMaintenanceTask.Builder()
                .add(factory.backupPhaseBuilder(state)
                        .add(factory.activeObjectsBackupStep(state), 15)
                        .add(factory.latchAndDrainDatabaseMigrationStep(LatchMode.LOCAL), 5) //20
                        .add(factory.releaseAffixedDatabaseStep(), 2)                        //22
                        .add(factory.databaseBackupStep(state), 75)                          //97
                        .add(factory.configurationBackupStep(state), 3)                      //100 backup
                        .build(), 18)
                .add(factory.restorePhaseBuilder(state)
                        .add(factory.unpackBackupFilesStep(state), 0)
                        .add(factory.databaseRestoreStep(state), 80)
                        .add(factory.unlatchDatabaseStep(LatchMode.LOCAL, state.getTargetDatabase()), 0)
                        .add(factory.activeObjectsRestoreStep(state), 20) //100 restore
                        .build(), 82) //100 overall
                .add(factory.finalizeMigrationStep(state), 0)
                .build();
    }

    /**
     * Reverts the {@link DataSourceConfiguration DataSourceConfiguration},
     * {@code DataSource} and {@code SessionFactoryImplementor}, restoring them to their pre-migration values. If
     * the swappables were {@link DatabaseManager#isLatched()} latched}, the latch is released to allow any blocked
     * callers to continue their processing on the original database.
     */
    private void revert() {
        DatabaseLatch latch = databaseManager.getCurrentLatch();
        if (latch == null) {
            // the database may have been swapped over and unlatched already.
            latch = databaseManager.acquireLatch(LatchMode.LOCAL);
            if (!drain(latch, drainTimeoutSeconds, forceDrainTimeoutSeconds)) {
                log.warn("Could not drain all database connections while reverting migration. Switching back to the " +
                        "original database anyway.");
            }
        }

        //This will restore the swappables to reference the source data source and session factory, and also
        //release the latch to ensure any calls that were parked on it are allowed to continue.
        latch.unlatchTo(state.getSourceDatabase());

        //Close the new data source, because the system will never be updated to use it. Note that, at this time,
        //the new database is an unknown state and may require system administrator interaction before it can be
        //used as a migration target again.
        state.getTargetDatabase().close();
    }
}
