package com.atlassian.bitbucket.internal.scm.git.lfs;

import com.atlassian.bitbucket.AuthorisationException;
import com.atlassian.bitbucket.event.repository.RepositoryDeletedEvent;
import com.atlassian.bitbucket.event.repository.RepositoryForkedEvent;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.internal.scm.git.lfs.event.GitLfsBatchDownloadEvent;
import com.atlassian.bitbucket.internal.scm.git.lfs.event.GitLfsBatchUploadEvent;
import com.atlassian.bitbucket.internal.scm.git.lfs.model.BatchRequest;
import com.atlassian.bitbucket.internal.scm.git.lfs.model.BatchResponse;
import com.atlassian.bitbucket.internal.scm.git.lfs.model.OperationType;
import com.atlassian.bitbucket.internal.scm.git.lfs.settings.GitLfsSettingsService;
import com.atlassian.bitbucket.internal.scm.git.lfs.store.StoreAccessor;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.permission.PermissionValidationService;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.server.ApplicationMode;
import com.atlassian.bitbucket.server.ApplicationPropertiesService;
import com.atlassian.event.api.EventListener;
import com.atlassian.event.api.EventPublisher;

import javax.annotation.Nonnull;

import static java.util.Objects.requireNonNull;

/**
 * Service that provides the core Git LFS metadata functionality. This is used by the REST resource that
 * fulfils the primary Git LFS API.
 */
public class DefaultGitLfsService implements GitLfsService {

    private final EventPublisher eventPublisher;
    private final I18nService i18nService;
    private final GitLfsSettingsService settingsService;
    private final StoreAccessor storeAccessor;
    private final PermissionValidationService validationService;
    private final ApplicationMode mode;

    public DefaultGitLfsService(EventPublisher eventPublisher, I18nService i18nService,
                                ApplicationPropertiesService propertiesService, GitLfsSettingsService settingsService,
                                StoreAccessor storeAccessor, PermissionValidationService validationService) {
        this.eventPublisher = eventPublisher;
        this.i18nService = i18nService;
        this.settingsService = settingsService;
        this.storeAccessor = storeAccessor;
        this.validationService = validationService;
        mode = propertiesService.getMode();
    }

    @Override
    public void enable() {
        validationService.validateForGlobal(Permission.ADMIN);
        settingsService.setEnabled(true);
    }

    @Override
    public void enable(@Nonnull Repository repository) {
        requireNonNull(repository, "repository");
        validationService.validateForRepository(repository, Permission.REPO_ADMIN);
        settingsService.setEnabled(repository, true);
    }

    @Override
    public void delete(@Nonnull Repository repository) {
        requireNonNull(repository, "repository");
        storeAccessor.delete(repository);
        settingsService.cleanup(repository);
    }

    @Override
    public void disable() {
        validationService.validateForGlobal(Permission.ADMIN);
        settingsService.setEnabled(false);
    }

    @Override
    public void disable(@Nonnull Repository repository) {
        requireNonNull(repository, "repository");
        validationService.validateForRepository(repository, Permission.REPO_ADMIN);
        settingsService.setEnabled(repository, false);
    }

    @Nonnull
    @Override
    public BatchResponse handleBatchRequest(@Nonnull Repository repository, @Nonnull BatchRequest request) {
        requireNonNull(repository, "repository");
        requireNonNull(request, "request");

        validateGitLfsEnabled(repository);
        validatePermission(repository, request.getType());

        try {
            publishBatchActivityEvents(repository, request.getType());
        } catch (Exception e) {
            // Currently just used for analytics, so the request should not be failed. Any failure is also
            // likely logged elsewhere, so don't log here.
        }

        return storeAccessor.handleBatchRequest(repository, request);
    }

    @Override
    public boolean isEnabled() {
        return isMirror() || settingsService.isEnabled();
    }

    @Override
    public boolean isEnabled(@Nonnull Repository repository) {
        requireNonNull(repository, "repository");
        validationService.validateRepositoryAccessible(repository);
        return isMirror() || (settingsService.isEnabled() && settingsService.isEnabled(repository));
    }

    private boolean isMirror() {
        return mode == ApplicationMode.MIRROR;
    }

    /**
     * Cleans up when a repository is deleted
     *
     * @param event the event fired when a repository is deleted
     */
    @SuppressWarnings("unused")
    @EventListener
    public void onRepositoryDeleted(RepositoryDeletedEvent event) {
        delete(event.getRepository());
    }

    /**
     * Performs actions required when an LFS enabled repository is forked
     *
     * @param event the event fired when a repository is forked
     */
    @SuppressWarnings("unused")
    @EventListener
    public void onRepositoryForked(RepositoryForkedEvent event) {
        Repository fork = event.getRepository();
        Repository origin = fork.getOrigin();
        if (settingsService.isEnabled() && origin != null) {
            if (isEnabled(origin)) {
                enable(fork);
            } else {
                disable(fork);
            }
        }
    }

    private void publishBatchActivityEvents(Repository repository, OperationType operationType) {
        if (operationType == OperationType.DOWNLOAD) {
            eventPublisher.publish(new GitLfsBatchDownloadEvent(this, repository));
        } else if (operationType == OperationType.UPLOAD) {
            eventPublisher.publish(new GitLfsBatchUploadEvent(this, repository));
        }
    }

    /**
     * @param repository the repository to validate that Git LFS is enabled
     * @throws GitLfsDisabledException if Git LFS is disabled instance wide, or on the specified repository
     */
    private void validateGitLfsEnabled(Repository repository) {
        if (!isEnabled(repository)) {
            throw new GitLfsDisabledException(i18nService.createKeyedMessage("bitbucket.scm.git.lfs.disabled"));
        }
    }

    /**
     * @throws AuthorisationException if the current user lacks the required permission
     * @throws BadBatchRequestException if the operation type is unknown or unsupported
     */
    private void validatePermission(Repository repository, OperationType operationType) {
        if (operationType == OperationType.DOWNLOAD) {
            validationService.validateRepositoryAccessible(repository);
        } else if (operationType == OperationType.UPLOAD || operationType == OperationType.VERIFY) {
            validationService.validateForRepository(repository, Permission.REPO_WRITE);
        } else {
            throw new BadBatchRequestException(i18nService.getMessage(
                    "bitbucket.scm.git.lfs.request.unsupported.operation", operationType.name()));
        }
    }
}
