package com.atlassian.bitbucket.internal.importer.repository;

import com.atlassian.bitbucket.ServiceException;
import com.atlassian.bitbucket.repository.MinimalRef;
import com.atlassian.bitbucket.repository.RefChange;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.repository.RepositoryService;
import com.atlassian.bitbucket.scm.Command;
import com.atlassian.bitbucket.scm.ScmService;
import com.atlassian.bitbucket.scm.mirror.MirrorSyncCallback;
import com.atlassian.bitbucket.scm.mirror.MirrorSyncCommandParameters;
import com.atlassian.bitbucket.scm.mirror.ScmMirrorCommandFactory;
import com.atlassian.bitbucket.user.SecurityService;
import com.atlassian.bitbucket.util.Timer;
import com.atlassian.bitbucket.util.TimerUtils;
import com.atlassian.event.api.EventPublisher;
import com.google.common.collect.ImmutableList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;

import static com.atlassian.bitbucket.internal.importer.repository.ImportTaskFailureType.*;

/**
 * A task that syncs all the {@link com.atlassian.bitbucket.repository.Ref refs} from the external source to
 * the newly created {@link Repository repository}
 */
public class RefSyncTask implements Callable<Void> {

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

    private final EventPublisher eventPublisher;
    private final RepositoryService repositoryService;
    private final RefSyncTaskParameters parameters;
    private final RepositoryImportTaskHelper repositoryImportTaskHelper;
    private final ScmService scmService;
    private final SecurityService securityService;

    public RefSyncTask(EventPublisher eventPublisher,
                       RepositoryImportTaskHelper repositoryImportTaskHelper,
                       RepositoryService repositoryService,
                       ScmService scmService,
                       SecurityService securityService,
                       RefSyncTaskParameters parameters) {
        this.eventPublisher = eventPublisher;
        this.repositoryImportTaskHelper = repositoryImportTaskHelper;
        this.repositoryService = repositoryService;
        this.scmService = scmService;
        this.securityService = securityService;
        this.parameters = parameters;
    }

    @Override
    public Void call() {
        String cloneUrl = parameters.getCloneUrl();
        Repository repository = parameters.getRepository();
        Long taskId = parameters.getTaskId();

        repositoryImportTaskHelper.updateImportTask(taskId, ImportTaskState.STARTED);

        ScmMirrorCommandFactory commandFactory = scmService.getMirrorCommandFactory(repository);

        MirrorSyncCommandParameters syncParameters = buildSyncParameters(cloneUrl);

        ImporterSyncCallback syncCallback = new ImporterSyncCallback();
        try (Timer ignored = TimerUtils.start("[" + repository + "] fetch from " + cloneUrl)) {

            securityService.impersonating(parameters.getUser(), "Importing repository").call(() -> {
                syncRefs(commandFactory, syncParameters, syncCallback);

                eventPublisher.publish(new RepositoryImportedEvent(this, repository, syncCallback.getChanges(),
                        syncCallback.getFailedRefs(), cloneUrl));

                return null;
            });

            repositoryImportTaskHelper.updateImportTask(taskId, ImportTaskState.SUCCESS);
        } catch (ServiceException e) {
            handleError(repository, taskId, SYNC_FAILED, e);
        }
        return null;
    }

    private void handleError(Repository repository, Long taskId, ImportTaskFailureType failureType, Exception e) {
        log.error("Failed to import repository: {}", repository, e);
        securityService.impersonating(parameters.getUser(), "Deleting repository due to failed import").call(() -> {
            repositoryService.delete(repository);
            repositoryImportTaskHelper.failImportTask(taskId, failureType);
            return null;
        });
    }

    private MirrorSyncCommandParameters buildSyncParameters(String cloneUrl) {
        MirrorSyncCommandParameters.Builder syncParameters = new MirrorSyncCommandParameters.Builder(cloneUrl);
        parameters.getCredential().ifPresent(credential -> {
            syncParameters.username(credential.getUsername());
            syncParameters.password(credential.getPassword());
        });
        return syncParameters.build();
    }

    private void syncRefs(ScmMirrorCommandFactory commandFactory, MirrorSyncCommandParameters syncParameters, ImporterSyncCallback syncCallback) {
        Command<Void> command = commandFactory.synchronize(syncParameters, syncCallback);
        command.setIdleTimeout(parameters.getIdleTimeout(TimeUnit.SECONDS));
        command.setExecutionTimeout(parameters.getExecutionTimeout(TimeUnit.SECONDS));

        command.call();
    }

    private class ImporterSyncCallback implements MirrorSyncCallback {

        private final ImmutableList.Builder<RefChange> changes = ImmutableList.builder();
        private final ImmutableList.Builder<MinimalRef> failedRefs = ImmutableList.builder();

        public List<RefChange> getChanges() {
            return changes.build();
        }

        public List<MinimalRef> getFailedRefs() {
            return failedRefs.build();
        }

        @Override
        public boolean onFailedRef(@Nonnull MinimalRef ref) throws IOException {
            failedRefs.add(ref);
            return true;
        }

        @Override
        public boolean onRefChange(@Nonnull RefChange refChange) throws IOException {
            changes.add(refChange);
            return true;
        }
    }
}