package com.atlassian.stash.internal.user;

import com.atlassian.bitbucket.project.Project;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.request.RequestContext;
import com.atlassian.bitbucket.request.RequestManager;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.user.EscalatedSecurityContext;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.util.Operation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;

import javax.annotation.Nonnull;
import java.util.Set;

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

class DefaultEscalatedSecurityContext implements EscalatedSecurityContext {

    private static final Logger log = LoggerFactory.getLogger(DefaultEscalatedSecurityContext.class);

    private final DefaultPermissionGraph elevatedPermissions;
    private final boolean impersonateUser;
    private final String reason;
    private final RequestManager requestManager;
    private final ApplicationUser user;

    private DefaultEscalatedSecurityContext(Builder builder) {
        elevatedPermissions = builder.permissionsBuilder.build();
        impersonateUser = builder.impersonateUser;
        reason = builder.reason;
        requestManager = builder.requestManager;
        user = builder.user;
    }

    @Override
    public <T, E extends Throwable> T call(@Nonnull Operation<T, E> operation) throws E {
        checkNotNull(operation, "operation cannot be null");

        // store the current token so we can restore it later on
        Authentication authToken = SecurityContextHolder.getContext().getAuthentication();
        Authentication runAsToken = createRunAsToken();

        SecurityContextHolder.getContext().setAuthentication(runAsToken);
        try {
            log.trace("doWithPermission: running as {}", runAsToken);
            return operation.perform();
        } finally {
            // restore the original token
            SecurityContextHolder.getContext().setAuthentication(authToken);
        }
    }

    @Override
    public void applyToRequest() {
        RequestContext requestContext = requestManager.getRequestContext();
        if (requestContext == null || !requestContext.isActive()) {
            throw new IllegalStateException("No request is active");
        }

        //The runAsToken will be cleared at the end of the request
        SecurityContextHolder.getContext().setAuthentication(createRunAsToken());
    }

    @Nonnull
    @Override
    public EscalatedSecurityContext withPermission(@Nonnull Permission permission) {
        return new Builder(this).withPermission(checkNotNull(permission, "permission")).build();
    }

    @Nonnull
    @Override
    public EscalatedSecurityContext withPermission(@Nonnull Object resource, @Nonnull Permission permission) {
        return new Builder(this).withPermission(resource, permission).build();
    }

    @Nonnull
    @Override
    public EscalatedSecurityContext withPermissions(@Nonnull Set<Permission> permissions) {
        DefaultPermissionGraph.Builder builder = new DefaultPermissionGraph.Builder().addGraph(elevatedPermissions);
        int index = 0;
        for (Permission permission : permissions) {
            builder.add(checkNotNull(permission, "permissions[%s]", index++), null);
        }
        return new Builder(this).withPermissions(permissions).build();
    }

    private StashUserAuthenticationToken createRunAsToken() {
        Authentication currentToken = SecurityContextHolder.getContext().getAuthentication();

        // create the custom token
        PermissionGraph currentElevatedPermissions = null;
        ApplicationUser currentUser = null;
        if (currentToken instanceof StashUserAuthenticationToken) {
            StashUserAuthenticationToken stashToken = (StashUserAuthenticationToken) currentToken;
            currentElevatedPermissions = stashToken.getElevatedPermissions();
            currentUser = stashToken.getPrincipal();
        }

        return new StashUserAuthenticationToken.Builder()
                .user(impersonateUser ? user : currentUser)
                .elevatedPermissions(CompositePermissionGraph.maybeCompose(currentElevatedPermissions, elevatedPermissions))
                .build();
    }

    static class Builder {
        private final RequestManager requestManager;
        private final String reason;

        private DefaultPermissionGraph.Builder permissionsBuilder;
        private boolean impersonateUser;
        private ApplicationUser user;

        Builder(@Nonnull String reason, @Nonnull RequestManager requestManager) {
            this.requestManager = checkNotNull(requestManager, "requestManager");
            this.reason = checkNotNull(reason, "reason");

            permissionsBuilder = new DefaultPermissionGraph.Builder();
        }

        Builder(@Nonnull DefaultEscalatedSecurityContext escalatedSecurityContext) {
            checkNotNull(escalatedSecurityContext, "escalatedSecurityContext");

            impersonateUser = escalatedSecurityContext.impersonateUser;
            permissionsBuilder = new DefaultPermissionGraph.Builder()
                    .addGraph(escalatedSecurityContext.elevatedPermissions);
            reason = escalatedSecurityContext.reason;
            requestManager = escalatedSecurityContext.requestManager;
            user = escalatedSecurityContext.user;
        }

        @Nonnull
        Builder anonymously() {
            impersonateUser = true;
            user = null;
            return this;
        }

        @Nonnull
        DefaultEscalatedSecurityContext build() {
            return new DefaultEscalatedSecurityContext(this);
        }

        @Nonnull
        Builder impersonating(@Nonnull ApplicationUser user) {
            impersonateUser = true;
            this.user = checkNotNull(user, "user");
            return this;
        }

        @Nonnull
        Builder withPermission(@Nonnull Permission permission) {
            permissionsBuilder.add(checkNotNull(permission, "permission"), null);
            return this;
        }

        @Nonnull
        Builder withPermission(@Nonnull Object resource, @Nonnull Permission permission) {
            Integer resourceId;
            if (checkNotNull(resource, "resource") instanceof Repository) {
                resourceId = ((Repository) resource).getId();
                checkArgument(permission.isResource(Repository.class), "Repository permission required");
            } else if (resource instanceof Project) {
                resourceId = ((Project) resource).getId();
                checkArgument(permission.isResource(Project.class), "Project permission required");
            } else {
                throw new IllegalArgumentException("Only repository and project resources are supported. Got " +
                        resource.getClass().getCanonicalName());
            }
            permissionsBuilder.add(permission, resourceId);
            return this;
        }

        @Nonnull
        Builder withPermissions(@Nonnull Iterable<Permission> permissions) {
            int index = 0;
            for (Permission permission : checkNotNull(permissions, "permissions")) {
                permissionsBuilder.add(checkNotNull(permission, "permissions[%s]", index++), null);
            }
            return this;
        }
    }

}