package com.atlassian.bitbucket.scm.cache.web;

import com.atlassian.bitbucket.experimental.event.repository.AnalyticsRepositoryCloneEvent;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.request.RequestContext;
import com.atlassian.bitbucket.request.RequestManager;
import com.atlassian.bitbucket.scm.git.protocol.GitPackets;
import com.atlassian.bitbucket.scm.http.HttpScmRequest;
import com.atlassian.bitbucket.scm.http.HttpScmRequestHandler;
import com.atlassian.bitbucket.util.Timer;
import com.atlassian.bitbucket.util.TimerUtils;
import com.atlassian.event.api.EventPublisher;
import com.atlassian.util.contentcache.CacheResult;
import com.atlassian.util.contentcache.ContentCache;
import com.atlassian.util.contentcache.ContentProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.annotation.concurrent.NotThreadSafe;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Date;

/**
 * Implementation of {@link HttpScmRequest} that attempts to stream the response from a {@link ContentCache} instead
 * of delegating it to the underlying SCM. If the request cannot be served out of the cache, a new cache entry will be
 * created for the request.
 */
@NotThreadSafe
public class CachingHttpScmRequest implements HttpScmRequest {
    private static final Logger log = LoggerFactory.getLogger(CachingHttpScmRequest.class);

    private final ContentCache cache;
    private final String cacheKey;
    private final HttpScmRequest delegate;
    private final EventPublisher eventPublisher;
    private final HttpServletRequest request;
    private final HttpScmRequestHandler requestHandler;
    private final RequestManager requestManager;
    private final String service;
    private final HttpServletResponse response;
    private final long ttlMillis;

    public CachingHttpScmRequest(ContentCache cache, String cacheKey, String service, long ttlMillis,
                                 HttpScmRequest delegate, EventPublisher eventPublisher,
                                 HttpScmRequestHandler requestHandler,
                                 RequestManager requestManager,
                                 HttpServletRequest request, HttpServletResponse response) {
        this.cache = cache;
        this.cacheKey = cacheKey;
        this.delegate = delegate;
        this.eventPublisher = eventPublisher;
        this.request = request;
        this.requestHandler = requestHandler;
        this.requestManager = requestManager;
        this.response = response;
        this.service = service;
        this.ttlMillis = ttlMillis;
    }

    @Nonnull
    @Override
    public Repository getRepository() {
        return delegate.getRepository();
    }

    @Override
    public void handleRequest() throws IOException {
        try (Timer ignored = TimerUtils.start("scm-cache: upload-pack-request")) {
            OutputStream os = response.getOutputStream();
            // handle service advertisement externally from the cache so we can reuse the same cached values for HTTP
            // and SSH (service advertisement is not done for SSH)
            if (service != null) {
                os.write(GitPackets.create("# service=" + service + "\n"));
                os.write(GitPackets.getFlush());
            }

            CacheResult cacheResult = cache.stream(cacheKey, os, new ContentProvider() {
                @Override
                public Date getExpiry() {
                    return new Date(System.currentTimeMillis() + ttlMillis);
                }

                @Override
                public void apply(OutputStream cacheOutputStream) throws IOException {
                    if (service != null) {
                        cacheOutputStream = new ServiceAdvertisementStrippingOutputStream(cacheOutputStream);
                    }

                    // pass through the http request to the 'real' request handler provided by the scm-git plugin, but
                    // redirect all output to the cache outputstream
                    DelegatingHttpServletResponse delegatingResponse =
                            new DelegatingHttpServletResponse(response, cacheOutputStream);

                    HttpScmRequest delegate = requestHandler.create(request, delegatingResponse).orElse(null);
                    if (delegate == null) {
                        cache.remove(cacheKey);
                        throw new IllegalStateException("Could not create delegate HTTP request handler");
                    }
                    delegate.handleRequest();
                    int status = delegatingResponse.getStatus();
                    if (status < 200 || status >= 300) {
                        log.debug("Invalidating cache entry because status code is {}", status);
                        cache.remove(cacheKey);
                    }
                }
            });

            try {
                RequestContext requestContext = requestManager.getRequestContext();
                if (requestContext != null) {
                    // Regardless of whether scm-cache is enabled a RequestContext is always available to
                    // HttpUploadPack. HttpUploadPack#onSuccess will not be called for a cache:hit so the events
                    // are published here.
                    if (cacheResult == CacheResult.HIT) {
                        String label = cacheKey.substring(0, cacheKey.indexOf('|'));
                        requestContext.addLabel(label);
                        if ("clone".equals(label)) {
                            //Published after request completion so that bytes read/written and duration are available
                            requestContext.addCleanupCallback(() ->
                                    eventPublisher.publish(
                                            new AnalyticsRepositoryCloneEvent(this, getRepository(), requestContext))
                            );
                        }
                    }
                    requestContext.addLabel(cacheResult.toString());
                }
            } catch (Exception e) {
                // this can happen when the SCM Cache plugin is disabled/uninstalled while a request is being handled
                log.debug("Could not append cache hit/miss information to request labels", e);
            }
        }
    }

    @Override
    public boolean isWrite() {
        return delegate.isWrite();
    }

    @Override
    public void sendError(@Nonnull String summary, @Nonnull String detailMessage) throws IOException {
        delegate.sendError(summary, detailMessage);
    }
}
