// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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

// MARK: - URL response error codes

/// Error code that indicates that the client ID provided was invalid.
private let kURLResponseErrorCodeInvalidClientID = "auth/invalid-oauth-client-id"

/// Error code that indicates that a network request within the SFSafariViewController or WKWebView
/// failed.
private let kURLResponseErrorCodeNetworkRequestFailed = "auth/network-request-failed"

/// Error code that indicates that an internal error occurred within the
/// SFSafariViewController or WKWebView failed.
private let kURLResponseErrorCodeInternalError = "auth/internal-error"

private let kFIRAuthErrorMessageMalformedJWT =
  "Failed to parse JWT. Check the userInfo dictionary for the full token."

@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
class AuthErrorUtils {
  static let internalErrorDomain = "FIRAuthInternalErrorDomain"
  static let userInfoDeserializedResponseKey = "FIRAuthErrorUserInfoDeserializedResponseKey"
  static let userInfoDataKey = "FIRAuthErrorUserInfoDataKey"

  /// This marker indicates that the server error message contains a detail error message which
  /// should be used instead of the hardcoded client error message.
  private static let kServerErrorDetailMarker = " : "

  static func error(code: SharedErrorCode, userInfo: [String: Any]? = nil) -> Error {
    switch code {
    case let .public(publicCode):
      var errorUserInfo: [String: Any] = userInfo ?? [:]
      if errorUserInfo[NSLocalizedDescriptionKey] == nil {
        errorUserInfo[NSLocalizedDescriptionKey] = publicCode.errorDescription
      }
      if let localizedDescription = errorUserInfo[NSLocalizedDescriptionKey] as? String,
         localizedDescription == "" {
        errorUserInfo[NSLocalizedDescriptionKey] = publicCode.errorDescription
      }
      errorUserInfo[AuthErrors.userInfoNameKey] = publicCode.errorCodeString
      return NSError(
        domain: AuthErrors.domain,
        code: publicCode.rawValue,
        userInfo: errorUserInfo
      )
    case let .internal(internalCode):
      // This is an internal error. Wrap it in an internal error.
      let error = NSError(
        domain: internalErrorDomain,
        code: internalCode.rawValue,
        userInfo: userInfo
      )

      return self.error(code: .public(.internalError), underlyingError: error)
    }
  }

  static func error(code: SharedErrorCode, underlyingError: Error?) -> Error {
    var errorUserInfo: [String: Any]?
    if let underlyingError = underlyingError {
      errorUserInfo = [NSUnderlyingErrorKey: underlyingError]
    }
    return error(code: code, userInfo: errorUserInfo)
  }

  static func error(code: AuthErrorCode, underlyingError: Error?) -> Error {
    error(code: SharedErrorCode.public(code), underlyingError: underlyingError)
  }

  static func error(code: AuthErrorCode, userInfo: [String: Any]? = nil) -> Error {
    error(code: SharedErrorCode.public(code), userInfo: userInfo)
  }

  static func error(code: AuthErrorCode, message: String?) -> Error {
    let userInfo: [String: Any]?
    if let message {
      userInfo = [NSLocalizedDescriptionKey: message]
    } else {
      userInfo = nil
    }
    return error(code: SharedErrorCode.public(code), userInfo: userInfo)
  }

  static func userDisabledError(message: String?) -> Error {
    error(code: .userDisabled, message: message)
  }

  static func wrongPasswordError(message: String?) -> Error {
    error(code: .wrongPassword, message: message)
  }

  static func tooManyRequestsError(message: String?) -> Error {
    error(code: .tooManyRequests, message: message)
  }

  static func invalidCustomTokenError(message: String?) -> Error {
    error(code: .invalidCustomToken, message: message)
  }

  static func customTokenMismatchError(message: String?) -> Error {
    error(code: .customTokenMismatch, message: message)
  }

  static func invalidCredentialError(message: String?) -> Error {
    error(code: .invalidCredential, message: message)
  }

  static func requiresRecentLoginError(message: String?) -> Error {
    error(code: .requiresRecentLogin, message: message)
  }

  static func invalidUserTokenError(message: String?) -> Error {
    error(code: .invalidUserToken, message: message)
  }

  static func invalidEmailError(message: String?) -> Error {
    error(code: .invalidEmail, message: message)
  }

  static func providerAlreadyLinkedError() -> Error {
    error(code: .providerAlreadyLinked)
  }

  static func noSuchProviderError() -> Error {
    error(code: .noSuchProvider)
  }

  static func userTokenExpiredError(message: String?) -> Error {
    error(code: .userTokenExpired, message: message)
  }

  static func userNotFoundError(message: String?) -> Error {
    error(code: .userNotFound, message: message)
  }

  static func invalidAPIKeyError() -> Error {
    error(code: .invalidAPIKey)
  }

  static func userMismatchError() -> Error {
    error(code: .userMismatch)
  }

  static func operationNotAllowedError(message: String?) -> Error {
    error(code: .operationNotAllowed, message: message)
  }

  static func weakPasswordError(serverResponseReason reason: String?) -> Error {
    let userInfo: [String: Any]?
    if let reason, !reason.isEmpty {
      userInfo = [
        NSLocalizedFailureReasonErrorKey: reason,
      ]
    } else {
      userInfo = nil
    }
    return error(code: .weakPassword, userInfo: userInfo)
  }

  static func appNotAuthorizedError() -> Error {
    error(code: .appNotAuthorized)
  }

  static func expiredActionCodeError(message: String?) -> Error {
    error(code: .expiredActionCode, message: message)
  }

  static func invalidActionCodeError(message: String?) -> Error {
    error(code: .invalidActionCode, message: message)
  }

  static func invalidMessagePayloadError(message: String?) -> Error {
    error(code: .invalidMessagePayload, message: message)
  }

  static func invalidSenderError(message: String?) -> Error {
    error(code: .invalidSender, message: message)
  }

  static func invalidRecipientEmailError(message: String?) -> Error {
    error(code: .invalidRecipientEmail, message: message)
  }

  static func missingIosBundleIDError(message: String?) -> Error {
    error(code: .missingIosBundleID, message: message)
  }

  static func missingAndroidPackageNameError(message: String?) -> Error {
    error(code: .missingAndroidPackageName, message: message)
  }

  static func invalidRecaptchaTokenError() -> Error {
    error(code: .invalidRecaptchaToken)
  }

  static func unauthorizedDomainError(message: String?) -> Error {
    error(code: .unauthorizedDomain, message: message)
  }

  static func invalidContinueURIError(message: String?) -> Error {
    error(code: .invalidContinueURI, message: message)
  }

  static func missingContinueURIError(message: String?) -> Error {
    error(code: .missingContinueURI, message: message)
  }

  static func missingEmailError(message: String?) -> Error {
    error(code: .missingEmail, message: message)
  }

  static func missingPhoneNumberError(message: String?) -> Error {
    error(code: .missingPhoneNumber, message: message)
  }

  static func invalidPhoneNumberError(message: String?) -> Error {
    error(code: .invalidPhoneNumber, message: message)
  }

  static func missingVerificationCodeError(message: String?) -> Error {
    error(code: .missingVerificationCode, message: message)
  }

  static func invalidVerificationCodeError(message: String?) -> Error {
    error(code: .invalidVerificationCode, message: message)
  }

  static func missingVerificationIDError(message: String?) -> Error {
    error(code: .missingVerificationID, message: message)
  }

  static func invalidVerificationIDError(message: String?) -> Error {
    error(code: .invalidVerificationID, message: message)
  }

  static func sessionExpiredError(message: String?) -> Error {
    error(code: .sessionExpired, message: message)
  }

  static func missingAppCredential(message: String?) -> Error {
    error(code: .missingAppCredential, message: message)
  }

  static func invalidAppCredential(message: String?) -> Error {
    error(code: .invalidAppCredential, message: message)
  }

  static func quotaExceededError(message: String?) -> Error {
    error(code: .quotaExceeded, message: message)
  }

  static func missingAppTokenError(underlyingError: Error?) -> Error {
    error(code: .missingAppToken, underlyingError: underlyingError)
  }

  static func localPlayerNotAuthenticatedError() -> Error {
    error(code: .localPlayerNotAuthenticated)
  }

  static func gameKitNotLinkedError() -> Error {
    error(code: .gameKitNotLinked)
  }

  static func RPCRequestEncodingError(underlyingError: Error) -> Error {
    error(code: .internal(.RPCRequestEncodingError), underlyingError: underlyingError)
  }

  static func JSONSerializationErrorForUnencodableType() -> Error {
    error(code: .internal(.JSONSerializationError))
  }

  static func JSONSerializationError(underlyingError: Error) -> Error {
    error(code: .internal(.JSONSerializationError), underlyingError: underlyingError)
  }

  static func networkError(underlyingError: Error) -> Error {
    error(code: .networkError, underlyingError: underlyingError)
  }

  static func emailAlreadyInUseError(email: String?) -> Error {
    var userInfo: [String: Any]?
    if let email, !email.isEmpty {
      userInfo = [AuthErrors.userInfoEmailKey: email]
    }
    return error(code: .emailAlreadyInUse, userInfo: userInfo)
  }

  static func credentialAlreadyInUseError(message: String?,
                                          credential: AuthCredential?,
                                          email: String?) -> Error {
    var userInfo: [String: Any] = [:]
    if let credential {
      userInfo[AuthErrors.userInfoUpdatedCredentialKey] = credential
    }
    if let email, !email.isEmpty {
      userInfo[AuthErrors.userInfoEmailKey] = email
    }
    if !userInfo.isEmpty {
      return error(code: .credentialAlreadyInUse, userInfo: userInfo)
    }
    return error(code: .credentialAlreadyInUse, message: message)
  }

  static func webContextAlreadyPresentedError(message: String?) -> Error {
    error(code: .webContextAlreadyPresented, message: message)
  }

  static func webContextCancelledError(message: String?) -> Error {
    error(code: .webContextCancelled, message: message)
  }

  static func appVerificationUserInteractionFailure(reason: String?) -> Error {
    let userInfo: [String: Any]?
    if let reason, !reason.isEmpty {
      userInfo = [NSLocalizedFailureReasonErrorKey: reason]
    } else {
      userInfo = nil
    }
    return error(code: .appVerificationUserInteractionFailure, userInfo: userInfo)
  }

  static func webSignInUserInteractionFailure(reason: String?) -> Error {
    let userInfo: [String: Any]?
    if let reason, !reason.isEmpty {
      userInfo = [NSLocalizedFailureReasonErrorKey: reason]
    } else {
      userInfo = nil
    }
    return error(code: .webSignInUserInteractionFailure, userInfo: userInfo)
  }

  static func urlResponseError(code: String, message: String?) -> Error {
    let errorCode: AuthErrorCode
    switch code {
    case kURLResponseErrorCodeInvalidClientID:
      errorCode = .invalidClientID
    case kURLResponseErrorCodeNetworkRequestFailed:
      errorCode = .webNetworkRequestFailed
    case kURLResponseErrorCodeInternalError:
      errorCode = .webInternalError
    default:
      return AuthErrorUtils.webSignInUserInteractionFailure(reason: "[\(code)] - \(message ?? "")")
    }
    return error(code: errorCode, message: message)
  }

  static func nullUserError(message: String?) -> Error {
    error(code: .nullUser, message: message)
  }

  static func invalidProviderIDError(message: String?) -> Error {
    error(code: .invalidProviderID, message: message)
  }

  static func invalidHostingLinkDomainError(message: String?) -> Error {
    error(code: .invalidHostingLinkDomain, message: message)
  }

  static func missingOrInvalidNonceError(message: String?) -> Error {
    error(code: .missingOrInvalidNonce, message: message)
  }

  static func keychainError(function: String, status: OSStatus) -> Error {
    let message = SecCopyErrorMessageString(status, nil) as String? ?? ""
    let reason = "\(function) (\(status)) \(message)"
    return error(code: .keychainError, userInfo: [NSLocalizedFailureReasonErrorKey: reason])
  }

  static func tenantIDMismatchError() -> Error {
    error(code: .tenantIDMismatch)
  }

  static func unsupportedTenantOperationError() -> Error {
    error(code: .unsupportedTenantOperation)
  }

  static func notificationNotForwardedError() -> Error {
    error(code: .notificationNotForwarded)
  }

  static func appNotVerifiedError(message: String?) -> Error {
    error(code: .appNotVerified, message: message)
  }

  static func missingClientIdentifierError(message: String?) -> Error {
    error(code: .missingClientIdentifier, message: message)
  }

  static func missingClientType(message: String?) -> Error {
    error(code: .missingClientType, message: message)
  }

  static func captchaCheckFailedError(message: String?) -> Error {
    error(code: .captchaCheckFailed, message: message)
  }

  static func unexpectedResponse(data: Data?, underlyingError: Error?) -> Error {
    var userInfo: [String: Any] = [:]
    if let data {
      userInfo[userInfoDataKey] = data
    }
    if let underlyingError {
      userInfo[NSUnderlyingErrorKey] = underlyingError
    }
    return error(code: .internal(.unexpectedResponse), userInfo: userInfo)
  }

  static func unexpectedErrorResponse(data: Data?,
                                      underlyingError: Error?) -> Error {
    var userInfo: [String: Any] = [:]
    if let data {
      userInfo[userInfoDataKey] = data
    }
    if let underlyingError {
      userInfo[NSUnderlyingErrorKey] = underlyingError
    }
    return error(code: .internal(.unexpectedErrorResponse), userInfo: userInfo)
  }

  static func unexpectedErrorResponse(deserializedResponse: Any?) -> Error {
    var userInfo: [String: Any]?
    if let deserializedResponse {
      userInfo = [userInfoDeserializedResponseKey: deserializedResponse]
    }
    return error(code: .internal(.unexpectedErrorResponse), userInfo: userInfo)
  }

  static func unexpectedResponse(deserializedResponse: Any?) -> Error {
    var userInfo: [String: Any]?
    if let deserializedResponse {
      userInfo = [userInfoDeserializedResponseKey: deserializedResponse]
    }
    return error(code: .internal(.unexpectedResponse), userInfo: userInfo)
  }

  static func unexpectedResponse(deserializedResponse: Any?,
                                 underlyingError: Error?) -> Error {
    var userInfo: [String: Any] = [:]
    if let deserializedResponse {
      userInfo[userInfoDeserializedResponseKey] = deserializedResponse
    }
    if let underlyingError {
      userInfo[NSUnderlyingErrorKey] = underlyingError
    }
    return error(code: .internal(.unexpectedResponse), userInfo: userInfo)
  }

  static func unexpectedErrorResponse(deserializedResponse: Any?,
                                      underlyingError: Error?) -> Error {
    var userInfo: [String: Any] = [:]
    if let deserializedResponse {
      userInfo[userInfoDeserializedResponseKey] = deserializedResponse
    }
    if let underlyingError {
      userInfo[NSUnderlyingErrorKey] = underlyingError
    }
    return error(
      code: .internal(.unexpectedErrorResponse),
      userInfo: userInfo.isEmpty ? nil : userInfo
    )
  }

  static func malformedJWTError(token: String, underlyingError: Error?) -> Error {
    var userInfo: [String: Any] = [
      NSLocalizedDescriptionKey: kFIRAuthErrorMessageMalformedJWT,
      userInfoDataKey: token,
    ]
    if let underlyingError {
      userInfo[NSUnderlyingErrorKey] = underlyingError
    }
    return error(code: .malformedJWT, userInfo: userInfo)
  }

  static func RPCResponseDecodingError(deserializedResponse: Any?,
                                       underlyingError: Error?) -> Error {
    var userInfo: [String: Any] = [:]
    if let deserializedResponse {
      userInfo[userInfoDeserializedResponseKey] = deserializedResponse
    }
    if let underlyingError {
      userInfo[NSUnderlyingErrorKey] = underlyingError
    }
    return error(code: .internal(.RPCResponseDecodingError), userInfo: userInfo)
  }

  static func accountExistsWithDifferentCredentialError(email: String?,
                                                        updatedCredential: AuthCredential?)
    -> Error {
    var userInfo: [String: Any] = [:]
    if let email {
      userInfo[AuthErrors.userInfoEmailKey] = email
    }
    if let updatedCredential {
      userInfo[AuthErrors.userInfoUpdatedCredentialKey] = updatedCredential
    }
    return error(code: .accountExistsWithDifferentCredential, userInfo: userInfo)
  }

  private static func extractJSONObjectFromString(from string: String) -> [String: Any]? {
    // 1. Find the start of the JSON object.
    guard let start = string.firstIndex(of: "{") else {
      return nil // No JSON object found
    }
    // 2. Find the end of the JSON object.
    // Start from the first curly brace `{`
    var curlyLevel = 0
    var endIndex: String.Index?

    for index in string.indices.suffix(from: start) {
      let char = string[index]
      if char == "{" {
        curlyLevel += 1
      } else if char == "}" {
        curlyLevel -= 1
        if curlyLevel == 0 {
          endIndex = index
          break
        }
      }
    }
    guard let end = endIndex else {
      return nil // Unbalanced curly braces
    }

    // 3. Extract the JSON string.
    let jsonString = String(string[start ... end])

    // 4. Convert JSON String to JSON Object
    guard let jsonData = jsonString.data(using: .utf8) else {
      return nil // Could not convert String to Data
    }

    do {
      if let jsonObject = try JSONSerialization
        .jsonObject(with: jsonData, options: []) as? [String: Any] {
        return jsonObject
      } else {
        return nil // JSON Object is not a dictionary
      }
    } catch {
      return nil // Failed to deserialize JSON
    }
  }

  static func blockingCloudFunctionServerResponse(message: String?) -> Error {
    guard let message else {
      return error(code: .blockingCloudFunctionError, message: message)
    }
    guard let jsonDict = extractJSONObjectFromString(from: message) else {
      return error(code: .blockingCloudFunctionError, message: message)
    }
    let errorDict = jsonDict["error"] as? [String: Any] ?? [:]
    let errorMessage = errorDict["message"] as? String
    return error(code: .blockingCloudFunctionError, message: errorMessage)
  }

  #if os(iOS) || os(macOS)
    static func secondFactorRequiredError(pendingCredential: String?,
                                          hints: [MultiFactorInfo],
                                          auth: Auth)
      -> Error {
      var userInfo: [String: Any] = [:]
      if let pendingCredential = pendingCredential {
        let resolver = MultiFactorResolver(with: pendingCredential, hints: hints, auth: auth)
        userInfo[AuthErrors.userInfoMultiFactorResolverKey] = resolver
      }

      return error(code: .secondFactorRequired, userInfo: userInfo)
    }
  #endif // os(iOS) || os(macOS)

  static func recaptchaSDKNotLinkedError() -> Error {
    // TODO(ObjC): point the link to GCIP doc once available.
    let message = "The reCAPTCHA SDK is not linked to your app. See " +
      "https://cloud.google.com/recaptcha-enterprise/docs/instrument-ios-apps"
    return error(code: .recaptchaSDKNotLinked, message: message)
  }

  static func recaptchaSiteKeyMissing() -> Error {
    // TODO(ObjC): point the link to GCIP doc once available.
    let message = "The site key for the reCAPTCHA SDK was not found. See " +
      "https://cloud.google.com/recaptcha-enterprise/docs/instrument-ios-apps"
    return error(code: .recaptchaSiteKeyMissing, message: message)
  }

  static func recaptchaActionCreationFailed() -> Error {
    // TODO(ObjC): point the link to GCIP doc once available.
    let message = "The reCAPTCHA SDK action class creation failed. See " +
      "https://cloud.google.com/recaptcha-enterprise/docs/instrument-ios-apps"
    return error(code: .recaptchaActionCreationFailed, message: message)
  }
}
