package com.atlassian.bitbucket.internal.key.ssh.dao.v3;

import com.atlassian.activeobjects.external.ActiveObjects;
import com.atlassian.activeobjects.external.ActiveObjectsUpgradeTask;
import com.atlassian.activeobjects.external.ModelVersion;
import com.atlassian.bitbucket.ao.AoUtils;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.internal.key.ssh.SshAccessKeyUtils;
import com.atlassian.bitbucket.internal.key.ssh.dao.AoSshKey;
import com.atlassian.bitbucket.internal.key.ssh.dao.v1.AoSshAccessKeyV1;
import com.atlassian.bitbucket.internal.key.ssh.dao.v1.AoSshKeyV1;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.permission.PermissionAdminService;
import com.atlassian.bitbucket.permission.SetPermissionRequest;
import com.atlassian.bitbucket.project.Project;
import com.atlassian.bitbucket.project.ProjectService;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.repository.RepositoryService;
import com.atlassian.bitbucket.user.SecurityService;
import com.atlassian.bitbucket.user.ServiceUser;
import com.atlassian.bitbucket.user.ServiceUserCreateRequest;
import com.atlassian.bitbucket.user.UserAdminService;
import com.atlassian.bitbucket.util.*;
import com.google.common.collect.Iterators;
import net.java.ao.Query;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Arrays;
import java.util.Iterator;

import static com.google.common.base.Preconditions.checkState;

/**
 *  Migrates schema version 2->3
 */
public class MigrateAccessKeysToServiceUsersTask implements ActiveObjectsUpgradeTask {
    private static final Logger log = LoggerFactory.getLogger(MigrateAccessKeysToServiceUsersTask.class);

    public static final int PAGE_SIZE = 1000;

    private final I18nService i18nService;
    private final PermissionAdminService permissionAdminService;
    private final ProjectService projectService;
    private final RepositoryService repositoryService;
    private final SecurityService securityService;
    private final UserAdminService userAdminService;

    public MigrateAccessKeysToServiceUsersTask(I18nService i18nService, PermissionAdminService permissionAdminService,
                                               ProjectService projectService, RepositoryService repositoryService,
                                               SecurityService securityService, UserAdminService userAdminService) {
        this.i18nService = i18nService;
        this.permissionAdminService = permissionAdminService;
        this.projectService = projectService;
        this.repositoryService = repositoryService;
        this.securityService = securityService;
        this.userAdminService = userAdminService;
    }

    @Override
    public ModelVersion getModelVersion() {
        // This should always represent the version this upgrade task upgrades *to*
        return ModelVersion.valueOf("3");
    }

    @Override
    public void upgrade(ModelVersion currentVersion, final ActiveObjects ao) {
        checkState(currentVersion.isSame(ModelVersion.valueOf("2")),
                "This upgrade task can only upgrade from version 2 to 3");

        //not including AoSshKey because that would perform schema modifications before we have a chance to migrate
        ao.migrateDestructively(AoSshKeyV1.class, AoSshAccessKeyV1.class);

        log.info("Migrating key accesses to service users and permissions");

        // Page keys to limit memory usage
        final Iterable<AoSshKeyV1> keys = new PagedIterable<>(new PageProvider<AoSshKeyV1>() {
            @Override
            public Page<AoSshKeyV1> get(PageRequest request) {
                return PageUtils.createPage(
                        Arrays.asList(ao.find(AoSshKeyV1.class, AoUtils.restrict(Query.select()
                                        .where(AoSshKeyV1.COLUMN_USER_ID + " is null")
                                                // Order is only for the test to be deterministic
                                        .order(AoSshKeyV1.COLUMN_ID), request
                        ))), request
                );
            }
        }, PAGE_SIZE);

        securityService.withPermission(Permission.ADMIN, "Migration of access keys to service users").call(
                new UncheckedOperation<Void>() {
                    @Override
                    public Void perform() {
                        migrate(keys, ao);

                        return null;
                    }
                });

        //Destroys any other tables that might exist from migration steps that got us to where we are
        ao.migrateDestructively(AoSshKey.class);

        log.info("Migrated key accesses to service users and permissions");
    }

    private void migrate(Iterable<AoSshKeyV1> keys, ActiveObjects ao) {
        for (AoSshKeyV1 key : keys) {
            final Iterator<AoSshAccessKeyV1> accessKeys = Iterators.forArray(key.getAccessEntries());

            if (accessKeys.hasNext()) {
                ServiceUser serviceUser = userAdminService.createServiceUser(
                        new ServiceUserCreateRequest.Builder()
                                .displayName(SshAccessKeyUtils.generateServiceUserDisplayName(
                                        i18nService, key.getText(), key.getLabel()))
                                .build());

                key.setUserId(serviceUser.getId());
                key.save();

                while (accessKeys.hasNext()) {
                    grantPermission(serviceUser, accessKeys.next());
                }
            } else {
                //a non-user key with no key accesses
                ao.delete(key);
            }
        }
    }

    private void grantPermission(ServiceUser serviceUser, AoSshAccessKeyV1 accessKey) {
        SetPermissionRequest.Builder builder = new SetPermissionRequest.Builder().user(serviceUser);
        Permission permission;
        switch (accessKey.getType()) {
            case PROJECT:
                permission = accessKey.isReadOnly() ? Permission.PROJECT_READ : Permission.PROJECT_WRITE;
                Project project = projectService.getById(accessKey.getResourceId());
                if (project == null) {
                    log.warn("Key access {} relates to a project {} that no longer exists. Not migrating.",
                            accessKey.getId(), accessKey.getResourceId());
                    return;
                }
                builder.projectPermission(permission, project);
                break;
            case REPOSITORY:
                permission = accessKey.isReadOnly() ? Permission.REPO_READ : Permission.REPO_WRITE;
                Repository repository = repositoryService.getById(accessKey.getResourceId());
                if (repository == null) {
                    log.warn("Key access {} relates to a repository {} that no longer exists. Not migrating.",
                            accessKey.getId(), accessKey.getResourceId());
                    return;
                }
                builder.repositoryPermission(permission, repository);
                break;
        }
        permissionAdminService.setPermission(builder.build());
    }
}
