//  Copyright (c) Microsoft Corporation.
//  All rights reserved.
//
//  This code is licensed under the MIT License.
//
//  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 :
//
//  The above copyright notice and this permission notice shall be included in
//  all copies or substantial portions of the Software.
//
//  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.aad.adal;

import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Log;

import androidx.annotation.Nullable;
import androidx.core.content.pm.PackageInfoCompat;

import com.microsoft.identity.common.adal.internal.util.StringExtensions;
import com.microsoft.identity.common.java.providers.microsoft.MicrosoftAuthorizationErrorResponse;
import com.microsoft.identity.common.java.providers.microsoft.azureactivedirectory.AzureActiveDirectory;
import com.microsoft.identity.common.java.providers.microsoft.azureactivedirectory.AzureActiveDirectoryCloud;

import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Date;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.AZURE_AUTHENTICATOR_APP_PACKAGE_NAME;
import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.BROKER_HOST_APP_PACKAGE_NAME;
import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.BROKER_REDIRECT_URI;
import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.COMPANY_PORTAL_APP_PACKAGE_NAME;
import static com.microsoft.identity.common.java.AuthenticationConstants.UIRequest.BROWSER_FLOW;

/**
 * Internal class for handling acquireToken logic, including the silent flow and interactive flow.
 */
class AcquireTokenRequest {

    private static final String TAG = AcquireTokenRequest.class.getSimpleName();

    /**
     * Singled threaded Executor for async work.
     */
    private static final ExecutorService THREAD_EXECUTOR = Executors.newSingleThreadExecutor();

    private final Context mContext;
    private final AuthenticationContext mAuthContext;
    private TokenCacheAccessor mTokenCacheAccessor;
    private final IBrokerProxy mBrokerProxy;

    private static Handler sHandler = null;

    private static final long CP_LLT_VERSION_CODE = 2950722;

    private static final long AUTHENTICATOR_LLT_VERSION_CODE = 138;

    /**
     * Instance validation related calls are serviced inside Discovery as a
     * module.
     */
    private Discovery mDiscovery;

    /**
     * Event Variable for the acquireToken API called. This will track whether the API succeeded or not.
     */
    private APIEvent mAPIEvent;

    /**
     * Constructor for {@link AcquireTokenRequest}.
     */
    AcquireTokenRequest(final Context appContext, final AuthenticationContext authContext, final APIEvent apiEvent) {
        mContext = appContext;
        mAuthContext = authContext;
        mDiscovery = new Discovery(mContext);

        if (authContext.getCache() != null && apiEvent != null) {
            mTokenCacheAccessor = new TokenCacheAccessor(appContext.getApplicationContext(), authContext.getCache(),
                    authContext.getAuthority(), apiEvent.getTelemetryRequestId());
            mTokenCacheAccessor.setValidateAuthorityHost(mAuthContext.getValidateAuthority());
        }
        mBrokerProxy = new BrokerProxy(appContext);

        mAPIEvent = apiEvent;
    }

    /**
     * Handles the acquire token logic. Will do authority validation first if developer set validateAuthority to be
     * true.
     */
    void acquireToken(final IWindowComponent activity, final boolean useDialog, final AuthenticationRequest authRequest,
                      final AuthenticationCallback<AuthenticationResult> authenticationCallback) {
        final String methodName = ":acquireToken";
        final CallbackHandler callbackHandle = new CallbackHandler(getHandler(), authenticationCallback);
        // Executes all the calls inside the Runnable to return immediately to
        // user. All UI
        // related actions will be performed using Handler.
        Logger.setCorrelationId(authRequest.getCorrelationId());
        Logger.v(TAG + methodName, "Sending async task from thread:" + android.os.Process.myTid());
        THREAD_EXECUTOR.execute(new Runnable() {
            @Override
            public void run() {
                // With the introduction of DiagnosticContext, correlationIds are now tracked
                // per-thread. To support passing the correlationId to the worker thread, we need
                // to call setCorrelationId() again.
                Logger.setCorrelationId(authRequest.getCorrelationId());

                Logger.v(TAG + methodName, "Running task in thread:" + android.os.Process.myTid());
                try {
                    // Validate acquire token call first.
                    validateAcquireTokenRequest(authRequest);
                    performAcquireTokenRequest(callbackHandle, activity, useDialog, authRequest);
                } catch (final AuthenticationException authenticationException) {
                    mAPIEvent.setSpeRing(authenticationException.getSpeRing());
                    mAPIEvent.setRefreshTokenAge(authenticationException.getRefreshTokenAge());
                    mAPIEvent.setServerErrorCode(authenticationException.getCliTelemErrorCode());
                    mAPIEvent.setServerSubErrorCode(authenticationException.getCliTelemSubErrorCode());
                    mAPIEvent.setWasApiCallSuccessful(false, authenticationException);
                    mAPIEvent.setCorrelationId(authRequest.getCorrelationId().toString());
                    mAPIEvent.stopTelemetryAndFlush();

                    callbackHandle.onError(authenticationException);
                }
            }
        });
    }

    /**
     * This API allows to obtain a new access token in exchange for a refresh token. The refresh
     * token must be provided to this API. The tokens obtained via this API may or may not be saved
     * in ADAL's cache depending on two things: The openid scope was requested in the token, and a
     * resource was supplied to the {@link AuthenticationRequest}. If both of those conditions are
     * true, then tokens are saved in the cache, else not.
     *
     * @param refreshToken          the refresh token to use when exchanging for an access token
     * @param authenticationRequest the {@link AuthenticationRequest} to use when requesting token
     * @param externalCallback      the callback to which the result should be posted
     */
    void refreshTokenWithoutCache(final String refreshToken, final AuthenticationRequest authenticationRequest,
                                  final AuthenticationCallback<AuthenticationResult> externalCallback) {
        final String methodName = ":refreshTokenWithoutCache";
        Logger.setCorrelationId(authenticationRequest.getCorrelationId());
        Logger.v(TAG + methodName, "Refresh token without cache");

        final CallbackHandler callbackHandle = new CallbackHandler(getHandler(), externalCallback);

        // Execute all the calls inside Runnable to return immediately. All UI
        // related actions will be performed using Handler.
        THREAD_EXECUTOR.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    // validate acquire token call first.
                    validateAcquireTokenRequest(authenticationRequest);

                    final AcquireTokenSilentHandler acquireTokenSilentHandler = new AcquireTokenSilentHandler(mContext,
                            authenticationRequest, mTokenCacheAccessor);
                    final AuthenticationResult result
                            = acquireTokenSilentHandler.acquireTokenWithRefreshToken(refreshToken);

                    final String correlationId = String.format(" CorrelationId: %s", authenticationRequest.getCorrelationId().toString());

                    if (result == null) {
                        Logger.e(TAG + methodName, "Returned result with exchanging refresh token for access token is null" + correlationId, "",
                                ADALError.AUTH_REFRESH_FAILED);
                        throw new AuthenticationException(
                                ADALError.AUTH_REFRESH_FAILED, "No result received from refresh token request.");
                    }

                    if (!StringExtensions.isNullOrBlank(result.getErrorCode())) {
                        Logger.e(TAG + methodName, " ErrorCode:" + result.getErrorCode(), " ErrorDescription:" + result.getErrorDescription(), ADALError.AUTH_REFRESH_FAILED);
                        throw new AuthenticationException(ADALError.AUTH_REFRESH_FAILED,
                                " ErrorCode:" + result.getErrorCode());
                    }

                    if (StringExtensions.isNullOrBlank(result.getAccessToken())) {
                        Logger.e(TAG + methodName, "Access Token not returned from server", "", ADALError.AUTH_FAILED_NO_TOKEN);
                        throw new AuthenticationException(ADALError.AUTH_FAILED_NO_TOKEN,
                                " Access Token not returned from server ");
                    }

                    final String rawIdToken = result.getIdToken();

                    // We will save tokens in cache if we get back an ID Token and also we have a
                    // resource available.
                    //
                    // The ID Token could be null if the caller did not ask for
                    // the openid scope by not passing it to the Authentication Request,
                    // whereas the resource could be null if the caller did not specify a resource
                    // on the AuthenticationRequest.
                    if (!TextUtils.isEmpty(rawIdToken) && !TextUtils.isEmpty(authenticationRequest.getResource())) {
                        final IdToken idTokenRecord = new IdToken(rawIdToken);
                        final UserInfo userInfo = new UserInfo(idTokenRecord);

                        TokenCacheItem tokenCacheItem = new TokenCacheItem();
                        tokenCacheItem.setRawIdToken(rawIdToken);
                        tokenCacheItem.setUserInfo(userInfo);
                        tokenCacheItem.setTenantId(idTokenRecord.getTenantId());

                        // This method is used in ADAL to store tokens in cache during a silent aka
                        // refresh token request. We are going to be using the same method for this
                        // API as well for consistency.
                        //
                        // To get this to work, we created a TokenCacheItem object above and
                        // populate it with an ID Token and UserInfo object as in this API ADAL does
                        // not do a cache look up and does not have the ID Token in cache already.
                        // We got an ID Token from eSTS and will be using that ID Token to create
                        // a UserInfo object and seed it into the cache along with the other tokens.
                        // This allows us to achieve SSO for this API.
                        mTokenCacheAccessor.updateCachedItemWithResult(authenticationRequest,
                                result, tokenCacheItem);
                    }

                    mAPIEvent.setWasApiCallSuccessful(true, null);
                    mAPIEvent.setIdToken(result.getIdToken());
                    callbackHandle.onSuccess(result);
                } catch (final AuthenticationException authenticationException) {
                    mAPIEvent.setWasApiCallSuccessful(false, authenticationException);
                    callbackHandle.onError(authenticationException);
                } finally {
                    mAPIEvent.setCorrelationId(authenticationRequest.getCorrelationId().toString());
                    mAPIEvent.stopTelemetryAndFlush();
                }
            }
        });
    }

    private void validateAcquireTokenRequest(final AuthenticationRequest authenticationRequest)
            throws AuthenticationException {
        final URL authorityUrl = StringExtensions.getUrl(authenticationRequest.getAuthority());
        if (authorityUrl == null) {
            throw new AuthenticationException(
                    ADALError.DEVELOPER_AUTHORITY_IS_NOT_VALID_URL);
        }

        // validate authority
        performAuthorityValidation(authenticationRequest, authorityUrl);

        // Verify broker redirect uri for non-silent request
        final BrokerProxy.SwitchToBroker canSwitchToBrokerFlag = mBrokerProxy.canSwitchToBroker(authenticationRequest.getAuthority());

        if (canSwitchToBrokerFlag != BrokerProxy.SwitchToBroker.CANNOT_SWITCH_TO_BROKER
                && mBrokerProxy.verifyUser(authenticationRequest.getLoginHint(), authenticationRequest.getUserId())
                && !authenticationRequest.isSilent()) {

            if (canSwitchToBrokerFlag == BrokerProxy.SwitchToBroker.NEED_PERMISSIONS_TO_SWITCH_TO_BROKER) {
                throw new UsageAuthenticationException(
                        ADALError.DEVELOPER_BROKER_PERMISSIONS_MISSING,
                        "Broker related permissions are missing for GET_ACCOUNTS.");
            }
            // Verify redirect uri match what we expecting for broker non-silent request. For silent request,
            // since we don't require developer to pass us the redirect uri, we won't perform the validation.
            verifyBrokerRedirectUri(authenticationRequest);
        }
    }

    private void performAuthorityValidation(final AuthenticationRequest authenticationRequest, final URL authorityUrl)
            throws AuthenticationException {
        // validate authority
        final String methodName = ":performAuthorityValidation";
        Telemetry.getInstance().startEvent(authenticationRequest.getTelemetryRequestId(),
                EventStrings.AUTHORITY_VALIDATION_EVENT);
        APIEvent apiEvent = new APIEvent(EventStrings.AUTHORITY_VALIDATION_EVENT);
        apiEvent.setCorrelationId(authenticationRequest.getCorrelationId().toString());
        apiEvent.setRequestId(authenticationRequest.getTelemetryRequestId());

        if (mAuthContext.getValidateAuthority()) {
            try {
                validateAuthority(authorityUrl, authenticationRequest.getUpnSuffix(), authenticationRequest.isSilent(),
                        authenticationRequest.getCorrelationId());
                apiEvent.setValidationStatus(EventStrings.AUTHORITY_VALIDATION_SUCCESS);
            } catch (final AuthenticationException authenticationException) {
                if (null != authenticationException.getCode()
                        && (authenticationException.getCode().equals(ADALError.DEVICE_CONNECTION_IS_NOT_AVAILABLE)
                        || authenticationException.getCode().equals(ADALError.NO_NETWORK_CONNECTION_POWER_OPTIMIZATION))) {
                    // The authority validation is not done because of network error.
                    apiEvent.setValidationStatus(EventStrings.AUTHORITY_VALIDATION_NOT_DONE);
                } else {
                    apiEvent.setValidationStatus(EventStrings.AUTHORITY_VALIDATION_FAILURE);
                }

                throw authenticationException;
            } finally {
                Telemetry.getInstance().stopEvent(authenticationRequest.getTelemetryRequestId(), apiEvent,
                        EventStrings.AUTHORITY_VALIDATION_EVENT);
            }
        } else {
            // check if it contains authority url as the key, if key exists, it means that the authority url validation has happened
            // for the authority already.
            if (!UrlExtensions.isADFSAuthority(authorityUrl) && !AuthorityValidationMetadataCache.containsAuthorityHost(authorityUrl)) {
                try {
                    mDiscovery.validateAuthority(authorityUrl);
                } catch (final AuthenticationException authenticationException) {
                    // Ignore the failure, save in the map as a failed instance discovery to avoid it being looked up another times in the same process
                    AuthorityValidationMetadataCache.updateInstanceDiscoveryMap(authorityUrl.getHost(), new InstanceDiscoveryMetadata(false));
                    AzureActiveDirectory.putCloud(authorityUrl.getHost(), new AzureActiveDirectoryCloud(false));
                    Logger.v(TAG + methodName, "Fail to get authority validation metadata back. Ignore the failure since authority validation is turned off.");
                }
            }
            // Even if it succeeds, we cannot mark the authority as validated authority.
            apiEvent.setValidationStatus(EventStrings.AUTHORITY_VALIDATION_NOT_DONE);
            Telemetry.getInstance().stopEvent(authenticationRequest.getTelemetryRequestId(), apiEvent,
                    EventStrings.AUTHORITY_VALIDATION_EVENT);
        }

        final InstanceDiscoveryMetadata metadata = AuthorityValidationMetadataCache.getCachedInstanceDiscoveryMetadata(authorityUrl);
        if (metadata == null || !metadata.isValidated()) {
            return;
        }

        updatePreferredNetworkLocation(authorityUrl, authenticationRequest, metadata);
    }

    private void updatePreferredNetworkLocation(final URL authorityUrl, final AuthenticationRequest request, final InstanceDiscoveryMetadata metadata)
            throws AuthenticationException {
        if (metadata == null || !metadata.isValidated()) {
            return;
        }

        // replace the authority if host is not the same as the original one.
        if (metadata.getPreferredNetwork() != null && !authorityUrl.getHost().equalsIgnoreCase(metadata.getPreferredNetwork())) {
            try {
                final URL replacedAuthority = Discovery.constructAuthorityUrl(authorityUrl, metadata.getPreferredNetwork());
                request.setAuthority(replacedAuthority.toString());
            } catch (final MalformedURLException ex) {
                //Intentionally empty.
                Logger.i(TAG, "preferred network is invalid", "use exactly the same authority url that is passed");
            }
        }
    }

    /**
     * Perform authority validation.
     * True if the passed in authority is valid, false otherwise.
     */
    private void validateAuthority(final URL authorityUrl,
                                   @Nullable final String domain,
                                   boolean isSilent,
                                   final UUID correlationId) throws AuthenticationException {
        final String methodName = ":validateAuthority";
        boolean isAdfsAuthority = UrlExtensions.isADFSAuthority(authorityUrl);
        final boolean isAuthorityValidated = AuthorityValidationMetadataCache.isAuthorityValidated(authorityUrl);
        if (isAuthorityValidated || isAdfsAuthority && mAuthContext.getIsAuthorityValidated()) {
            return;
        }

        Logger.v(TAG + methodName, "Start validating authority");
        mDiscovery.setCorrelationId(correlationId);

        Discovery.verifyAuthorityValidInstance(authorityUrl);

        if (!isSilent && isAdfsAuthority && domain != null) {
            mDiscovery.validateAuthorityADFS(authorityUrl, domain);
        } else {
            if (isSilent && UrlExtensions.isADFSAuthority(authorityUrl)) {
                Logger.v(TAG + methodName, "Silent request. Skipping AD FS authority validation");
            }

            mDiscovery.validateAuthority(authorityUrl);
        }

        Logger.v(TAG + methodName, "The passed in authority is valid.");
        mAuthContext.setIsAuthorityValidated(true);
    }

    /**
     * 1. For Silent flow, we should always try to look local cache first.
     * i> If valid AT is returned from cache, use it.
     * ii> If no valid AT is returned, but RT is returned, use the RT.
     * iii> If RT request fails, and if we can talk to broker, go to broker and check if there is a valid token.
     * 2. For Non-Silent flow.
     * i> Do silent cache lookup first, same as 1.
     * a) If we can talk to broker, go to broker for auth.
     * b) If not, launch webview with embedded flow.
     * If silent request succeeds, we'll return the token back via callback.
     * If silent request fails and no prompt is allowed, we'll return the exception back via callback.
     * If silent request fails and prompt is allowed, we'll prompt the user and launch webview.
     */
    private void performAcquireTokenRequest(final CallbackHandler callbackHandle,
                                            final IWindowComponent activity,
                                            final boolean useDialog,
                                            final AuthenticationRequest authenticationRequest)
            throws AuthenticationException {

        // tryAcquireTokenSilent will:
        // 1) throw AuthenticationException if
        //    a) No access token is returned from local flow and no prompt is allowed
        //    b) Server error or network errors when sending post request to token endpoint with grant_type
        //       as refresh_token in local flow or with grant type as assertion
        //    c) Broker returns ERROR_CODE_BAD_ARGUMENTS, ACCOUNT_MANAGER_ERROR_CODE_BAD_AUTHENTICATION
        //       or ERROR_CODE_UNSUPPORTED_OPERATION
        // 2) Non-null AuthenticationResult if we could try silent request, and silent request either successfully
        //    return the token back or returns the AuthenticationResult containing oauth error
        // 3) Null AuthenticationResult if
        //    a) we cannot try silent request
        //    b) silent request returns a null result. Broker will return a null result if 1) no matching account in
        //       broker 2) broker doesn't return any token back.
        final String methodName = ":performAcquireTokenRequest";
        final AuthenticationResult authenticationResultFromSilentRequest = tryAcquireTokenSilent(authenticationRequest);
        if (isAccessTokenReturned(authenticationResultFromSilentRequest)) {
            mAPIEvent.setWasApiCallSuccessful(true, null);
            mAPIEvent.setCorrelationId(authenticationRequest.getCorrelationId().toString());
            mAPIEvent.setIdToken(authenticationResultFromSilentRequest.getIdToken());
            mAPIEvent.stopTelemetryAndFlush();
            callbackHandle.onSuccess(authenticationResultFromSilentRequest);
            return;
        }

        Logger.d(TAG + methodName, "Trying to acquire token interactively.");
        acquireTokenInteractiveFlow(callbackHandle, activity, useDialog, authenticationRequest);
    }

    private AuthenticationResult tryAcquireTokenSilent(final AuthenticationRequest authenticationRequest)
            throws AuthenticationException {
        final String methodName = ":tryAcquireTokenSilent";
        AuthenticationResult authenticationResult = null;

        if (shouldTrySilentFlow(authenticationRequest)) {
            Logger.v(TAG + methodName, "Try to acquire token silently, return valid AT or use RT in the cache.");
            authenticationResult = acquireTokenSilentFlow(authenticationRequest);

            final boolean isAccessTokenReturned = isAccessTokenReturned(authenticationResult);
            // Silent request, if token not returned, return AUTH_REFRESH_FAILED_PROMPT_NOT_ALLOWED back
            // to developer.
            if (!isAccessTokenReturned && authenticationRequest.isSilent()) {
                // TODO: investigate which server response actually should force user to sign in again
                // and which error actually should just notify user that some resource require extra steps

                final String errorInfo = authenticationResult == null
                        ? "No result returned from acquireTokenSilent" : " ErrorCode:" + authenticationResult.getErrorCode();
                // User does not want to launch activity
                Logger.e(TAG + methodName,
                        "Prompt is not allowed and failed to get token. " + errorInfo,
                        authenticationRequest.getLogInfo(),
                        ADALError.AUTH_REFRESH_FAILED_PROMPT_NOT_ALLOWED);
                final AuthenticationException authenticationException = new AuthenticationException(
                        ADALError.AUTH_REFRESH_FAILED_PROMPT_NOT_ALLOWED, authenticationRequest.getLogInfo()
                        + " " + errorInfo);

                addHttpInfoToException(authenticationResult, authenticationException);

                throw authenticationException;
            }

            if (isAccessTokenReturned) {
                Logger.v(TAG + methodName, "Token is successfully returned from silent flow. ");
            }
        }

        return authenticationResult;
    }

    private void addHttpInfoToException(AuthenticationResult result, AuthenticationException exception) {
        if (result != null && exception != null) {
            if (result.getHttpResponseHeaders() != null) {
                exception.setHttpResponseHeaders(result.getHttpResponseHeaders());
            }

            if (result.getHttpResponseBody() != null) {
                exception.setHttpResponseBody(result.getHttpResponseBody());
            }
            exception.setServiceStatusCode(result.getServiceStatusCode());
        }
    }

    private boolean shouldTrySilentFlow(final AuthenticationRequest authenticationRequest) {
        boolean result = true;
        if (authenticationRequest.isClaimsChallengePresent()) {
            result = checkIfBrokerHasLltChanges();
        }
        return authenticationRequest.isSilent() || (result && authenticationRequest.getPrompt() == PromptBehavior.Auto);
    }

    /**
     * The previous behavior to always  do an interactive call if claims challenge is present is changed with long live token feature.
     * However to support the case were the broker app is not updated to have Llt changes, we check the version code of the both the broker
     * and retain the old behavior.
     */
    private boolean checkIfBrokerHasLltChanges() {
        final PackageManager packageManager = mContext.getPackageManager();
        long authVersionCode = Long.MAX_VALUE;
        long cpVersionCode = Long.MAX_VALUE;
        long brokerHostVersionCode = Integer.MAX_VALUE;

        try {
            final PackageInfo authPackageInfo = packageManager.getPackageInfo(AZURE_AUTHENTICATOR_APP_PACKAGE_NAME, 0);
            authVersionCode = PackageInfoCompat.getLongVersionCode(authPackageInfo);
        } catch (final PackageManager.NameNotFoundException ignored) {
        }

        try {
            final PackageInfo cpPackageInfo = packageManager.getPackageInfo(COMPANY_PORTAL_APP_PACKAGE_NAME, 0);
            cpVersionCode = PackageInfoCompat.getLongVersionCode(cpPackageInfo);
        } catch (final PackageManager.NameNotFoundException ignored) {
        }

        try {
            final PackageInfo brokerHostPackageInfo = packageManager.getPackageInfo(BROKER_HOST_APP_PACKAGE_NAME, 0);
            brokerHostVersionCode = PackageInfoCompat.getLongVersionCode(brokerHostPackageInfo);
        } catch (final PackageManager.NameNotFoundException ignored) {
        }

        return authVersionCode >= AUTHENTICATOR_LLT_VERSION_CODE
                && cpVersionCode >= CP_LLT_VERSION_CODE
                && brokerHostVersionCode >= CP_LLT_VERSION_CODE;

    }

    /**
     * Handles the silent flow. Will always lookup local cache. If there is a valid AT in local cache, will use it. If
     * AT in local cache is already expired, will try RT in the local cache. If RT requst failed, then use saml assertion passed in the
     * request to acquire RT and AT. If this too fails and if we can switch to broker for auth,
     * will switch to broker for authentication.
     */
    private AuthenticationResult acquireTokenSilentFlow(final AuthenticationRequest authenticationRequest)
            throws AuthenticationException {

        final boolean requestEligibleForBroker = mBrokerProxy.verifyBrokerForSilentRequest(authenticationRequest);
        final String methodName = ":acquireTokenSilentFlow";

        //1. if forceRefresh == true OR claimsChallenge is not null AND the request is eligible for the broker
        if ((authenticationRequest.getForceRefresh() || authenticationRequest.isClaimsChallengePresent()) && requestEligibleForBroker) {
            return tryAcquireTokenSilentWithBroker(authenticationRequest);
        }

        //2. Try to acquire silent locally
        final AuthenticationResult authResult = tryAcquireTokenSilentLocally(authenticationRequest);
        if (isAccessTokenReturned(authResult)) {
            return authResult;
        }

        //3. try SAML Assertion
        if (authenticationRequest.getSamlAssertion() != null && authenticationRequest.getAssertionType() != null) {
            final AuthenticationResult authResultFromSaml = tryAcquireTokenSilentWithAssertion(authenticationRequest);
            if (isAccessTokenReturned(authResultFromSaml)) {
                Logger.v(TAG + methodName, "Access token has been acquired using the saml assertion.");
                return authResultFromSaml;
            } else {
                Logger.w(TAG + methodName, "Failed to acquire tokens using saml assertion.");
            }
        }

        //4. We couldn't get locally...If eligible return via broker... otherwise return local result
        if (requestEligibleForBroker) {
            return tryAcquireTokenSilentWithBroker(authenticationRequest);
        } else {
            return authResult;
        }

    }

    /**
     * Try acquire token silent locally.
     */
    private AuthenticationResult tryAcquireTokenSilentLocally(final AuthenticationRequest authenticationRequest)
            throws AuthenticationException {
        final String methodName = ":tryAcquireTokenSilentLocally";
        Logger.v(TAG + methodName, "Try to silently get token from local cache.");
        final AcquireTokenSilentHandler acquireTokenSilentHandler = new AcquireTokenSilentHandler(mContext,
                authenticationRequest, mTokenCacheAccessor);

        return acquireTokenSilentHandler.getAccessToken();
    }

    /**
     * Try acquire token using saml assertion.
     */
    private AuthenticationResult tryAcquireTokenSilentWithAssertion(final AuthenticationRequest authenticationRequest)
            throws AuthenticationException {
        final String methodName = ":tryAcquireTokenSilentWithAssertion";
        Logger.v(TAG + methodName, "Try to silently get token using SAML Assertion.");
        final AcquireTokenSilentHandler acquireTokenSilentHandler = new AcquireTokenSilentHandler(mContext,
                authenticationRequest, mTokenCacheAccessor);

        return acquireTokenSilentHandler.getAccessTokenUsingAssertion();
    }

    /**
     * Try acquire token silent with broker.
     */
    private AuthenticationResult tryAcquireTokenSilentWithBroker(final AuthenticationRequest authenticationRequest)
            throws AuthenticationException {

        final String methodName = ":tryAcquireTokenSilentWithBroker";

        // If we can try with broker for silent flow, it indicates ADAL can switch to broker for auth. Even broker does
