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

import com.atlassian.bitbucket.auth.AuthenticationException;
import com.atlassian.bitbucket.auth.AuthenticationSystemException;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.internal.mirroring.mirror.InternalUpstreamService;
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.UpstreamServer;
import com.atlassian.bitbucket.permission.EffectivePermission;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.user.EscalatedSecurityContext;
import com.atlassian.bitbucket.user.SecurityService;
import com.atlassian.bitbucket.util.Chainable;
import com.atlassian.fugue.Pair;
import com.atlassian.sal.api.transaction.TransactionTemplate;
import com.atlassian.util.concurrent.Promise;
import com.google.common.base.Throwables;
import com.google.common.collect.Iterables;
import org.codehaus.jackson.JsonFactory;
import org.codehaus.jackson.map.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeoutException;
import java.util.function.Function;

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

public abstract class DelegatedAuthenticationHandlerBase {
    private static final Pair<Map<String, Integer>, Map<String, Integer>> EMPTY_PAIR =
            Pair.pair(Collections.emptyMap(), Collections.emptyMap());

    protected final Logger log = LoggerFactory.getLogger(getClass());

    protected final InternalUpstreamClientFactory clientFactory;
    protected final I18nService i18nService;
    protected final JsonFactory jsonFactory;
    protected final InternalUpstreamService upstreamService;
    protected final TransactionTemplate transactionTemplate;
    protected final EscalatedSecurityContext withRead;

    public DelegatedAuthenticationHandlerBase(InternalUpstreamClientFactory clientFactory, I18nService i18nService,
                                              InternalUpstreamService upstreamService,
                                              SecurityService securityService,
                                              TransactionTemplate transactionTemplate) {
        this.clientFactory = clientFactory;
        this.i18nService = i18nService;
        this.jsonFactory = new ObjectMapper().getJsonFactory();
        this.upstreamService = upstreamService;
        this.transactionTemplate = transactionTemplate;

        withRead = securityService.withPermission(Permission.PROJECT_READ, "for mapping remote projects and " +
                "repositories to local versions");
    }

    protected ApplicationUserWithPermissions localiseResourcePermissions(final ApplicationUserWithPermissions user) {
        checkNotNull(user, "user");
        return withRead.call(() -> {
            Iterable<EffectivePermission> effectivePermissions = user.getEffectivePermissions();

            if (!Iterables.isEmpty(effectivePermissions)) {

                ResourceIdCollectingPermissionVisitor resourceIdCollectingVisitor = new ResourceIdCollectingPermissionVisitor();

                for (EffectivePermission permission : effectivePermissions) {
                    permission.accept(resourceIdCollectingVisitor);
                }

                // left: projectId lookup, right: repositoryId lookup
                Pair<Map<String, Integer>, Map<String, Integer>> resourceIdLookups = lookupLocalResourceIds(
                        resourceIdCollectingVisitor.getProjectIds(),
                        resourceIdCollectingVisitor.getRepositoryIds());

                effectivePermissions = Chainable.chain(effectivePermissions)
                        .transform(localiseResourcePermissions(resourceIdLookups.left(), resourceIdLookups.right()))
                        .filter(Objects::nonNull)
                        .toList();
            }

            return new SimpleApplicationUserWithPermissions(user, effectivePermissions);
        });
    }

    protected ApplicationUserWithPermissions getWithTimeout(Promise<ApplicationUserWithPermissions> promise) {
        try {
            return promise.claim();
        } catch (Exception e) {
            try {
                throw Throwables.getRootCause(e);
            } catch (AuthenticationException e2) {
                throw e2;
            } catch (ConnectException e2) {
                log.info("Could not delegate user authentication because the upstream server is not reachable");
                throw throwUnexpectedDelegationFailure(null);
            } catch (TimeoutException | SocketTimeoutException e2) {
                log.info("Timed out delegating user authentication to the upstream server");
                throw throwUnexpectedDelegationFailure(null);
            } catch (Exception e2) {
                log.warn("An unexpected error occurred while delegating authentication for user", e2);
                throw throwUnexpectedDelegationFailure(e2);
            } catch (Throwable t) {
                throw Throwables.propagate(t);
            }
        }
    }

    private AuthenticationSystemException throwUnexpectedDelegationFailure(Throwable cause) {
        throw new AuthenticationSystemException(
                i18nService.createKeyedMessage("bitbucket.mirroring.authentication.delegation.failure.unexpected"), cause);
    }

    private Function<EffectivePermission, EffectivePermission> localiseResourcePermissions(
            Map<String, Integer> projectIdLookup, Map<String, Integer> repositoryIdLookup) {

        final LocalisingPermissionVisitor localisingVisitor = new LocalisingPermissionVisitor(projectIdLookup, repositoryIdLookup);

        return effectivePermission -> effectivePermission.accept(localisingVisitor);
    }

    private Pair<Map<String, Integer>, Map<String, Integer>> lookupLocalResourceIds(final Set<String> remoteProjectIds,
                                                                                    final Set<String> remoteRepositoryIds) {
        final UpstreamServer upstreamServer = upstreamService.get();
        if (upstreamServer == null) {
            log.warn("No upstream server found. Resource mapping lookup failed.");
            return EMPTY_PAIR;
        }

        return transactionTemplate.execute(() -> Pair.pair(
                upstreamService.mapToLocalProjects(upstreamServer.getId(), remoteProjectIds),
                upstreamService.mapToLocalRepositories(upstreamServer.getId(), remoteRepositoryIds)));
    }
}
