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

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.InternalUpstreamService;
import com.atlassian.bitbucket.internal.mirroring.mirror.auth.DelegatedAuthenticationHandlerBase;
import com.atlassian.bitbucket.internal.mirroring.mirror.auth.SimpleUser;
import com.atlassian.bitbucket.internal.mirroring.mirror.client.InternalUpstreamClientFactory;
import com.atlassian.bitbucket.internal.mirroring.user.ApplicationUserWithPermissions;
import com.atlassian.bitbucket.internal.mirroring.user.SimpleApplicationUserWithPermissions;
import com.atlassian.bitbucket.mirroring.mirror.IntegrationState;
import com.atlassian.bitbucket.mirroring.mirror.UpstreamServer;
import com.atlassian.bitbucket.permission.EffectivePermission;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.permission.SimpleEffectiveGlobalPermission;
import com.atlassian.bitbucket.permission.SimpleEffectiveRepositoryPermission;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.repository.RepositoryService;
import com.atlassian.bitbucket.scm.http.RepositoryUrlFragment;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.user.DetailedUser;
import com.atlassian.bitbucket.user.SecurityService;
import com.atlassian.bitbucket.user.UserAdminService;
import com.atlassian.bitbucket.util.DevModeUtils;
import com.atlassian.crowd.embedded.api.DirectoryType;
import com.atlassian.crowd.exception.DirectoryNotFoundException;
import com.atlassian.crowd.manager.directory.DirectoryManager;
import com.atlassian.sal.api.transaction.TransactionTemplate;
import com.atlassian.util.concurrent.Promise;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;
import org.apache.commons.lang3.StringUtils;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.servlet.http.HttpServletRequest;
import java.util.List;

import static com.atlassian.bitbucket.auth.HttpAuthenticationContext.METHOD_BASIC;
import static com.atlassian.bitbucket.internal.mirroring.mirror.MirrorDescriptionUtils.describe;
import static com.google.common.net.HttpHeaders.AUTHORIZATION;

public class DelegatedHttpAuthenticationHandler extends DelegatedAuthenticationHandlerBase implements HttpAuthenticationHandler {

    private final DirectoryManager directoryManager;
    private final RepositoryService repositoryService;
    private final SecurityService securityService;
    private final UserAdminService userAdminService;

    public DelegatedHttpAuthenticationHandler(InternalUpstreamClientFactory clientFactory, DirectoryManager directoryManager,
                                              I18nService i18nService, InternalUpstreamService upstreamService,
                                              RepositoryService repositoryService, SecurityService securityService,
                                              TransactionTemplate transactionTemplate,
                                              UserAdminService userAdminService) {
        super(clientFactory, i18nService, upstreamService, securityService, transactionTemplate);
        this.directoryManager = directoryManager;
        this.repositoryService = repositoryService;
        this.securityService = securityService;
        this.userAdminService = userAdminService;
    }

    @Nullable
    @Override
    public ApplicationUser authenticate(@Nonnull HttpAuthenticationContext context) {
        if (!upstreamService.isMirror()) {
            log.trace("Not delegating HTTP authentication because this is not a mirror");
            return null;
        }

        String username = context.getUsername();
        String password = context.getCredentials() instanceof String ? (String) context.getCredentials() : null;

        if (StringUtils.isBlank(username)) {
            log.trace("Not delegating HTTP authentication because username not provided");
            return null;
        }

        if (StringUtils.isBlank(password)) {
            log.trace("Not delegating HTTP authentication because no password provided or credentials are of an unrecognised kind");
            return null;
        }

        // needed for UPM plugin operations
        if (isDevMode() && isLocalEmbeddedCrowdUser(context.getUsername())) {
            log.trace("Not delegating HTTP authentication because we are in dev mode and user {} exists locally",
                    context.getUsername());
            // if standard authentication is attempted for an internal user in dev mode CAPTCHA won't be applied because
            // CAPTCHA is disabled in mirroring mode
            return null;
        }

        UpstreamServer upstream = upstreamService.get();
        if (upstream != null && upstream.getState() == IntegrationState.UNKNOWN) {
            log.info("Not delegating HTTP authentication because this mirror's installation state with respect to the " +
                    "upstream server is currently unknown");
            return null;
        } else if (upstream == null || upstream.getState() != IntegrationState.INSTALLED) {
            log.info("Not delegating HTTP authentication because this mirror is not installed on an " +
                    "upstream server");
            return null;
        }

        log.debug("Delegating HTTP authentication for user {}", context.getUsername());

        switch (upstream.getType()) {
            case BITBUCKET_CLOUD:
                return authenticateCloud(upstream, context);
            case BITBUCKET_SERVER:
                Promise<ApplicationUserWithPermissions> promise = clientFactory.createForServer(upstream)
                        .authenticateForUser(username, password).map(this::localiseResourcePermissions);
                return getWithTimeout(promise);
            default:
                log.info("Delegation failed due to unknown upstream type. Upstream: {}", describe(upstream));
                return null;
        }
    }

    @Override
    public void validateAuthentication(@Nonnull HttpAuthenticationContext context) {
        // nothing to do here
    }

    @VisibleForTesting
    protected boolean isDevMode() {
        return DevModeUtils.isEnabled();
    }

    private ApplicationUserWithPermissions authenticateCloud(UpstreamServer upstreamServer,
                                                             HttpAuthenticationContext context) {
        if (!METHOD_BASIC.equals(context.getMethod())) {
            log.trace("Authentication header is not of type {}. Skipping delegation.", METHOD_BASIC);
            return null;
        }

        HttpServletRequest request = context.getRequest();
        RepositoryUrlFragment url = request.getPathInfo() == null ? null :
                RepositoryUrlFragment.fromNamespacedPathInfo(request.getPathInfo());
        
        if (url != null) {
            Repository repository = securityService.withPermission(Permission.REPO_READ, "Delegated HTTP authentication")
                    .call(() -> repositoryService.getBySlug(
                            url.getProjectNamespace(), url.getProjectKey(), url.getRepositorySlug()));

            boolean hasAccess = clientFactory.createForCloud(upstreamServer)
                    .canReadRepository(url.getRepositorySlug(), request.getHeader(AUTHORIZATION))
                    .claim();

            if (hasAccess) {
                log.trace("Delegated authentication successful.");
                List<EffectivePermission> permissions = Lists.newArrayList(
                        new SimpleEffectiveGlobalPermission(Permission.LICENSED_USER));

                if (repository != null) {
                    log.trace("Granting access to repository '{}'", repository.getName());
                    permissions.add(new SimpleEffectiveRepositoryPermission(repository.getId(), Permission.REPO_READ));
                }

                return new SimpleApplicationUserWithPermissions(new SimpleUser(context.getUsername()), permissions);
            }
        }

        return null;
    }

    private boolean isLocalEmbeddedCrowdUser(final String username) {
        return securityService.withPermission(Permission.SYS_ADMIN, "Checking directory type during delegated authentication")
                .call(() -> {
                    DetailedUser user = userAdminService.getUserDetails(username);
                    if (user != null) {
                        String directoryName = user.getDirectoryName();
                        if (directoryName != null) {
                            try {
                                return directoryManager.findDirectoryByName(directoryName).getType() == DirectoryType.INTERNAL;
                            } catch (DirectoryNotFoundException e) {
                                // fall through
                            }
                        }
                    }
                    return false;
                });
    }
}
