//
// 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
import AWSCognitoIdentityProvider

class FetchUserPoolTokensOperation: AWSAsyncOperation {

    // Used to identify the operation.
    //
    // Identifier is mainly used in logs to identify different
    // operations and its lifecycle
    private let identifier: String = UUID().uuidString

    // Completion to be called when the operation completes.
    //
    private let completion: TokenCompletion

    // Holds an instance of the user pool low level SDK
    //
    private let userPool: CognitoUserPoolBehavior

    public weak var delegate: FetchUserPoolTokensDelegate?
    private var tokenFetchState: TokenFetchState = .notStarted

    // Synchronizes access to `tokenFetchState`. The state is mutated through this queue, we should
    // make sure that the state is not mutated/read outside this queue.
    private let stateQueue = DispatchQueue(
        label: "com.amazonaws.awsmobileClient.tokenFetch.state",
        target: DispatchQueue.global())

    init(userPool: CognitoUserPoolBehavior = AWSUserPoolClientHelper.userPoolClient(for: "tokenFetchUserPool"),
         completion: @escaping TokenCompletion) {
        self.completion = completion
        self.userPool = userPool
        AWSMobileClientLogging.verbose("\(identifier) Created FetchUserPoolTokensOperation")
    }

    override func main() {
        AWSMobileClientLogging.verbose("\(identifier) Start execution")
        acceptEvent(.startOperation)
    }

    func acceptEvent(_ event: TokenFetchEvent) {
        guard !self.isFinished else { return }
        
        stateQueue.async { [weak self] in
            guard let self = self else { return }

            let newState = self.tokenFetchState.resolveState(event: event)
            AWSMobileClientLogging.verbose("""
            \(self.identifier) \(self.tokenFetchState): \(event) -> \(newState)
            """)
            if newState == self.tokenFetchState { return }
            self.tokenFetchState = newState

            // Based on the newState perform the effect
            switch self.tokenFetchState {
            case .fetching:
                DispatchQueue.global().async {
                    self.fetchToken()
                }
            case .waitingForSignIn:
                DispatchQueue.global().async {
                    self.delegate?.tokenFetchNeedsAuthentication(operation: self)
                }

            case .fetched(let tokens):
                self.finishWithResult(tokens: tokens)

            case .error(let internalError):
                switch internalError {
                case .unableToSignIn:
                    self.finishWithUnableToSignIn()
                case .noUserFound:
                    self.finishWithNotSignedInError()
                case .serviceError(let error):
                    self.finishWithError(error: error)
                }
            default:
                break
            }
        }
    }

    func authStateChanged(_ state: UserState) {
        switch state {
        case .signedOut, .guest:
            acceptEvent(.signedOut)
        case .signedIn:
            acceptEvent(.signedIn)
        default:
            break
        }
    }

    private func fetchToken() {

        AWSMobileClientLogging.verbose("\(self.identifier) Inside fetch token")

        guard
            !self.isCancelled
        else {
            AWSMobileClientLogging.verbose("\(self.identifier) Cancelled")
            finish()
            return
        }

        guard
            let username = self.delegate?.getCurrentUsername(operation: self),
            !username.isEmpty
        else {
            AWSMobileClientLogging.verbose("\(self.identifier)  No User name")
            self.acceptEvent(.noUserFound)
            return
        }

        let user = self.userPool.getIdentityUser(username)
        user.getUserPoolToken { [weak self] result in
            guard let self = self else { return }

            guard
                !self.isCancelled
            else {
                AWSMobileClientLogging.verbose("\(self.identifier) Cancelled")
                self.finish()
                return
            }

            switch result {
            case .success(let tokens):
                self.acceptEvent(.tokenFetched(tokens))
            case .failure(let error):
                if (error as NSError).didTokenExpire {
                    AWSMobileClientLogging.verbose("\(self.identifier) Need re-authentication")
                    self.acceptEvent(.tokenExpired)

                } else {
                    self.acceptEvent(.serviceError(error))
                }
            }
        }
    }

    private func finishWithUnableToSignIn() {
        let message = AWSMobileClientConstants.noValidSignInSession
        let error = AWSMobileClientError.unableToSignIn(message: message)
        finishWithError(error: error)
    }

    private func finishWithNotSignedInError() {
        let message = AWSMobileClientConstants.notSignedInMessage
        let error = AWSMobileClientError.notSignedIn(message: message)
        finishWithError(error: error)
    }

    private func finishWithResult(tokens: Tokens) {
        AWSMobileClientLogging.verbose("\(self.identifier) Success")
        completion(tokens, nil)
        finish()
    }

    private func finishWithError(error: Error) {
        AWSMobileClientLogging.verbose("\(self.identifier) Error")
        completion(nil, error)
        finish()
    }
}

extension NSError {

    // The underlying SDK returns different errors when the user token is expired based on the signIn
    // mechanism.
    //
    // In case if user is signedin via non-HostedUI method and if the error returned is
    // `InvalidAuthenticationDelegate` it means that userPool client is trying to get
    // authentication details, which means that the refresh token is expired.
    // On the other hand if the user is signedin via HostedUI, the SDK returns
    // `errorExpiredRefreshToken` when the token expire.
    //
    var didTokenExpire: Bool {
        let authCode = AWSCognitoIdentityClientErrorType.providerClientErrorInvalidAuthenticationDelegate.rawValue
        if domain == AWSCognitoIdentityProviderErrorDomain,
           code == authCode  {
            return true
        } else if domain == AWSCognitoAuthErrorDomain,
                  code == AWSCognitoAuthClientErrorType.errorExpiredRefreshToken.rawValue {
            return true
        }
        return false
    }
}
