package com.atlassian.stash.internal.rest.content;

import com.atlassian.bitbucket.content.*;
import com.atlassian.bitbucket.rest.content.*;
import com.atlassian.bitbucket.rest.enrich.AvatarEnricher;
import com.atlassian.bitbucket.rest.enrich.LinkEnricher;
import com.atlassian.bitbucket.rest.RestErrors;
import com.atlassian.bitbucket.rest.util.*;
import com.atlassian.plugins.rest.common.security.AnonymousAllowed;
import com.atlassian.bitbucket.avatar.AvatarRequest;
import com.atlassian.bitbucket.repository.RefService;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.util.PageRequest;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.sun.jersey.spi.container.ContainerRequest;
import com.sun.jersey.spi.resource.Singleton;

import javax.annotation.Nonnull;
import javax.ws.rs.*;
import javax.ws.rs.Path;
import javax.ws.rs.core.*;
import java.io.IOException;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

import static com.atlassian.bitbucket.rest.util.CachePolicies.getCacheControlObjectId;
import static com.atlassian.bitbucket.rest.util.RestDocUtils.pageOf;

@AnonymousAllowed
@Consumes({MediaType.APPLICATION_JSON})
@Path(ResourcePatterns.REPOSITORY_URI + "/browse")
@Produces({RestUtils.APPLICATION_JSON_UTF8})
@Singleton
public class ContentResource {

    private final AvatarEnricher avatarEnricher;
    private final ContentService contentService;
    private final LinkEnricher linkEnricher;
    private final RefService refService;

    public ContentResource(AvatarEnricher avatarEnricher, ContentService contentService,
                           LinkEnricher linkEnricher, RefService refService) {
        this.avatarEnricher = avatarEnricher;
        this.contentService = contentService;
        this.linkEnricher = linkEnricher;
        this.refService = refService;
    }

    /**
     * Retrieve a page of content for a file path at a specified revision.
     * <p>
     * The authenticated user must have <strong>REPO_READ</strong> permission for the specified repository to call this
     * resource.
     *
     * @param at        the commit ID or ref to retrieve the content for.
     * @param type      if true only the type will be returned for the file path instead of the contents.
     * @param blame     if present the blame will be returned for the file as well.
     * @param noContent if present and used with blame only the blame is retrieved instead of the contents.
     * @param path      the file path to retrieve content from
     * @response.representation.200.mediaType application/json
     * @response.representation.200.qname page
     * @response.representation.200.example {@link #EXAMPLE}
     * @response.representation.200.doc A page of contents from a file.
     * @response.representation.400.mediaType application/json
     * @response.representation.400.qname errors
     * @response.representation.400.example {@link RestErrors#EXAMPLE}
     * @response.representation.400.doc The path or until parameters were not supplied.
     * @response.representation.401.mediaType application/json
     * @response.representation.401.qname errors
     * @response.representation.401.example {@link RestErrors#EXAMPLE}
     * @response.representation.401.doc The currently authenticated user has insufficient permissions
     * to view the repository.
     * @response.representation.404.mediaType application/json
     * @response.representation.404.qname errors
     * @response.representation.404.example {@link RestErrors#EXAMPLE}
     * @response.representation.404.doc The repository does not exist.
     */
    @GET
    @Path("{path:.*}")
    public Response getContent(@Context Repository repository,
                               @QueryParam("at") @DefaultValue("") String at,
                               @PathParam("path") @DefaultValue("") String path,
                               @QueryParam("type") @DefaultValue("false") boolean type,
                               @QueryParam("blame") String blame,
                               @QueryParam("noContent") String noContent,
                               @Context ContainerRequest request) {
        at = RestUtils.getOrDefaultBranch(refService, repository, at);

        CacheControl cacheControl = getCacheControlObjectId(at);
        ContentTreeNode.Type nodeType = contentService.getType(repository, at, path);
        if (type) {
            return ResponseFactory.ok(new RestContentTreeNode.RestType(nodeType), cacheControl).build();
        }

        PageRequest pageRequest = RestUtils.makePageRequest(request, 500);
        if (nodeType == ContentTreeNode.Type.DIRECTORY) {
            return ResponseFactory.ok(getDirectory(repository, at, path, pageRequest), cacheControl).build();
        }

        // safeguard consumers of the api adding ?blame=false
        // empty string should be true (show blame), ?blame
        boolean showBlame = (blame != null && !"false".equalsIgnoreCase(blame));
        if (showBlame && noContent != null) {
            //This will automatically apply the LinkEnricher; we don't need to do it ourselves
            return ResponseFactory.ok(getBlame(repository, at, path, pageRequest), cacheControl).build();
        }

        AvatarRequest avatarRequest = null;
        if (showBlame) {
            //Because unified content and blame is streamed, we must handle the AvatarRequest directly; the response
            //filter doesn't get to see actual entities on streamed responses (and therefore cannot filter them).
            avatarRequest = RestUtils.makeAvatarRequest(request);
        }

        return ResponseFactory.ok(getFile(repository, at, path, avatarRequest, pageRequest, showBlame), cacheControl).build();
    }

    // This is intentionally undocumented. REST doclets don't handle overloaded method well
    // and copy the documentation from the first method.
    @GET
    public Response getContent(@Context Repository repository,
                               @QueryParam("at") @DefaultValue("") String objectId,
                               @QueryParam("type") @DefaultValue("false") boolean typeOnly,
                               @QueryParam("blame") String withBlame,
                               @QueryParam("noContent") String noContent,
                               @Context ContainerRequest request) {
        return getContent(repository, objectId, "", typeOnly, withBlame, noContent, request);
    }

    @SuppressWarnings("deprecation")
    private Collection<RestBlame> getBlame(Repository repository, String objectId, String path, PageRequest pageRequest) {
        //Do not return the transform directly--doing so prevents avatar URLs from being correctly set because each
        //time the collection is iterated it is retransformed.
        return ImmutableList.copyOf(
                Collections2.transform(contentService.getBlame(repository, objectId, path, pageRequest),
                        RestBlame.REST_TRANSFORM::apply));
    }

    private RestDirectoryRevision getDirectory(Repository repository, String commitId, String path, PageRequest pageRequest) {
        final List<RestContentTreeNode> nodes = Lists.newArrayList();
        final ContentTreeSummary[] summaryRef = new ContentTreeSummary[1];

        contentService.streamDirectory(repository, commitId, path, false, new AbstractContentTreeCallback() {

            @Override
            public void onEnd(@Nonnull ContentTreeSummary summary) {
                summaryRef[0] = summary;
            }

            @Override
            public boolean onTreeNode(@Nonnull ContentTreeNode node) {
                if (node.getType() == ContentTreeNode.Type.DIRECTORY) {
                    nodes.add(new RestDirectory((Directory) node));
                } else if (node.getType() == ContentTreeNode.Type.SUBMODULE) {
                    nodes.add(new RestSubmodule((Submodule) node));
                } else {
                    nodes.add(new RestFile((File) node));
                }

                return true;
            }
        }, pageRequest);

        ContentTreeSummary summary = summaryRef[0];
        return new RestDirectoryRevision(new RestPath(path), commitId,
                new RestPage<>(
                        summary.getPageRequest().getStart(),
                        summary.getPageRequest().getLimit(),
                        summary.getSize(),
                        summary.isLastPage(),
                        nodes,
                        summary.getNextPageRequest()));
    }

    private StreamingOutput getFile(final Repository repository, final String objectId, final String path,
                                    final AvatarRequest avatarRequest, final PageRequest pageRequest,
                                    final boolean withBlame) {
        Function<Blame, RestBlame> transformer = getBlameTransformer(withBlame, avatarRequest);

        return new JsonStreamingOutput() {

            @Override
            public void write(StatefulJsonWriter writer) throws IOException, WebApplicationException {
                contentService.streamFile(repository, objectId, path, pageRequest, withBlame,
                        new JsonFileContentCallback(writer, new RestPath(path), transformer));
            }
        };
    }

    public static final RestPage<Map<String, Object>> EXAMPLE;

    static {
        Map<String, Object> line = new LinkedHashMap<>();
        line.put("text", "print('hello world')");
        EXAMPLE = pageOf(line);
        EXAMPLE.put("lines", EXAMPLE.get("values"));
        EXAMPLE.remove("values");
    }

    private Function<Blame, RestBlame> getBlameTransformer(boolean withBlame, AvatarRequest avatarRequest) {
        if (!withBlame) {
            //Pass a null transformer if blame isn't requested, because it shouldn't be called anyway
            return null;
        }

        Function<Blame, RestBlame> transformer = RestBlame.REST_TRANSFORM.andThen(blame -> {
            linkEnricher.enrich(blame);
            return blame;
        });
        //Using a different function allows us to perform the null check once rather than on every transform
        if (avatarRequest != null) {
            transformer = transformer.andThen(blame -> {
                avatarEnricher.enrich(blame, avatarRequest);
                return blame;
            });
        }

        return transformer;
    }
}
