package com.atlassian.bitbucket.internal.mirroring.mirror.auth.http;

import com.atlassian.bitbucket.auth.ExpiredAuthenticationException;
import com.atlassian.bitbucket.auth.HttpAuthenticationContext;
import com.atlassian.bitbucket.auth.HttpAuthenticationHandler;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.internal.mirroring.mirror.InternalUpstreamServer;
import com.atlassian.bitbucket.internal.mirroring.mirror.InternalUpstreamService;
import com.atlassian.bitbucket.internal.mirroring.mirror.MirrorAuthenticationContext;
import com.atlassian.bitbucket.internal.mirroring.mirror.UpstreamUserHelper;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.user.ServiceUser;
import com.atlassian.sal.api.transaction.TransactionTemplate;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.servlet.http.HttpServletRequest;

import static com.atlassian.bitbucket.internal.mirroring.mirror.MirrorConstants.ATTR_JWT_HIGHEST_PERM;
import static com.atlassian.bitbucket.internal.mirroring.mirror.MirrorConstants.PARAM_HIGHEST_PERM;
import static java.util.Optional.of;

/**
 * Detects requests originating from an upstream server (authenticated by JWT) and handles authentication for
 * the corresponding service user.
 */
public class UpstreamServerUserAuthenticationHandler implements HttpAuthenticationHandler {
    private static final Logger log = LoggerFactory.getLogger(UpstreamServerUserAuthenticationHandler.class);

    private final MirrorAuthenticationContext mirrorAuthenticationContext;
    private final I18nService i18nService;
    private final InternalUpstreamService upstreamService;
    private final TransactionTemplate transactionTemplate;
    private final UpstreamUserHelper upstreamUserHelper;

    public UpstreamServerUserAuthenticationHandler(InternalUpstreamService upstreamService,
                                                   TransactionTemplate transactionTemplate, I18nService i18nService,
                                                   UpstreamUserHelper upstreamUserHelper,
                                                   @Qualifier("defaultMirrorAuthenticationContext") MirrorAuthenticationContext mirrorAuthenticationContext) {
        this.i18nService = i18nService;
        this.mirrorAuthenticationContext = mirrorAuthenticationContext;
        this.upstreamService = upstreamService;
        this.transactionTemplate = transactionTemplate;
        this.upstreamUserHelper = upstreamUserHelper;
    }

    /**
     * Upstream service users can only be authenticated via JWT and since JWT signing is a per-request action it makes
     * no sense if this persists across requests through sessions. So if we detect an upstream user is currently
     * authenticated we need to invalidate their login by throwing an {@link ExpiredAuthenticationException}
     */
    @Override
    public void validateAuthentication(@Nonnull HttpAuthenticationContext context) {
        mirrorAuthenticationContext.getCurrentUpstreamUser().ifPresent(upstreamUser -> {
            String jwtIssuerId = getJwtIssuerId(context);
            if (jwtIssuerId != null) {
                // the current request is signed with a JWT token. Time to re-authenticate
                log.debug("Re-authenticating upstream user {} because the request is JWT signed", upstreamUser.getName());
                throw new ExpiredAuthenticationException(
                        i18nService.createKeyedMessage("bitbucket.mirroring.authentication.jwt.session.expired"));
            }
        });
    }

    @Nullable
    @Override
    public ApplicationUser authenticate(@Nonnull HttpAuthenticationContext authenticationContext) {
        if (!upstreamService.isMirror()) {
            return null;
        }

        if (!HttpAuthenticationContext.METHOD_TOKEN.equals(authenticationContext.getMethod())) {
            return null;
        }

        String jwtIssuerId = getJwtIssuerId(authenticationContext);
        if (StringUtils.isEmpty(jwtIssuerId)) {
            return null;
        }

        return transactionTemplate.execute(() -> {
            InternalUpstreamServer upstream = upstreamService.get();
            if (upstream == null) {
                // we aren't installed on an upstream server yet - how the JWT request got through
                // well that's anybody's guess
                return null;
            }

            if (!of(jwtIssuerId).equals(upstream.getIssuerId())) {
                return null;
            }

            ServiceUser upstreamUser = upstreamUserHelper.getUserForUpstream(upstream.getId());
            if (upstreamUser == null || !upstreamUser.isActive()) {
                return null;
            }

            // if the upstream has requested a higher permission for this request, store it as a request attribute to be
            // picked up by the MirrorAuthenticationContext
            HttpServletRequest request = authenticationContext.getRequest();
            request.setAttribute(ATTR_JWT_HIGHEST_PERM, valueOf(request.getParameter(PARAM_HIGHEST_PERM)));

            return upstreamUser;
        });
    }

    private String getJwtIssuerId(@Nonnull HttpAuthenticationContext authenticationContext) {
        return (String) authenticationContext.getRequest().getAttribute("Plugin-Key");
    }

    private Permission valueOf(String permission) {
        if (StringUtils.isEmpty(permission)) {
            return null;
        }
        try {
            return Permission.valueOf(permission);
        } catch (IllegalArgumentException e) {
            return null;
        }
    }
}
