package com.atlassian.stash.internal.content;

import com.atlassian.bitbucket.ServerException;
import com.atlassian.bitbucket.ServiceException;
import com.atlassian.bitbucket.content.*;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.io.TypeAwareOutputSupplier;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.scm.*;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.user.Person;
import com.atlassian.bitbucket.util.Chainable;
import com.atlassian.bitbucket.util.PageRequest;
import com.atlassian.plugin.spring.AvailableToPlugins;
import com.atlassian.stash.internal.scm.AbstractScmService;
import com.atlassian.stash.internal.user.InternalUserService;
import com.google.common.base.Throwables;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;

import javax.annotation.Nonnull;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;

@AvailableToPlugins(ContentService.class)
@PreAuthorize("isRepositoryAccessible(#repository)")
@Service("contentService")
public class DefaultContentService extends AbstractScmService implements ContentService {

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

    private final I18nService i18nService;
    private final InternalUserService userService;

    @Autowired
    public DefaultContentService(ScmService scmService, I18nService i18nService, InternalUserService userService) {
        super(scmService);

        this.i18nService = i18nService;
        this.userService = userService;
    }

    @Nonnull
    @Override
    @SuppressWarnings("deprecation")
    public List<Blame> getBlame(@Nonnull Repository repository, @Nonnull String objectId, @Nonnull String path,
                                @Nonnull PageRequest pageRequest) {
        List<Blame> blame = getCommandFactory(repository)
                .blame(new BlameCommandParameters.Builder().commitId(objectId).path(path).build(), pageRequest)
                .call();

        return blame == null ? Collections.emptyList() : updateAuthors(blame);
    }

    @Nonnull
    @Override
    public ContentTreeNode.Type getType(@Nonnull Repository repository, @Nonnull String objectId, String path) {
        ContentTreeNode.Type type = getCommandFactory(repository)
                .type(new TypeCommandParameters.Builder().commitId(objectId).path(path).build())
                .call();
        if (type == null) {
            throw new NoSuchPathException(
                    i18nService.createKeyedMessage("bitbucket.service.content.type.none", path, objectId),
                    path, objectId);
        }
        return type;
    }

    @Override
    public void streamDirectory(@Nonnull Repository repository, @Nonnull String objectId, String path, boolean recursive,
                                @Nonnull ContentTreeCallback callback, @Nonnull PageRequest pageRequest) {
        pageRequest = pageRequest.buildRestrictedPageRequest(recursive ? maxDirectoryRecursiveChildren : maxDirectoryChildren);

        DirectoryCommandParameters parameters = new DirectoryCommandParameters.Builder()
                .commitId(objectId)
                .path(path)
                .recurse(recursive)
                .withSizes(!recursive) //For recursive, omit the sizes because it's more expensive to include them
                .build();
        getCommandFactory(repository).directory(parameters, callback, pageRequest).call();
    }

    @Override
    public void streamFile(@Nonnull Repository repository, @Nonnull String objectId, @Nonnull String path,
                           @Nonnull PageRequest pageRequest, boolean withBlame,
                           @Nonnull FileContentCallback callback) throws ServiceException {
        pageRequest = pageRequest.buildRestrictedPageRequest(maxSourceLines);

        if (withBlame) {
            callback = new AuthorUpdatingFileContentCallback(callback);
        }

        FileCommandParameters parameters = new FileCommandParameters.Builder()
                .annotate(withBlame)
                .commitId(objectId)
                .maxLineLength(maxLineLength)
                .path(path)
                .build();

        Command<Void> command = getCommandFactory(repository).file(parameters, callback, pageRequest);
        try {
            command.call();
        } catch (CommandException e) {
            Throwable rootCause = Throwables.getRootCause(e);
            log.error("{}: Error streaming {}:{}", repository, objectId, path, rootCause);

            throw unexpectedError(rootCause);
        }
    }

    //This is intentionally not paged, as it is intended to retrieve the raw contents of any file
    @Override
    public void streamFile(@Nonnull Repository repository, @Nonnull String objectId, @Nonnull String path,
                           @Nonnull TypeAwareOutputSupplier supplier) throws ServiceException {
        RawFileCommandParameters parameters = new RawFileCommandParameters.Builder()
                .commitId(objectId)
                .path(path)
                .build();
        getCommandFactory(repository).rawFile(parameters, supplier).call();
    }

    private ServerException unexpectedError(Throwable cause) {
        throw new ServerException(i18nService.createKeyedMessage("bitbucket.service.unexpected"), cause);
    }

    @SuppressWarnings("unchecked")
    private List<Blame> updateAuthors(List<? extends Blame> blames) {
        if (CollectionUtils.isEmpty(blames)) {
            return (List) blames;
        }

        Set<String> emailAddresses = Chainable.chain(blames)
                .transform(Blame::getAuthor)
                .transform(Person::getEmailAddress)
                .filter(StringUtils::isNotBlank)
                .toSet();

        final Map<String, ApplicationUser> usersByEmail = userService.mapUsersByEmail(emailAddresses);
        if (!usersByEmail.isEmpty()) {
            blames = Chainable.chain(blames)
                    .transform(blame -> {
                        ApplicationUser user = usersByEmail.get(blame.getAuthor().getEmailAddress());
                        if (user != null) {
                            return new SimpleBlame.Builder(blame).author(user).build();
                        } else {
                            return blame;
                        }
                    })
                    .toList();
        }
        return (List) blames;
    }

    private class AuthorUpdatingFileContentCallback implements FileContentCallback {

        private final FileContentCallback delegate;

        AuthorUpdatingFileContentCallback(FileContentCallback callback) {
            delegate = callback;
        }

        @Override
        public void offerBlame(@Nonnull List<Blame> blames) throws IOException {
            delegate.offerBlame(updateAuthors(blames));
        }

        @Override
        public void onBinary() throws IOException {
            delegate.onBinary();
        }

        @Override
        public void onEnd(@Nonnull FileSummary summary) throws IOException {
            delegate.onEnd(summary);
        }

        @Override
        public void onStart(@Nonnull FileContext context) throws IOException {
            delegate.onStart(context);
        }

        @Override
        public boolean onLine(int lineNumber, String line, boolean truncated) throws IOException {
            return delegate.onLine(lineNumber, line, truncated);
        }
    }
}
