//
// Copyright 2017-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License").
// You may not use this file except in compliance with the License.
// A copy of the License is located at
//
// http://aws.amazon.com/apache2.0
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
// express or implied. See the License for the specific language governing
// permissions and limitations under the License.
//

import Foundation

extension AWSMobileClient {

    /// Asynchronous signout method which requires network activity. Based on the options specified in `SignOutOptions`, appropriate tasks will be performed.
    ///
    /// - Parameters:
    ///   - options: SignOutOptions which specify actions.
    ///   - presentationAnchor: If you have
    ///   - completionHandler: completion handler for success or error callback.
    @available(iOS 13, *)
    public func signOut(presentationAnchor: ASPresentationAnchor,
                        options: SignOutOptions = SignOutOptions(),
                        completionHandler: @escaping ((Error?) -> Void)) {
        internalSignOut(presentationAnchor: presentationAnchor,
                        options: options,
                        completionHandler: completionHandler)
    }

    /// Asynchronous signout method which requires network activity. Based on the options specified in `SignOutOptions`, appropriate tasks will be performed.
    ///
    /// - Parameters:
    ///   - options: SignOutOptions which specify actions.
    ///   - completionHandler: completion handler for success or error callback.
    public func signOut(options: SignOutOptions = SignOutOptions(),
                        completionHandler: @escaping ((Error?) -> Void)) {
        internalSignOut(presentationAnchor: nil,
                        options: options,
                        completionHandler: completionHandler)
    }

    /// Signs out the current logged in user and clears the local keychain store.
    /// Note: This does not invalidate the tokens from the service or sign out the user from other devices.
    public func signOut() {
        self.credentialsFetchCancellationSource.cancel()
        if federationProvider == .hostedUI {
            AWSCognitoAuth.init(forKey: AWSMobileClientConstants.CognitoAuthRegistrationKey).signOutLocallyAndClearLastKnownUser()
        }
        self.cachedLoginsMap = [:]
        self.customRoleArnInternal = nil
        self.setCustomRoleArnInternal(nil, for: self)
        self.saveLoginsMapInKeychain()
        self.setLoginProviderMetadataAndSaveInKeychain(provider: .none)
        self.performUserPoolSignOut()
        self.internalCredentialsProvider?.identityProvider.identityId = nil
        self.internalCredentialsProvider?.clearKeychain()
        self.mobileClientStatusChanged(userState: .signedOut, additionalInfo: [:])
        self.federationProvider = .none
        self.credentialsFetchCancellationSource = AWSCancellationTokenSource()
        self.clearHostedUIOptionsScopesFromKeychain()
    }

    /// Invoke this function to release any sign-in waits.
    /// When you receive a notifcation which is `signedOutFederatedTokensInvalid` or `signedOutUserPoolsTokensInvalid`
    /// you need to provide SDK the token via `federate` method or call the `signIn` method and complete the sign-in flow.
    /// If you can't get the latest token from the user, you can call this method to un-block any waiting calls.
    public func releaseSignInWait() {
        for operation in tokenOperations.allObjects {
            operation.acceptEvent(.releaseWait)
        }
        if self.federationProvider == .userPools {
            self.userpoolOpsHelper?.customAuthChallengeTaskCompletionSource?.set(error: AWSMobileClientError.unableToSignIn(message: AWSMobileClientConstants.noValidSignInSession))
            self.userpoolOpsHelper?.customAuthChallengeTaskCompletionSource = nil
        } else if self.federationProvider == .oidcFederation {
            self.pendingAWSCredentialsCompletion?(nil, AWSMobileClientError.unableToSignIn(message: "Could not get valid federation token from the user."))
            self.pendingAWSCredentialsCompletion = nil
        }
    }

    private func handleHostedUISignOutResult(_ error: Error?, completionHandler: @escaping ((Error?) -> Void)) {
        if (error != nil) {
            completionHandler(AWSMobileClientError.makeMobileClientError(from: error!))
        } else {
            // If the cookie is successfully invalidated, we attempt to
            // revoke access tokens and then sign out locally.
            revokeAccessTokensAndSignOutLocally(completionHandler: completionHandler)
        }
    }

    private func internalSignOut(presentationAnchor: ASPresentationAnchor? = nil,
                                 options: SignOutOptions = SignOutOptions(),
                        completionHandler: @escaping ((Error?) -> Void)) {
        // If using hosted UI, we need to launch SFSafariVC/SFAuthSession/ASWebAuthenticationSession to invalidate cookie
        if federationProvider == .hostedUI {
            // Ensure UI actions are performed on the main thread since revoke token may be performed on a
            // background thread.
            DispatchQueue.main.async {
                if let anchor = presentationAnchor {
                    self.hostedUISignOut(presentationAnchor: anchor, completionHandler: completionHandler)
                } else {
                    self.hostedUILegacySignOut(completionHandler: completionHandler)
                }
            }
            return
        }
        // If using userpools sign in and global sign out is specified, we try logging out the user from all devices.
        if federationProvider == .userPools && options.signOutGlobally == true {
            guard let currentActiveUser = self.userpoolOpsHelper?.currentActiveUser else {
                completionHandler(Self.missingCurrentActiveUser())
                return
            }
            let _ = currentActiveUser.globalSignOut().continueWith { [weak self] (task) -> Any? in
                guard let self = self else {
                    let message = "Unexpectedly encountered nil when unwrapping self. This should not happen."
                    completionHandler(AWSMobileClientError.unknown(message: message))
                    return nil
                }
                if task.result != nil {
                    // If global signout is successful, we attempt to revoke access tokens and sign out locally.
                    self.revokeAccessTokensAndSignOutLocally(completionHandler: completionHandler)
                } else if let error = task.error {
                    // If there is an error signing out globally, we notify the developer.
                    completionHandler(AWSMobileClientError.makeMobileClientError(from: error))
                }
                return nil
            }
            return
        }
        // If using userpools sign in (not global sign out or federated sign in), revoke the token before signing out
        if federationProvider == .userPools {
            revokeAccessTokensAndSignOutLocally(completionHandler: completionHandler)
            return
        }
        // If signing in with federated sign in, perform local sign out
        signOut()
        completionHandler(nil)
    }

    internal func revokeAccessTokensAndSignOutLocally(completionHandler: @escaping ((Error?) -> Void)) {
        revokeIfSessionIsRevocable { _ in
            self.signOut()
            completionHandler(nil)
        }
    }

    /// Returns if the session is not revocable, attempts to revoke the token if it is.
    private func revokeIfSessionIsRevocable(completionHandler: @escaping ((Error?) -> Void)) {
        guard let userpoolOpsHelper = self.userpoolOpsHelper else {
            completionHandler(Self.missingUserpoolOpsHelperError())
            return
        }
        guard let isSessionRevocable = userpoolOpsHelper.currentActiveUser?.isSessionRevocable,
              isSessionRevocable else {
            completionHandler(nil)
            return
        }

        let _ = userpoolOpsHelper.currentActiveUser?.revokeToken().continueWith { (task) -> Any? in
            if let error = task.error {
                completionHandler(AWSMobileClientError.makeMobileClientError(from: error))
            } else if let _ = task.result {
                completionHandler(nil)
            }
            return nil
        }
    }

    private func hostedUISignOut(presentationAnchor: ASPresentationAnchor,
                                 completionHandler: @escaping ((Error?) -> Void)) {
        if #available(iOS 13, *) {
            AWSCognitoAuth.init(forKey: AWSMobileClientConstants.CognitoAuthRegistrationKey).signOut(withWebUI: presentationAnchor) { (error) in
                self.handleHostedUISignOutResult(error, completionHandler: completionHandler)
            }
        } else {
            // Fallback on earlier versions
            self.hostedUILegacySignOut(completionHandler: completionHandler)
        }
    }

    private func hostedUILegacySignOut(completionHandler: @escaping ((Error?) -> Void)) {
        AWSCognitoAuth.init(forKey: AWSMobileClientConstants.CognitoAuthRegistrationKey).signOut { (error) in
            self.handleHostedUISignOutResult(error, completionHandler: completionHandler)
        }
    }

    internal func performUserPoolSignOut() {
        guard let userpoolOpsHelper = self.userpoolOpsHelper else {
            invokeSignInCallback(signResult: nil, error: Self.missingUserpoolOpsHelperError())
            return
        }

        if let task = userpoolOpsHelper.customAuthChallengeTaskCompletionSource?.task, !task.isCompleted {
            let error = AWSMobileClientError.unableToSignIn(message: "Could not get end user to sign in.")
            userpoolOpsHelper.customAuthChallengeTaskCompletionSource?.set(error: error)
        }
        userpoolOpsHelper.customAuthChallengeTaskCompletionSource = nil
        invokeSignInCallback(signResult: nil, error: AWSMobileClientError.unableToSignIn(message: "Could not get end user to sign in."))
        self.userPoolClient?.clearAll()
    }
}
