package com.atlassian.stash.internal.web.attach;

import com.atlassian.bitbucket.ServiceException;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.io.ContentDetectionUtils;
import com.atlassian.bitbucket.nav.NavBuilder;
import com.atlassian.bitbucket.permission.PermissionService;
import com.atlassian.bitbucket.repository.RefService;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.repository.RepositoryService;
import com.atlassian.fugue.Either;
import com.atlassian.stash.internal.attach.AttachmentService;
import com.atlassian.stash.internal.attach.AttachmentSupplier;
import com.atlassian.stash.internal.project.InternalProjectService;
import com.atlassian.stash.internal.scm.InternalScmService;
import com.atlassian.stash.internal.web.auth.IgnoresXsrf;
import com.atlassian.stash.internal.web.util.CachePolicies;
import com.atlassian.stash.internal.web.util.DownloadHeaderHelper;
import com.atlassian.stash.internal.web.util.RepositoryControllerSupport;
import com.google.common.base.Function;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.io.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.util.UriUtils;

import javax.servlet.http.HttpServletResponse;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import static java.util.Objects.requireNonNull;

/**
 * @since 3.1
 */
@Controller
@RequestMapping("/projects/{projectKey}/repos/{repoSlug}/attachments")
public class AttachmentController extends RepositoryControllerSupport {

    private final AttachmentService attachmentService;
    private final DownloadHeaderHelper downloadHeaderHelper;
    private final NavBuilder navBuilder;

    @Autowired
    public AttachmentController(I18nService i18nService, RefService refService,
                                PermissionService permissionService, InternalProjectService projectService,
                                RepositoryService repositoryService, InternalScmService scmService,
                                AttachmentService attachmentService, DownloadHeaderHelper downloadHeaderHelper,
                                NavBuilder navBuilder) {
        super(i18nService, refService, permissionService, projectService, repositoryService, scmService);

        this.attachmentService = attachmentService;
        this.downloadHeaderHelper = downloadHeaderHelper;
        this.navBuilder = navBuilder;
    }

    @RequestMapping(method = RequestMethod.DELETE, value = "/{token}/{attachmentName}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void delete(@PathVariable("projectKey") String projectKey, @PathVariable("repoSlug") String repoSlug,
                       @PathVariable("token") String token, @PathVariable("attachmentName") String attachmentName) {
        Repository repository = getRepository(projectKey, repoSlug);
        attachmentName = decode(attachmentName); // HACK: Tuckey doesn't decode parameters you rewrite which is wrong

        attachmentService.delete(repository, toId(token, attachmentName));
    }

    @RequestMapping(method = RequestMethod.GET, value = "/{token}/{attachmentName}")
    public void retrieve(@PathVariable("projectKey") String projectKey, @PathVariable("repoSlug") String repoSlug,
                         @PathVariable("token") String token, @PathVariable("attachmentName") String encodedAttachmentName,
                         @RequestHeader(value = "User-Agent", required = false) String userAgent,
                         HttpServletResponse response) throws IOException {
        Repository repository = getRepository(projectKey, repoSlug);
        String attachmentName = decode(encodedAttachmentName); // HACK: Tuckey doesn't decode parameters you rewrite which is wrong
        AttachmentSupplier supplier = attachmentService.read(repository, toId(token, attachmentName));

        ByteSource source = new ByteSource() {
            @Override
            public InputStream openStream() throws IOException {
                BufferedInputStream stream = new BufferedInputStream(supplier.open());

                String contentType = MoreObjects.firstNonNull(
                        ContentDetectionUtils.detectContentType(stream, supplier.getName()),
                        MediaType.APPLICATION_OCTET_STREAM_VALUE);

                downloadHeaderHelper.setDownloadHeaders(response, attachmentName, contentType, userAgent);

                response.setHeader("Content-Length", Long.toString(supplier.getSize()));
                CachePolicies.cacheForFourWeeks(response);

                return stream;
            }
        };

        source.copyTo(response.getOutputStream());
        response.flushBuffer();
    }

    @IgnoresXsrf //Attachments are uploaded from a context which does not have access to a normal form or XSRF token
    @RequestMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE,
            method = RequestMethod.POST)
    public ResponseEntity<Map<String, Object>> save(@PathVariable("projectKey") String projectKey,
                                                    @PathVariable("repoSlug") String repoSlug,
                                                    AttachmentForm attachments) {
        final Repository repository = getRepository(projectKey, repoSlug);
        List<Either<ServiceException, String>> ids = attachmentService.saveAll(repository, attachments.toSuppliers());

        Function<Either<ServiceException, String>, RestAttachment> transform =
                either -> either.fold(RestAttachment::new, id -> createRestAttachment(id, repository));

        //The body (RestAttachments) will be converted to JSON using an HttpMessageConverter. Using ResponseEntity
        //instead of @ResponseBody allows us to set the status code
        return new ResponseEntity<>(
                new RestAttachments(Lists.transform(ids, transform)),
                HttpStatus.CREATED);
    }

    //token/attachmentName is a bit of a cross-domain trespass with the AttachmentService, but mapping URLs with
    //slashes in them is not really well-supported by SpringMVC
    private static String toId(String token, String attachmentName) {
        return token + "/" + attachmentName;
    }

    private RestAttachment createRestAttachment(String id, Repository repository) {
        String url = navBuilder.repo(repository).attachments().attachment(id).buildAbsolute();
        AttachmentUri attachmentUri = new AttachmentUri(requireNonNull(repository.getId(), "repository.id"), id);
        return new RestAttachment(id, url, attachmentUri.toString());
    }

    private String decode(String str) {
        try {
            return UriUtils.decode(str, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            return str;
        }
    }

    public static class RestAttachment extends LinkedHashMap<String, Object> {

        public RestAttachment(String id, String url, String attachmentUri) {
            put("id", id);
            put("url", url);
            put("links", createLinks(url, attachmentUri));
        }

        public RestAttachment(ServiceException e) {
            put("errors", Collections.singletonList(
                    ImmutableMap.of("message", e.getLocalizedMessage(),
                            "exceptionName", e.getClass().getCanonicalName())));
        }

        private static Map<String, ?> createLink(String href) {
            return ImmutableMap.of("href", href);
        }

        private Map<String, ?> createLinks(String url, String attachmentUri) {
            return ImmutableMap.of(
                    "self", createLink(url),
                    "attachment", createLink(attachmentUri));
        }
    }

    public static class RestAttachments extends LinkedHashMap<String, Object> {

        public RestAttachments(List<RestAttachment> attachments) {
            put("attachments", attachments);
        }
    }
}