/**
 * Copyright (c) Microsoft Corporation
 * <p/>
 * All rights reserved.
 * <p/>
 * MIT License
 * <p/>
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
 * documentation files (the "Software"), to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
 * to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 * <p/>
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of
 * the Software.
 * <p/>
 * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
 * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package com.microsoft.intellij.helpers.o365;

import com.google.common.base.*;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import com.intellij.openapi.project.Project;
import com.microsoft.directoryservices.*;
import com.microsoft.directoryservices.odata.ApplicationFetcher;
import com.microsoft.directoryservices.odata.DirectoryClient;
import com.microsoft.directoryservices.odata.DirectoryObjectOperations;
import com.microsoft.intellij.helpers.graph.PluginDependencyResolver;
import com.microsoft.intellij.helpers.graph.ServicePermissionEntry;
import com.microsoft.services.odata.ODataCollectionFetcher;
import com.microsoft.services.odata.ODataEntityFetcher;
import com.microsoft.services.odata.ODataOperations;
import com.microsoft.tooling.msservices.components.AppSettingsNames;
import com.microsoft.tooling.msservices.components.DefaultLoader;
import com.microsoft.tooling.msservices.components.PluginSettings;
import com.microsoft.tooling.msservices.helpers.NotNull;
import com.microsoft.tooling.msservices.helpers.Nullable;
import com.microsoft.tooling.msservices.helpers.StringHelper;
import com.microsoft.tooling.msservices.helpers.auth.AADManager;
import com.microsoft.tooling.msservices.helpers.auth.AADManagerImpl;
import com.microsoft.tooling.msservices.helpers.auth.UserInfo;
import com.microsoft.tooling.msservices.helpers.azure.AzureCmdException;
import com.microsoft.tooling.msservices.model.Office365Permission;
import com.microsoft.tooling.msservices.model.Office365PermissionList;
import com.microsoft.tooling.msservices.model.Office365Service;

import java.util.List;
import java.util.UUID;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Office365ManagerImpl implements Office365Manager {
    public class ServiceAppIds {
        public static final String EXCHANGE = "00000002-0000-0ff1-ce00-000000000000";
        public static final String SHARE_POINT = "00000003-0000-0ff1-ce00-000000000000";
        public static final String AZURE_ACTIVE_DIRECTORY = "00000002-0000-0000-c000-000000000000";
    }

    public static final String GRAPH_API_URI_TEMPLATE = "{base_uri}{tenant_domain}?api-version={api_version}";
    public static final String PROJECT_APP_ID = "com.microsoft.intellij.ProjectAppId";

    private static Office365Manager instance;
    private static Gson gson;

    private AADManager aadManager;

    private ReentrantReadWriteLock authDataLock = new ReentrantReadWriteLock(false);

    private UserInfo userInfo;
    private String accessToken;
    private DirectoryClient directoryDataServiceClient;

    private Office365ManagerImpl() {
        aadManager = AADManagerImpl.getManager();

        String json = DefaultLoader.getIdeHelper().getProperty(AppSettingsNames.O365_USER_INFO);

        if (!StringHelper.isNullOrWhiteSpace(json)) {
            try {
                UserInfo userInfo = gson.fromJson(json, UserInfo.class);
                setUserInfo(userInfo);
            } catch (JsonSyntaxException ignored) {
                DefaultLoader.getIdeHelper().unsetProperty(AppSettingsNames.O365_USER_INFO);
            }
        }
    }

    @NotNull
    public static synchronized Office365Manager getManager() {
        if (instance == null) {
            gson = new Gson();
            instance = new Office365ManagerImpl();
        }

        return instance;
    }

    @Override
    public void authenticate() throws AzureCmdException {
        PluginSettings settings = DefaultLoader.getPluginComponent().getSettings();

        UserInfo userInfo = aadManager.authenticate(settings.getGraphApiUri(), "Sign in to your Office 365 account");

        setUserInfo(userInfo);
    }

    @Override
    public boolean authenticated() {
        return getUserInfo() != null;
    }

    @Override
    public void clearAuthentication() {
        setUserInfo(null);
    }

    @NotNull
    @Override
    public ListenableFuture<List<Application>> getApplicationList() {
        return requestFutureWithToken(new RequestCallback<ListenableFuture<List<Application>>>() {
            @Override
            public ListenableFuture<List<Application>> execute()
                    throws Throwable {
                return getAllObjects(getDirectoryClient().getapplications());
            }
        });
    }

    @Override
    @NotNull
    public ListenableFuture<Application> getApplicationByObjectId(@NotNull final String objectId) {
        return requestFutureWithToken(new RequestCallback<ListenableFuture<Application>>() {
            @Override
            public ListenableFuture<Application> execute()
                    throws Throwable {
                return getDirectoryClient().getapplications().getById(objectId).read();
            }
        });
    }

    @Override
    @NotNull
    public ListenableFuture<List<ServicePermissionEntry>> getO365PermissionsForApp(@NotNull final String objectId) {
        return requestFutureWithToken(new RequestCallback<ListenableFuture<List<ServicePermissionEntry>>>() {
            @Override
            public ListenableFuture<List<ServicePermissionEntry>> execute()
                    throws Throwable {
                return Futures.transform(getApplicationByObjectId(objectId),
                        new AsyncFunction<Application, List<ServicePermissionEntry>>() {
                            @Override
                            public ListenableFuture<List<ServicePermissionEntry>> apply(Application application) throws Exception {

                                final String[] filterAppIds = new String[]{
                                        ServiceAppIds.SHARE_POINT,
                                        ServiceAppIds.EXCHANGE,
                                        ServiceAppIds.AZURE_ACTIVE_DIRECTORY
                                };

                                // build initial list of permission from the app's permissions
                                final List<ServicePermissionEntry> servicePermissions = getO365PermissionsFromResourceAccess(application.getrequiredResourceAccess(), filterAppIds);

                                // get permissions list from O365 service principals
                                return Futures.transform(getServicePrincipalsForO365(), new AsyncFunction<List<ServicePrincipal>, List<ServicePermissionEntry>>() {
                                    @Override
                                    public ListenableFuture<List<ServicePermissionEntry>> apply(List<ServicePrincipal> servicePrincipals) throws Exception {

                                        for (final ServicePrincipal servicePrincipal : servicePrincipals) {
                                            // lookup this service principal in app's list of resources; if it's not found add an entry
                                            ServicePermissionEntry servicePermissionEntry = Iterables.find(servicePermissions, new Predicate<ServicePermissionEntry>() {
                                                @Override
                                                public boolean apply(ServicePermissionEntry servicePermissionEntry) {
                                                    return servicePermissionEntry.getKey().getId().equals(servicePrincipal.getappId());
                                                }
                                            }, null);

                                            if (servicePermissionEntry == null) {
                                                servicePermissions.add(servicePermissionEntry = new ServicePermissionEntry(
                                                        new Office365Service(),
                                                        new Office365PermissionList()
                                                ));
                                            }

                                            Office365Service service = servicePermissionEntry.getKey();
                                            Office365PermissionList permissionList = servicePermissionEntry.getValue();
                                            service.setId(servicePrincipal.getappId());
                                            service.setName(servicePrincipal.getdisplayName());

                                            List<OAuth2Permission> permissions = servicePrincipal.getoauth2Permissions();
                                            for (final OAuth2Permission permission : permissions) {
                                                // lookup permission in permissionList
                                                Office365Permission office365Permission = Iterables.find(permissionList, new Predicate<Office365Permission>() {
                                                    @Override
                                                    public boolean apply(Office365Permission office365Permission) {
                                                        return office365Permission.getId().equals(permission.getid().toString());
                                                    }
                                                }, null);

                                                if (office365Permission == null) {
                                                    permissionList.add(office365Permission = new Office365Permission());
                                                    office365Permission.setEnabled(false);
                                                }

                                                office365Permission.setId(permission.getid().toString());
                                                office365Permission.setName(getPermissionDisplayName(permission.getvalue()));
                                                office365Permission.setDescription(permission.getuserConsentDisplayName());
                                            }
                                        }

                                        return Futures.immediateFuture(servicePermissions);
                                    }
                                });
                            }
                        });
            }
        });
    }

    private String getPermissionDisplayName(String displayName) {
        // replace '.' and '_' with space characters and title case the display name
        return Joiner.on(' ').
                join(Iterables.transform(
                                Splitter.on(' ').split(
                                        CharMatcher.anyOf("._").
                                                replaceFrom(displayName, ' ')),
                                new Function<String, String>() {
                                    @Override
                                    public String apply(String str) {
                                        return Character.toUpperCase(str.charAt(0)) + str.substring(1);
                                    }
                                }
                        )
                );
    }

    private List<ServicePermissionEntry> getO365PermissionsFromResourceAccess(
            List<RequiredResourceAccess> requiredResourceAccesses,
            String[] filterAppIds) {

        List<ServicePermissionEntry> entryList = Lists.newArrayList();
        if (requiredResourceAccesses == null) {
            return entryList;
        }

        for (final RequiredResourceAccess requiredResourceAccess : requiredResourceAccesses) {
            // we're interested in this resource only if it is one of the app id's in "filterAppIds"
            boolean isO365Resource = Iterators.any(Iterators.forArray(filterAppIds), new Predicate<String>() {
                @Override
                public boolean apply(String appId) {
                    return requiredResourceAccess.getresourceAppId().equals(appId);
                }
            });
            if (!isO365Resource) {
                continue;
            }

            Office365Service service = new Office365Service();
            Office365PermissionList permissions = new Office365PermissionList();
            entryList.add(new ServicePermissionEntry(service, permissions));

            service.setId(requiredResourceAccess.getresourceAppId());
            List<ResourceAccess> resourceAccesses = requiredResourceAccess.getresourceAccess();
            if (resourceAccesses == null) {
                continue;
            }
            for (ResourceAccess resourceAccess : resourceAccesses) {
                Office365Permission permission = new Office365Permission(
                        resourceAccess.getid().toString(),
                        "", "",
                        resourceAccess.gettype().equals("Scope")
                );
                permissions.add(permission);
            }
        }

        return entryList;
    }

    @Override
    @NotNull
    public ListenableFuture<Application> setO365PermissionsForApp(
            @NotNull Application application,
            @NotNull List<ServicePermissionEntry> permissionEntryList) {
        List<RequiredResourceAccess> requiredResourceAccesses = application.getrequiredResourceAccess();
        if (requiredResourceAccesses == null) {
            application.setrequiredResourceAccess(requiredResourceAccesses = Lists.newArrayList());
        }

        for (ServicePermissionEntry permissionEntry : permissionEntryList) {
            final Office365Service service = permissionEntry.getKey();

            // filter permissions for enabled permissions
            Iterable<Office365Permission> permissionList = Iterables.filter(permissionEntry.getValue(),
                    new Predicate<Office365Permission>() {
                        @Override
                        public boolean apply(Office365Permission office365Permission) {
                            return office365Permission.isEnabled();
                        }
                    });

            // transform Office365Permission objects into ResourceAccess objects
            List<ResourceAccess> resourceAccessList = Lists.newArrayList(Iterables.transform(permissionList,
                    new Function<Office365Permission, ResourceAccess>() {
                        @Override
                        public ResourceAccess apply(Office365Permission office365Permission) {
                            ResourceAccess resourceAccess = new ResourceAccess();
                            resourceAccess.setid(UUID.fromString(office365Permission.getId()));
                            resourceAccess.settype("Scope");
                            return resourceAccess;
                        }
                    }));

            // get reference to service from app in case it exists
            RequiredResourceAccess requiredResourceAccess = Iterables.find(requiredResourceAccesses, new Predicate<RequiredResourceAccess>() {
                @Override
                public boolean apply(RequiredResourceAccess requiredResourceAccess) {
                    return requiredResourceAccess.getresourceAppId().equals(service.getId());
                }
            }, null);

            if (requiredResourceAccess == null && !resourceAccessList.isEmpty()) {
                requiredResourceAccesses.add(requiredResourceAccess = new RequiredResourceAccess());
                requiredResourceAccess.setresourceAppId(service.getId());
            }

            if (requiredResourceAccess != null) {
                if (resourceAccessList.isEmpty()) {
                    // remove requiredResourceAccess from requiredResourceAccesses
                    requiredResourceAccesses.remove(requiredResourceAccess);
                } else {
                    requiredResourceAccess.setresourceAccess(resourceAccessList);
                }
            }
        }

        return updateApplication(application);
    }

    @Override
    @NotNull
    public ListenableFuture<Application> updateApplication(@NotNull final Application application) {
        return requestFutureWithToken(new RequestCallback<ListenableFuture<Application>>() {
            @Override
            public ListenableFuture<Application> execute()
                    throws Throwable {
                ApplicationFetcher appFetcher = getDirectoryClient().getapplications().getById(application.getobjectId());
                return appFetcher.update(application);
            }
        });
    }

    @Override
    @NotNull
    public ListenableFuture<List<ServicePrincipal>> getServicePrincipals() {
        return requestFutureWithToken(new RequestCallback<ListenableFuture<List<ServicePrincipal>>>() {
            @Override
            public ListenableFuture<List<ServicePrincipal>> execute()
                    throws Throwable {
                return getAllObjects(getDirectoryClient().getservicePrincipals());
            }
        });
    }

    @Override
    @NotNull
    public ListenableFuture<List<ServicePrincipal>> getServicePrincipalsForO365() {
        return requestFutureWithToken(new RequestCallback<ListenableFuture<List<ServicePrincipal>>>() {
            @Override
            public ListenableFuture<List<ServicePrincipal>> execute()
                    throws Throwable {
                // build the filter
                String[] appIds = new String[]{
                        ServiceAppIds.AZURE_ACTIVE_DIRECTORY,
                        ServiceAppIds.EXCHANGE,
                        ServiceAppIds.SHARE_POINT
                };
                String filter = "appId eq '" + Joiner.on("' or appId eq '").join(appIds) + "'";
                return getDirectoryClient().getservicePrincipals().filter(filter).read();
            }
        });
    }

    private <T> ListenableFuture<T> getFirstItem(ListenableFuture<List<T>> future) {
        return Futures.transform(future, new AsyncFunction<List<T>, T>() {
            @Override
            public ListenableFuture<T> apply(List<T> items) throws Exception {
                return Futures.immediateFuture((items != null && items.size() > 0) ? items.get(0) : null);
            }
        });
    }

    @Override
    @NotNull
    public ListenableFuture<List<OAuth2PermissionGrant>> getPermissionGrants() {
        return requestFutureWithToken(new RequestCallback<ListenableFuture<List<OAuth2PermissionGrant>>>() {
            @Override
            public ListenableFuture<List<OAuth2PermissionGrant>> execute()
                    throws Throwable {
                return getDirectoryClient().getoauth2PermissionGrants().read();
            }
        });
    }

    private <E extends DirectoryObject, F extends ODataEntityFetcher<E, ? extends DirectoryObjectOperations>, O extends ODataOperations>
    ListenableFuture<List<E>> getAllObjects(final ODataCollectionFetcher<E, F, O> fetcher) {

        return Futures.transform(fetcher.read(), new AsyncFunction<List<E>, List<E>>() {
            @Override
            public ListenableFuture<List<E>> apply(List<E> entities) throws Exception {
                return Futures.successfulAsList(Lists.transform(entities, new Function<E, ListenableFuture<? extends E>>() {
                    @Override
                    public ListenableFuture<? extends E> apply(E e) {
                        return fetcher.getById(e.getobjectId()).read();
                    }
                }));
            }
        });
    }

    @Override
    @NotNull
    public ListenableFuture<Application> registerApplication(@NotNull final Application application) {
        return requestFutureWithToken(new RequestCallback<ListenableFuture<Application>>() {
            @Override
            public ListenableFuture<Application> execute()
                    throws Throwable {
                // register the app and then create a service principal for the app if there isn't already one
                return Futures.transform(getDirectoryClient().getapplications().add(application), new AsyncFunction<Application, Application>() {
                    @Override
                    public ListenableFuture<Application> apply(final Application application) throws Exception {
                        return Futures.transform(getServicePrincipalsForApp(application), new AsyncFunction<List<ServicePrincipal>, Application>() {
                            @Override
                            public ListenableFuture<Application> apply(List<ServicePrincipal> servicePrincipals) throws Exception {
                                if (servicePrincipals.size() == 0) {
                                    return createServicePrincipalForApp(application);
                                }

                                return Futures.immediateFuture(application);
                            }
                        });
                    }
                });
            }
        });
    }

    private ListenableFuture<Application> createServicePrincipalForApp(final Application application) throws AzureCmdException {
        ServicePrincipal servicePrincipal = new ServicePrincipal();
        servicePrincipal.setappId(application.getappId());

        servicePrincipal.setaccountEnabled(true);

        return Futures.transform(getDirectoryClient().getservicePrincipals().add(servicePrincipal), new AsyncFunction<ServicePrincipal, Application>() {
            @Override
            public ListenableFuture<Application> apply(ServicePrincipal servicePrincipal) throws Exception {
                return Futures.immediateFuture(application);
            }
        });
    }

    @Override
    @NotNull
    public ListenableFuture<Application> getApplicationForProject(@NotNull Project project) {
        final String appId = DefaultLoader.getIdeHelper().getProperty(project, PROJECT_APP_ID);
        if (StringHelper.isNullOrWhiteSpace(appId)) {
            return Futures.immediateFuture(null);
        }

        return requestFutureWithToken(new RequestCallback<ListenableFuture<Application>>() {
            @Override
            public ListenableFuture<Application> execute()
                    throws Throwable {
                return getFirstItem(
                        getDirectoryClient().
                                getapplications().
                                filter("appId eq '" + appId + "'").
                                read());
            }
        });
    }

    @Override
    public void setApplicationForProject(@NotNull Project project, @NotNull Application application) {
        DefaultLoader.getIdeHelper().setProperty(project, PROJECT_APP_ID, application.getappId());
        project.save();
    }

    @NotNull
    @Override
    public ListenableFuture<List<ServicePrincipal>> getServicePrincipalsForApp(@NotNull final Application application) {
        return requestFutureWithToken(new RequestCallback<ListenableFuture<List<ServicePrincipal>>>() {
            @Override
            public ListenableFuture<List<ServicePrincipal>> execute()
                    throws Throwable {
                return getDirectoryClient().
                        getservicePrincipals().
                        filter("appId eq '" + application.getappId() + "'").
                        read();
            }
        });
    }

    @Override
    @NotNull
    public ListenableFuture<List<ServicePrincipal>> getO365ServicePrincipalsForApp(@NotNull final Application application) {
        return requestFutureWithToken(new RequestCallback<ListenableFuture<List<ServicePrincipal>>>() {
            @Override
            public ListenableFuture<List<ServicePrincipal>> execute()
                    throws Throwable {
                @SuppressWarnings("unchecked")
                ListenableFuture<List<ServicePrincipal>>[] futures = new ListenableFuture[]{
                        getServicePrincipalsForApp(application),
                        getServicePrincipalsForO365()
                };

                final String[] filterAppIds = new String[]{
                        ServiceAppIds.SHARE_POINT,
                        ServiceAppIds.EXCHANGE,
                        ServiceAppIds.AZURE_ACTIVE_DIRECTORY
                };

                return Futures.transform(Futures.allAsList(futures), new AsyncFunction<List<List<ServicePrincipal>>, List<ServicePrincipal>>() {
                    @Override
                    public ListenableFuture<List<ServicePrincipal>> apply(List<List<ServicePrincipal>> lists) throws Exception {
                        // According to Guava documentation for allAsList, the list of results is in the
                        // same order as the input list. So first we get the service principals for the app
                        // filtered for O365 and Graph service principals.
                        final List<ServicePrincipal> servicePrincipalsForApp = Lists.newArrayList(Iterables.filter(lists.get(0), new Predicate<ServicePrincipal>() {
                            @Override
                            public boolean apply(final ServicePrincipal servicePrincipal) {
                                // we are only interested in O365 and Graph service principals
                                return Iterators.any(Iterators.forArray(filterAppIds), new Predicate<String>() {
                                    @Override
                                    public boolean apply(String appId) {
                                        return appId.equals(servicePrincipal.getappId());
                                    }
                                });
                            }
                        }));

                        // next we get the O365/graph service principals
                        final List<ServicePrincipal> servicePrincipalsForO365 = lists.get(1);

                        // then we add service principals from servicePrincipalsForO365 to servicePrincipalsForApp
                        // where the service principal is not available in the latter
                        Iterable<ServicePrincipal> servicePrincipalsToBeAdded = Iterables.filter(servicePrincipalsForO365, new Predicate<ServicePrincipal>() {
                            @Override
                            public boolean apply(ServicePrincipal servicePrincipal) {
                                return !servicePrincipalsForApp.contains(servicePrincipal);
                            }
                        });
                        Iterables.addAll(servicePrincipalsForApp, servicePrincipalsToBeAdded);

                        // assign the appid to the service principal and reset permissions on new service principals;
                        // we do Lists.newArrayList calls below to create a copy of the service lists because Lists.transform
                        // invokes the transformation function lazily and this causes problems for us; we force immediate
                        // evaluation of our transfomer by copying the elements to a new list
                        List<ServicePrincipal> servicePrincipals = Lists.newArrayList(Lists.transform(servicePrincipalsForApp, new Function<ServicePrincipal, ServicePrincipal>() {
                            @Override
                            public ServicePrincipal apply(ServicePrincipal servicePrincipal) {
                                if (!servicePrincipal.getappId().equals(application.getappId())) {
                                    servicePrincipal.setappId(application.getappId());
                                    servicePrincipal.setoauth2Permissions(Lists.newArrayList(Lists.transform(servicePrincipal.getoauth2Permissions(), new Function<OAuth2Permission, OAuth2Permission>() {
                                        @Override
                                        public OAuth2Permission apply(OAuth2Permission oAuth2Permission) {
                                            oAuth2Permission.setisEnabled(false);
                                            return oAuth2Permission;
                                        }
                                    })));
                                }

                                return servicePrincipal;
                            }
                        }));

                        return Futures.immediateFuture(servicePrincipals);
                    }
                });
            }
        });
    }

    @Override
    @NotNull
    public ListenableFuture<List<ServicePrincipal>> addServicePrincipals(
            @NotNull final List<ServicePrincipal> servicePrincipals) {

        return requestFutureWithToken(new RequestCallback<ListenableFuture<List<ServicePrincipal>>>() {
            @Override
            public ListenableFuture<List<ServicePrincipal>> execute()
                    throws Throwable {
                List<ListenableFuture<ServicePrincipal>> futures = Lists.transform(
                        servicePrincipals,
                        new Function<ServicePrincipal, ListenableFuture<ServicePrincipal>>() {
                            @Override
                            public ListenableFuture<ServicePrincipal> apply(ServicePrincipal servicePrincipal) {
                                return getDirectoryClient().getservicePrincipals().add(servicePrincipal);
                            }
                        }
                );

                return Futures.allAsList(futures);
            }
        });
    }

    @Nullable
    private UserInfo getUserInfo() {
        authDataLock.readLock().lock();

        try {
            return userInfo;
        } finally {
            authDataLock.readLock().unlock();
        }
    }

    private void setUserInfo(@Nullable UserInfo userInfo) {
        authDataLock.writeLock().lock();

        try {
            this.userInfo = userInfo;
            setAccessToken(null);

            String json = gson.toJson(userInfo, UserInfo.class);
            DefaultLoader.getIdeHelper().setProperty(AppSettingsNames.O365_USER_INFO, json);
        } finally {
            authDataLock.writeLock().unlock();
        }
    }

    @Nullable
    private String getAccessToken() {
        authDataLock.readLock().lock();

        try {
            return accessToken;
        } finally {
            authDataLock.readLock().unlock();
        }
    }

    private void setAccessToken(@Nullable String accessToken) {
        authDataLock.writeLock().lock();

        try {
            this.accessToken = accessToken;

            DirectoryClient directoryDataServiceClient = null;

            if (accessToken != null) {
                PluginDependencyResolver dependencyResolver = new PluginDependencyResolver(accessToken);
                directoryDataServiceClient = new DirectoryClient(getGraphApiUri(), dependencyResolver);
            }

            setDirectoryDataServiceClient(directoryDataServiceClient);
        } finally {
            authDataLock.writeLock().unlock();
        }
    }

    // NOTE: The result of calling getDirectoryClient should never be cached. This is because of the following
    // reasons:
    //  [a] every directory client object is associated with an authentication token
    //  [b] as part of execution of the method, tokens might expire and be renewed in which case a new directory
    //      client will be instantiated; if we use cached objects then we'll continue using the client with the
    //      expired token instead of the new one
    private DirectoryClient getDirectoryClient() {
        authDataLock.readLock().lock();

        try {
            return directoryDataServiceClient;
        } finally {
            authDataLock.readLock().unlock();
        }
    }

    private void setDirectoryDataServiceClient(DirectoryClient directoryDataServiceClient) {
        authDataLock.writeLock().lock();

        try {
            this.directoryDataServiceClient = directoryDataServiceClient;
        } finally {
            authDataLock.writeLock().unlock();
        }
    }

    private String getGraphApiUri() {
        PluginSettings settings = DefaultLoader.getPluginComponent().getSettings();

        return GRAPH_API_URI_TEMPLATE.
                replace("{base_uri}", settings.getGraphApiUri()).
                replace("{tenant_domain}", getTenantDomain()).
                replace("{api_version}", settings.getGraphApiVersion());
    }

    private String getTenantDomain() {
        UserInfo userInfo = getUserInfo();

        if (userInfo == null) {
            throw new IllegalStateException("user is null");
        }

        return userInfo.getTenantId();
    }

    @NotNull
    private <V> ListenableFuture<V> requestFutureWithToken(@NotNull final RequestCallback<ListenableFuture<V>> requestCallback) {
        PluginSettings settings = DefaultLoader.getPluginComponent().getSettings();

        com.microsoft.tooling.msservices.helpers.auth.RequestCallback<ListenableFuture<V>> aadRequestCB =
                new com.microsoft.tooling.msservices.helpers.auth.RequestCallback<ListenableFuture<V>>() {
                    @NotNull
                    @Override
                    public ListenableFuture<V> execute(@NotNull String accessToken)
                            throws Throwable {
                        if (!accessToken.equals(getAccessToken())) {
                            authDataLock.writeLock().lock();

                            try {
                                if (!accessToken.equals(getAccessToken())) {
                                    setAccessToken(accessToken);
                                }
                            } finally {
                                authDataLock.writeLock().unlock();
                            }
                        }

                        return requestCallback.execute();
                    }
                };

        return aadManager.requestFuture(userInfo,
                settings.getGraphApiUri(),
                "Sign in to your Office 365 account",
                aadRequestCB);
    }
}