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

import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.internal.scm.git.lfs.GitLfsConstants;
import com.atlassian.bitbucket.internal.scm.git.lfs.embedded.EmbeddedStoreService;
import com.atlassian.bitbucket.internal.scm.git.lfs.embedded.ObjectStreamingOutput;
import com.atlassian.bitbucket.internal.scm.git.lfs.embedded.ReadObjectRequest;
import com.atlassian.bitbucket.repository.NoSuchRepositoryException;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.repository.RepositoryService;
import com.atlassian.bitbucket.rest.util.ResourcePatterns;
import com.atlassian.bitbucket.rest.util.ResponseFactory;
import com.atlassian.http.mime.HostileExtensionDetector;
import com.atlassian.plugins.rest.common.security.AnonymousAllowed;
import com.sun.jersey.spi.resource.Singleton;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.io.InputStream;
import java.util.Optional;

import static org.apache.commons.lang3.StringUtils.trimToNull;

/**
 * Implements the REST API providing LFS object upload and download capabilities for the
 * embedded object store.
 */
@SuppressWarnings("PathAnnotation")
@AnonymousAllowed
@Path("/storage")
@Singleton
public class GitLfsStorageResource {

    //This is separated out so that the namespace does not capture the optional '/'
    private static final String IGNORED_SEPARATOR = "{ignored: /?}";
    private static final Logger log = LoggerFactory.getLogger(GitLfsStorageResource.class);
    private static final String OPTIONAL_NAMESPACE = "{namespace : (.*?)?}";
    private static final String OID_URI = "/" + OPTIONAL_NAMESPACE + IGNORED_SEPARATOR +
            ResourcePatterns.PROJECT_KEY_PATH + "/" + ResourcePatterns.REPOSITORY_SLUG_PATH + "/{oid}";
    private final HostileExtensionDetector hostileExtensionDetector;
    private final I18nService i18nService;
    private final RepositoryService repositoryService;
    private final EmbeddedStoreService storeService;

    public GitLfsStorageResource(EmbeddedStoreService storeService,
                                 HostileExtensionDetector hostileExtensionDetector,
                                 I18nService i18nService, RepositoryService repositoryService) {
        this.storeService = storeService;
        this.hostileExtensionDetector = hostileExtensionDetector;
        this.i18nService = i18nService;
        this.repositoryService = repositoryService;
    }

    @GET
    @Produces(MediaType.APPLICATION_OCTET_STREAM)
    @Path(OID_URI)
    public Response getObject(@PathParam("namespace") String namespace,
                              @PathParam(ResourcePatterns.PROJECT_KEY) String projectKey,
                              @PathParam(ResourcePatterns.REPOSITORY_SLUG) String repositorySlug,
                              @PathParam("oid") String oid,
                              @HeaderParam("Range") String rangeHeader,
                              @QueryParam("response-content-disposition") String responseContentDisposition,
                              @QueryParam("response-content-type") String responseContentType) {

        Repository repository = repositoryService.getBySlug(trimToNull(namespace), projectKey, repositorySlug);

        if (repository == null) {
            throw new NoSuchRepositoryException(i18nService.createKeyedMessage("bitbucket.rest.nosuchrepo", projectKey,
                    repositorySlug), null);
        }

        Optional<HttpRange> parsedRange = HttpRange.parseRange(rangeHeader);
        if (rangeHeader != null && !parsedRange.isPresent()) {
            log.debug("HTTP range format invalid or unsupported");
        }

        ObjectStreamingOutput stream = storeService.readObject(
                new ReadObjectRequest.Builder(repository, oid)
                        .range(parsedRange.orElse(null))
                        .build());

        // Storage services aren't required to support the HttpRange even if it is passed. They can indicate no
        // support by not including a HttpRange in the ObjectStreamingOutput object returned. A Http status 200
        // is then returned to the client who then knows to expect the entire object will be streamed
        Response.ResponseBuilder builder = stream.range()
                .map(streamRange -> ResponseFactory
                        .status(206)
                        .entity(stream)
                        .header("Content-Range", String.format("bytes %s/*", streamRange))
                ).orElse(ResponseFactory.ok().entity(stream));

        if (StringUtils.isNotEmpty(responseContentDisposition)) {
            // Permits the client to request a specific filename to be used when the file is saved.
            // By default, the filename will be the OID.
            builder.header("Content-Disposition", responseContentDisposition);
        }
        if (StringUtils.isNotEmpty(responseContentType) &&
                !hostileExtensionDetector.isExecutableContentType(responseContentType)) {
            // Permits the client to request that the file is served with a specific content type.
            // By default, the content type will be "application/octet-stream".
            builder.header("Content-Type", responseContentType);
        }
        return builder.build();
    }

    @PUT
    @Consumes(MediaType.APPLICATION_OCTET_STREAM)
    @Produces({GitLfsConstants.CONTENT_TYPE})
    @Path(OID_URI)
    public Response putObject(@Context Repository repository, @PathParam("oid") String oid,
                              InputStream is) throws IOException {
        storeService.writeObject(repository, oid, is);

        return ResponseFactory.ok().build();
    }
}
