//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import Amplify
import AWSCognitoAuthPlugin
import SwiftUI

/// The Base class for all State classes.
public class AuthenticatorBaseState: ObservableObject {
    /// Whether an operation is in progress
    @Published public var isBusy: Bool = false

    /// A message to be displayed
    @Published public var message: AuthenticatorMessage? = nil {
        didSet {
            credentials.message = nil
        }
    }

    @ObservedObject var credentials: Credentials

    var errorTransform: ((AuthError) -> AuthenticatorError)? = nil
    private(set) var authenticatorState: AuthenticatorStateProtocol = .empty

    init(credentials: Credentials) {
        self.credentials = credentials
    }

    func configure(with authenticatorState: AuthenticatorStateProtocol) {
        self.authenticatorState = authenticatorState
    }

    var authenticationService: AuthenticationService {
        return authenticatorState.authenticationService
    }

    var configuration: CognitoConfiguration {
        return authenticatorState.configuration
    }

    func setBusy(_ isBusy: Bool) {
        DispatchQueue.main.async {
            self.isBusy = isBusy
            if isBusy {
                self.message = nil
            }
        }
    }

    func setMessage(_ message: AuthenticatorMessage) {
        DispatchQueue.main.async {
            self.isBusy = false
            self.message = message
        }
    }

    func nextStep(for result: AuthSignInResult) async throws -> Step {
        log.verbose("Sign In next step is \(result.nextStep)")
        switch result.nextStep {
        case .confirmSignInWithSMSMFACode(let details, _):
            return .confirmSignInWithMFACode(deliveryDetails: details)
        case .confirmSignInWithCustomChallenge(_):
            return .confirmSignInWithCustomChallenge
        case .confirmSignInWithNewPassword(_):
            return .confirmSignInWithNewPassword
        case .resetPassword(_):
            return await nextStepForResetPassword()
        case .confirmSignUp(_):
            return .confirmSignUp(deliveryDetails: nil)
        case .done:
            let attributes = try await authenticationService.fetchUserAttributes(options: nil)
            var verifiedAttributes: [AuthUserAttributeKey] = []
            var unverifiedAttributes: [AuthUserAttributeKey] = []

            for attribute in attributes {
                guard attribute.key == .emailVerified || attribute.key == .phoneNumberVerified,
                      let isVerified = Bool(attribute.value) else {
                    continue
                }

                let verificationAttribute: AuthUserAttributeKey
                if attribute.key == .emailVerified {
                    verificationAttribute = .email
                } else {
                    verificationAttribute = .phoneNumber
                }

                if isVerified {
                    verifiedAttributes.append(verificationAttribute)
                } else {
                    unverifiedAttributes.append(verificationAttribute)
                }
            }

            if !verifiedAttributes.isEmpty || unverifiedAttributes.isEmpty {
                log.verbose("User is verified, moving to Signed In step")
                let user = try await authenticationService.getCurrentUser()
                return .signedIn(user: user)
            } else {
                log.verbose("User has attributes pending verification: \(unverifiedAttributes)")
                return .verifyUser(attributes: unverifiedAttributes)
            }
        }
    }

    func nextStep(for result: AuthSignUpResult) async throws -> Step {
        log.verbose("Sign Up next step is \(result.nextStep)")
        switch result.nextStep {
        case .confirmUser(let details, _, _):
            return .confirmSignUp(deliveryDetails: details)
        case .done:
            do {
                let signInResult = try await authenticationService.signIn(
                    username: credentials.username,
                    password: credentials.password,
                    options: nil
                )
                return try await nextStep(for: signInResult)
            } catch {
                // Unable to Sign In
                log.verbose("Unable to Sign In after sucessfull sign up")
                log.error(error)
                credentials.message = self.error(for: error)
                return .signIn
            }
        }
    }
    
    private func nextStepForResetPassword() async -> Step {
        do {
            let result = try await authenticationService.resetPassword(
                for: credentials.username,
                options: nil
            )

            log.verbose("Reset password next step is \(result.nextStep)")
            switch result.nextStep {
            case .confirmResetPasswordWithCode(let details, _):
                return .confirmResetPassword(deliveryDetails: details)
            case .done:
                log.warn("Received done next step after initiating a reset password. This is unexpected")
                // This should not happen, go back to Sign In screen
                return .signIn
            }
        } catch {
            log.verbose("Unable to initiate a password reset.")
            log.error(error)
            // Take the user to manually request a password reset
            return .resetPassword
        }
    }

    func localizedMessage(for details: AuthCodeDeliveryDetails?) -> String {
        guard let destination = details?.destination.value, !destination.isEmpty else {
            return "authenticator.banner.sendCodeGeneric".localized()
        }

        return "authenticator.banner.sendCode".localized(using: destination)
    }

    func error(for error: Error) -> AuthenticatorError {
        log.error(error)

        guard let authError = error as? AuthError else {
            return .unknown(from: error)
        }

        if let errorTransform = errorTransform {
            return errorTransform(authError)
        }

        if let localizedMessage = localizedMessage(for: authError) {
            return .error(
                message: localizedMessage
            )
        }

        return .unknown(from: error)
    }

    private func localizedMessage(for error: AuthError) -> String? {
        if case .notAuthorized(_, _, _) = error {
            return "authenticator.authError.incorrectCredentials".localized()
        }

        guard let cognitoError = error.underlyingError as? AWSCognitoAuthError else {
            log.verbose("Unable to localize error that is not of type AWSCognitoAuthError")
            return nil
        }

        let key = "authenticator.cognitoError.\(cognitoError)"
        let localized = key.localized()

        if key != localized {
            log.verbose("A localizable string was found for error of type '\(cognitoError)'")
            return localized
        }
        
        log.verbose("No localizable string was found for error of type '\(cognitoError)'")
        return nil
    }
}

extension AuthenticatorBaseState: Equatable {
    public static func == (lhs: AuthenticatorBaseState, rhs: AuthenticatorBaseState) -> Bool {
        lhs === rhs
    }
}

extension AuthenticatorBaseState: AuthenticatorLogging {}