package com.atlassian.bitbucket.scm.throttle;

import com.atlassian.bitbucket.help.HelpPathService;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.scm.ScmRequest;
import com.atlassian.bitbucket.scm.http.HttpScmRequest;
import com.atlassian.bitbucket.scm.ssh.SshScmRequest;
import com.atlassian.bitbucket.throttle.ThrottleService;
import com.atlassian.bitbucket.throttle.Ticket;
import com.google.common.annotations.VisibleForTesting;

import javax.annotation.Nonnull;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

import static com.google.common.base.Preconditions.checkNotNull;

/**
 * Helper for SCM implementation providers to decorate {@link HttpScmRequest} and {@link SshScmRequest} instances with
 * {@link ThrottleService throttling}.
 */
public class ThrottledScmRequestFactory {

    @VisibleForTesting
    // NOTE this constant is duplicated in com.atlassian.stash.internal.annotation.Throttled.SCM_HOSTING there are long
    // term plans to consolidate these constants.
    static final String RESOURCE_NAME = "scm-hosting";

    private final ThrottleService throttleService;

    /**
     * @deprecated in 4.9 for removal in 5.0. Use {@link #ThrottledScmRequestFactory(ThrottleService)} instead.
     */
    @Deprecated
    public ThrottledScmRequestFactory(ThrottleService throttleService, I18nService i18nService,
                                      HelpPathService helpPathService) {
        this(throttleService);
    }

    public ThrottledScmRequestFactory(ThrottleService throttleService) {
        this.throttleService = checkNotNull(throttleService, "throttleService");
    }

    @Nonnull
    public SshScmRequest throttled(@Nonnull SshScmRequest request) {
        return throttled(checkNotNull(request, "request"), SshScmRequest.class);
    }

    @Nonnull
    public HttpScmRequest throttled(@Nonnull HttpScmRequest request) {
        return throttled(checkNotNull(request, "request"), HttpScmRequest.class);
    }

    private <T extends ScmRequest> T throttled(@Nonnull T request, Class<T> clazz) {
        return clazz.cast(Proxy.newProxyInstance(request.getClass().getClassLoader(),
                new Class<?>[] {clazz},
                new ThrottlingInvocationHandler(request)));
    }

    private class ThrottlingInvocationHandler implements InvocationHandler {
        private final ScmRequest target;

        private ThrottlingInvocationHandler(ScmRequest target) {
            this.target = target;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Ticket ticket = null;
            try {
                if ("handleRequest".equals(method.getName())) {
                    ticket = throttleService.acquireTicket(RESOURCE_NAME);
                }

                return method.invoke(target, args);
            } catch (InvocationTargetException e) {
                //Method.invoke _always_ wraps exceptions, even those that are acceptable to the method's throws
                //clause, in InvocationTargetException. This unwraps that
                throw e.getTargetException();
            } finally {
                if (ticket != null)  {
                    ticket.close();
                }
            }
        }
    }
}
