/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import FBSDKCoreKit
import FBSDKCoreKit_Basics
import UIKit

/**
 Provides methods for logging the user in and out.

 It works directly with `AccessToken` (for data access) and `AuthenticationToken` (for authentication);
 it sets the "current" tokens upon successful authorizations (or sets to `nil` in case of `logOut`).

 You should check `AccessToken.current` before calling a login method to see if there is
 a cached token available (typically in a `viewDidLoad` implementation).

 @warning If you are managing your own tokens outside of `AccessToken`, you will need to set
 `AccessToken.current` before calling a login method to authorize further permissions on your tokens.
 */
@objcMembers
@objc(FBSDKLoginManager)
public final class LoginManager: NSObject {

  /// The default audience. You should set this if you intend to ask for publish permissions.
  public var defaultAudience = DefaultAudience.friends

  var handler: IdentifiedLoginResultHandler?
  private(set) var configuration: LoginConfiguration?

  private weak var fromViewController: UIViewController?
  var requestedPermissions: Set<FBPermission>?
  var logger: LoginManagerLogger?
  var state = LoginManagerState.idle
  var usedSafariSession = false

  var isPerformingLogin: Bool {
    state == .performingLogin
  }

  private enum Keys {
    static let expectedChallenge = "expected_login_challenge"
    static let expectedNonce = "expected_login_nonce"
    static let expectedCodeVerifier = "expected_login_code_verifier"
  }

  private static let clientStateChallengeLength = UInt(20)
  private static let oAuthPath = "/dialog/oauth"

  private enum CanceledLoginErrorDomains {
    static let safariServices = "com.apple.SafariServices.Authentication"
    static let authenticationServices = "com.apple.AuthenticationServices.WebAuthenticationSession"

    static func isValidDomain(_ domain: String) -> Bool {
      [safariServices, authenticationServices].contains(domain)
    }
  }

  private enum LoggerAuthenticationMethod {
    static let browser = "browser_auth"
    static let safariViewController = "sfvc_auth"
  }

  var configuredDependencies: ObjectDependencies?

  lazy var defaultDependencies: ObjectDependencies? = {
    let keychainStoreFactory = KeychainStoreFactory()
    guard let bundleIdentifier = Bundle.main.bundleIdentifier else {
      fatalError("Unable to find main bundle identifier. Cannot create keychain service identifier")
    }

    let keychainStore = keychainStoreFactory.createKeychainStore(
      service: "com.facebook.sdk.loginmanager.\(bundleIdentifier)",
      accessGroup: nil
    )

    return ObjectDependencies(
      accessTokenWallet: AccessToken.self,
      authenticationTokenWallet: AuthenticationToken.self,
      errorFactory: _ErrorFactory(),
      graphRequestFactory: GraphRequestFactory(),
      internalUtility: InternalUtility.shared,
      keychainStore: keychainStore,
      loginCompleterFactory: LoginCompleterFactory(),
      profileProvider: Profile.self,
      settings: Settings.shared,
      urlOpener: _BridgeAPI.shared
    )
  }()

  /**
   Initialize an instance of `LoginManager.`

   - parameter defaultAudience: Optional default audience to use. Default: `.friends`.
   */
  public convenience init(defaultAudience: DefaultAudience = .friends) {
    self.init()
    self.defaultAudience = defaultAudience
  }

  // MARK: - Logging In

  /**
   Logs the user in or authorizes additional permissions.

   @param viewController the view controller from which to present the login UI. If nil, the topmost view
   controller will be automatically determined and used.
   @param configuration the login configuration to use.
   @param completion the login completion handler.

   Use this method when asking for permissions. You should only ask for permissions when they
   are needed and the value should be explained to the user. You can inspect the
   `FBSDKLoginManagerLoginResultBlock`'s `result.declinedPermissions` to provide more information
   to the user if they decline permissions.
   To reduce unnecessary login attempts, you should typically check if `AccessToken.current`
   already contains the permissions you need. If it does, you probably do not need to call this method.

   @warning You can only perform one login call at a time. Calling a login method before the completion handler is
   called on a previous login attempt will result in an error.
   @warning This method will present a UI to the user and thus should be called on the main thread.
   */
  @available(swift, obsoleted: 0.1)
  @objc(logInFromViewController:configuration:completion:)
  public func logIn(
    from viewController: UIViewController?,
    configuration: LoginConfiguration?,
    completion: @escaping LoginManagerLoginResultBlock
  ) {
    guard validateLoginStartState() else { return }

    commonLogIn(from: viewController, configuration: configuration, completion: completion)
  }

  /**
   Logs the user in or authorizes additional permissions.

   Use this method when asking for permissions. You should only ask for permissions when they
   are needed and the value should be explained to the user. You can inspect the result's `declinedPermissions` to also
   provide more information to the user if they decline permissions.

   This method will present a UI to the user. To reduce unnecessary app switching, you should typically check if
   `AccessToken.current` already contains the permissions you need. If it does, you probably
   do not need to call this method.

   You can only perform one login call at a time. Calling a login method before the completion handler is called
   on a previous login will result in an error.

   - parameter viewController: Optional view controller to present from. Default: topmost view controller.
   - parameter configuration the login configuration to use.
   - parameter completion: Optional callback.
   */
  @nonobjc
  public func logIn(
    viewController: UIViewController? = nil,
    configuration: LoginConfiguration?,
    completion: @escaping LoginResultBlock
  ) {
    let legacyCompletion = { (result: LoginManagerLoginResult?, error: Error?) in
      let result = LoginResult(result: result, error: error)
      completion(result)
    }

    commonLogIn(from: viewController, configuration: configuration, completion: legacyCompletion)
  }

  private func commonLogIn(
    from viewController: UIViewController?,
    configuration: LoginConfiguration?,
    completion: LoginManagerLoginResultBlock?
  ) {
    guard let configuration = configuration else {
      let failureMessage = """
        Cannot login without a valid login configuration. Please make sure the `LoginConfiguration` provided is non-nil.
        """

      _Logger.singleShotLogEntry(.developerErrors, logEntry: failureMessage)

      // swiftformat:disable:next redundantSelf
      let error = self.errorFactory?.error(
        code: CoreError.errorInvalidArgument.rawValue,
        message: failureMessage,
        underlyingError: nil
      )
      handler = completion.flatMap(IdentifiedLoginResultHandler.init)
      return invokeHandler(error: error)
    }

    fromViewController = viewController
    self.configuration = configuration
    requestedPermissions = configuration.requestedPermissions

    logIn(permissions: configuration.requestedPermissions, handler: completion)
  }

  /**
   Logs the user in or authorizes additional permissions.

   @param permissions the optional array of permissions. Note this is converted to NSSet and is only
   an NSArray for the convenience of literal syntax.
   @param viewController the view controller to present from. If nil, the topmost view controller will be
   automatically determined as best as possible.
   @param handler the callback.

   Use this method when asking for read permissions. You should only ask for permissions when they
   are needed and explain the value to the user. You can inspect the `FBSDKLoginManagerLoginResultBlock`'s
   `result.declinedPermissions` to provide more information to the user if they decline permissions.
   You typically should check if `AccessToken.current` already contains the permissions you need before
   asking to reduce unnecessary login attempts. For example, you could perform that check in `viewDidLoad`.

   @warning You can only perform one login call at a time. Calling a login method before the completion handler is
   called on a previous login attempt will result in an error.
   @warning This method will present a UI to the user and thus should be called on the main thread.
   */
  @objc(logInWithPermissions:fromViewController:handler:)
  public func logIn(
    permissions: [String],
    from viewController: UIViewController?,
    handler: LoginManagerLoginResultBlock?
  ) {
    guard validateLoginStartState() else { return }

    let configuration = LoginConfiguration(permissions: permissions, tracking: .enabled)
    commonLogIn(from: viewController, configuration: configuration, completion: handler)
  }

  /**
   Logs the user in or authorizes additional permissions.

   Use this method when asking for permissions. You should only ask for permissions when they
   are needed and the value should be explained to the user. You can inspect the result's `declinedPermissions` to also
   provide more information to the user if they decline permissions.

   This method will present a UI to the user. To reduce unnecessary app switching, you should typically check if
   `AccessToken.current` already contains the permissions you need. If it does, you probably
   do not need to call this method.

   You can only perform one login call at a time. Calling a login method before the completion handler is called
   on a previous login will result in an error.

   - parameter permissions: Array of read permissions. Default: `[.PublicProfile]`
   - parameter viewController: Optional view controller to present from. Default: topmost view controller.
   - parameter completion: Optional callback.
   */
  func logIn(
    permissions: [Permission] = [.publicProfile],
    viewController: UIViewController? = nil,
    completion: LoginResultBlock? = nil
  ) {
    logIn(
      permissions: permissions.map(\.name),
      from: viewController
    ) { result, error in
      completion?(LoginResult(result: result, error: error))
    }
  }

  private func logIn(permissions: Set<FBPermission>, handler: LoginManagerLoginResultBlock?) {
    if let configuration = configuration {
      let provider = ServerConfigurationProvider()
      logger = LoginManagerLogger(loggingToken: provider.loggingToken, tracking: configuration.tracking)
    }

    self.handler = handler.flatMap(IdentifiedLoginResultHandler.init)

    logger?.startSession(for: self)
    logIn()
  }

  private func logIn() {
    usedSafariSession = false

    performBrowserLogIn { [self] didPerformLogIn, potentialError in
      if didPerformLogIn {
        state = .performingLogin
      } else if let error = potentialError as NSError?,
                CanceledLoginErrorDomains.isValidDomain(error.domain) {
        handleImplicitCancelOfLogIn()
      } else {
        let error = potentialError ?? NSError(domain: LoginErrorDomain, code: LoginError.unknown.rawValue)
        invokeHandler(error: error)
      }
    }
  }

  // MARK: - Reauthorization

  /**
   Requests user's permission to reathorize application's data access, after it has expired due to inactivity.
   @param viewController the view controller from which to present the login UI. If nil, the topmost view
   controller will be automatically determined and used.
   @param handler the callback.

   Use this method when you need to reathorize your app's access to user data via the Graph API.
   You should only call this after access has expired.
   You should provide as much context to the user as possible as to why you need to reauthorize the access, the
   scope of access being reathorized, and what added value your app provides when the access is reathorized.
   You can inspect the `result.declinedPermissions` to determine if you should provide more information to the
   user based on any declined permissions.

   @warning This method will reauthorize using a `LoginConfiguration` with `FBSDKLoginTracking` set to `.enabled`.
   @warning This method will present UI the user. You typically should call this if `AccessToken.isDataAccessExpired`
   is true.
   */
  @objc(reauthorizeDataAccess:handler:)
  public func reauthorizeDataAccess(
    from viewController: UIViewController,
    handler: @escaping LoginManagerLoginResultBlock
  ) {
    guard
      validateLoginStartState(),
      let dependencies = try? getDependencies()
    else { return }

    guard dependencies.accessTokenWallet.current != nil else {
      let message = "Must have an access token for which to reauthorize data access"
      let error = dependencies.errorFactory.error(
        domain: LoginErrorDomain,
        code: LoginError.missingAccessToken.rawValue,
        message: message,
        underlyingError: nil
      )

      _Logger.singleShotLogEntry(.developerErrors, logEntry: message)
      return handler(nil, error)
    }

    let configuration = LoginConfiguration(
      permissions: [], // Don't need to pass permissions for data reauthorization.
      tracking: .enabled,
      messengerPageId: nil,
      authType: .reauthorize
    )

    commonLogIn(from: fromViewController, configuration: configuration, completion: handler)
  }

  // MARK: - Logging Out

  /**
   Logs the user out

   This nils out the singleton instances of `AccessToken`, `AuthenticationToken` and `Profle`.

   @note This is only a client side logout. It will not log the user out of their Facebook account.
   */
  @objc(logOut)
  public func logOut() {
    guard let dependencies = try? getDependencies() else { return }

    dependencies.accessTokenWallet.current = nil
    dependencies.authenticationTokenWallet.current = nil
    dependencies.profileProvider.current = nil
  }

  // MARK: - Helpers

  private func handleImplicitCancelOfLogIn() {
    let result = LoginManagerLoginResult(
      token: nil,
      authenticationToken: nil,
      isCancelled: true,
      grantedPermissions: [],
      declinedPermissions: []
    )

    result.addLoggingExtra(true, forKey: "implicit_cancel")
    invokeHandler(result: result)
  }

  private func validateLoginStartState() -> Bool {
    switch state {
    case .start:
      if usedSafariSession {
        // Using safari makes an interstitial dialog that blocks the app, but in certain situations
        // such as screen lock it can be dismissed and have the control returned to the app without invoking the
        // completionHandler. In this case, the view controller has the control back and tried to reinvoke the login.
        // This is acceptable behavior and we should pop up the dialog again
        return true
      } else {
        let message = """
          ** WARNING: You are trying to start a login while a previous login has not finished yet. This is unsupported \
          behavior. You should wait until the previous login handler gets called to start a new login.
          """

        _Logger.singleShotLogEntry(.developerErrors, logEntry: message)
        return false
      }

    case .performingLogin:
      handleImplicitCancelOfLogIn()
      return true

    case .idle:
      state = .start
      return true
    }
  }

  func completeAuthentication(parameters: _LoginCompletionParameters, expectChallenge: Bool) {
    let isCancelled = (parameters.accessTokenString == nil)
      && (parameters.authenticationToken == nil)

    var error = parameters.error

    if expectChallenge,
       !isCancelled,
       error == nil {
      error = verifyChallenge(with: parameters)
    }

    storeExpectedChallenge(nil)

    var result: LoginManagerLoginResult?

    if error == nil {
      if !isCancelled {
        result = getSuccessResult(from: parameters)

        if result?.token != nil,
           // swiftformat:disable:next redundantSelf
           let accessToken = self.accessTokenWallet?.current {
          // In a reauthentication, short circuit and let the login handler be called when the validation finishes.
          return validateReauthentication(accessToken: accessToken, loginResult: result)
        }
      } else {
        result = getCancelledResult(from: parameters)
      }
    }

    setGlobalProperties(parameters: parameters, loginResult: result)

    invokeHandler(result: result, error: error)
  }

  private func setGlobalProperties(parameters: _LoginCompletionParameters, loginResult: LoginManagerLoginResult?) {
    guard let dependencies = try? getDependencies() else { return }

    let hasNewAuthenticationToken = (parameters.authenticationToken != nil)
    let hasNewOrUpdatedAccessToken = (loginResult?.token != nil)

    guard hasNewAuthenticationToken || hasNewOrUpdatedAccessToken else {
      // Assume cancellation. Don't do anything
      return
    }

    dependencies.authenticationTokenWallet.current = parameters.authenticationToken
    dependencies.accessTokenWallet.current = loginResult?.token
    dependencies.profileProvider.current = parameters.profile
  }

  // Returns an error if a stored challenge cannot be obtained from the completion parameters
  private func verifyChallenge(with completionParameters: _LoginCompletionParameters) -> Error? {
    let expectedChallenge = loadExpectedChallenge()?.replacingOccurrences(of: "+", with: " ")

    if expectedChallenge != completionParameters.challenge {
      return NSError(domain: LoginErrorDomain, code: LoginError.badChallengeString.rawValue)
    } else {
      return nil
    }
  }

  private func invokeHandler(
    result: LoginManagerLoginResult? = nil,
    error: Error? = nil
  ) {
    logger?.endLogin(result: result, error: error as NSError?)
    logger?.endSession()
    logger?.postLoginHeartbeat()
    logger = nil
    state = .idle

    guard let handlerBeforeInvocation = handler else { return }

    handlerBeforeInvocation(result, error)

    if let handlerAfterInvocation = handler,
       handlerBeforeInvocation == handlerAfterInvocation {
      handler = nil
    } else {
      let message = """
        ** WARNING: You are requesting permissions inside the completion block of an existing login. \
        This is unsupported behavior. You should request additional permissions only when they are needed, such as \
        requesting for publish_actions when the user performs a sharing action.
        """
      _Logger.singleShotLogEntry(.developerErrors, logEntry: message)
    }
  }

  func logInParameters(
    configuration: LoginConfiguration?,
    loggingToken: String?,
    authenticationMethod: String
  ) -> [String: String]? {
    // Making sure configuration is not nil in case this method gets called
    // internally without specifying a configuration.
    guard let dependencies = try? getDependencies() else { return nil }

    guard let configuration = configuration else {
      let error = dependencies.errorFactory.error(
        code: LoginError.unknown.rawValue,
        userInfo: nil,
        message: "Unable to perform login.",
        underlyingError: nil
      )
      invokeHandler(error: error)
      return nil
    }

    dependencies.internalUtility.validateURLSchemes()

    let cbtInMilliseconds = round(1000 * Date().timeIntervalSince1970)
    let nullableParameters: [String: String?] = [
      "client_id": dependencies.settings.appID,
      "display": "touch",
      "sdk": "ios",
      "return_scopes": "true",
      "sdk_version": FBSDK_VERSION_STRING,
      "fbapp_pres": NSNumber(value: dependencies.internalUtility.isFacebookAppInstalled).stringValue,
      "auth_type": configuration.authType?.rawValue,
      "logging_token": loggingToken,
      "cbt": String(cbtInMilliseconds),
      "ies": NSNumber(value: dependencies.settings.isAutoLogAppEventsEnabled).stringValue,
      "local_client_id": dependencies.settings.appURLSchemeSuffix,
      "default_audience": LoginUtility.stringForAudience(defaultAudience),
    ]
    var parameters = nullableParameters.compactMapValues { $0 }

    var permissions = configuration.requestedPermissions
    if let openIDPermission = FBPermission(string: "openid") {
      permissions.insert(openIDPermission)
    }
    parameters["scope"] = permissions.map(\.value).joined(separator: ",")

    if let messengerPageID = configuration.messengerPageId {
      parameters["messenger_page_id"] = messengerPageID
    }

    if let redirectURL = try? dependencies.internalUtility.appURL(
      withHost: "authorize",
      path: "",
      queryParameters: [:]
    ) {
      parameters["redirect_uri"] = redirectURL.absoluteString
    }

    let expectedChallenge = getStringForChallenge()
    let encodedChallenge = expectedChallenge.flatMap(Utility.encode(urlString:))
    let state: [String: Any] = ["challenge": encodedChallenge ?? NSNull()]
    if let clientState = LoginManagerLogger.getClientState(
      authenticationMethod: authenticationMethod,
      existingState: state,
      logger: logger
    ) {
      parameters["state"] = clientState
    }
    storeExpectedChallenge(expectedChallenge)

    switch configuration.tracking {
    case .limited:
      parameters["response_type"] = "id_token,graph_domain"
      parameters["tp"] = "ios_14_do_not_track"

    case .enabled:
      parameters["response_type"] = "id_token,token_or_nonce,signed_request,graph_domain"
      parameters["code_challenge"] = configuration.codeVerifier.challenge
      parameters["code_challenge_method"] = "S256"
      storeExpectedCodeVerifier(configuration.codeVerifier)
    }

    parameters["nonce"] = configuration.nonce
    storeExpectedNonce(configuration.nonce)

    let values = ["init": MonotonicTimer().getCurrentSeconds()]
    if let timestamp = try? BasicUtility.jsonString(for: values) {
      parameters["e2e"] = timestamp
    }

    return parameters
  }

  private func getStringForChallenge() -> String? {
    fb_randomString(Self.clientStateChallengeLength)?
      .replacingOccurrences(of: "+", with: "=")
  }

  func validateReauthentication(accessToken: AccessToken, loginResult: LoginManagerLoginResult?) {
    guard let dependencies = try? getDependencies() else { return }

    let request = dependencies.graphRequestFactory.createGraphRequest(
      withGraphPath: "me",
      parameters: ["fields": ""],
      tokenString: loginResult?.token?.tokenString,
      httpMethod: nil,
      flags: [.doNotInvalidateTokenOnError, .disableErrorRecovery]
    )

    request.start { [self] _, anyResult, error in
      guard
        let result = anyResult as? [String: Any],
        let actualID = result["id"] as? String,
        accessToken.userID == actualID
      else {
        let wrappedError = dependencies.errorFactory.error(
          domain: LoginErrorDomain,
          code: LoginError.userMismatch.rawValue,
          message: nil,
          underlyingError: error
        )
        return invokeHandler(error: wrappedError)
      }

      dependencies.accessTokenWallet.current = loginResult?.token
      invokeHandler(result: loginResult)
    }
  }

  private typealias BrowserLoginSuccessBlock = (_ didOpen: Bool, _ error: Error?) -> Void

  private func performBrowserLogIn(handler browserLoginHandler: BrowserLoginSuccessBlock?) {
    guard let dependencies = try? getDependencies() else { return }

    // swiftformat:disable:next redundantSelf
    let urlScheme = "fb\(self.settings?.appID ?? "")\(self.settings?.appURLSchemeSuffix ?? "")"
    logger?.willAttemptAppSwitchingBehavior(urlScheme: urlScheme)
    let serverConfigurationProvider = ServerConfigurationProvider()
    let shouldUseSafariViewController = serverConfigurationProvider.shouldUseSafariViewController(
      forDialogName: "login"
    )
    let authenticationMethod = shouldUseSafariViewController
      ? LoggerAuthenticationMethod.safariViewController
      : LoggerAuthenticationMethod.browser

    logger?.start(authenticationMethod: authenticationMethod)

    var potentialError: Error?
    var authenticationURL: URL?
    let loginParameters = logInParameters(
      configuration: configuration,
      loggingToken: serverConfigurationProvider.loggingToken,
      authenticationMethod: authenticationMethod
    )

    if let parameters = loginParameters,
       parameters["redirect_uri"] != nil {
      do {
        authenticationURL = try dependencies.internalUtility.facebookURL(
          hostPrefix: "m.",
          path: Self.oAuthPath,
          queryParameters: parameters
        )
      } catch {
        potentialError = error
      }
    }

    let loginHandler = browserLoginHandler ?? { _, _ in }

    guard let url = authenticationURL else {
      let error = potentialError ?? dependencies.errorFactory.error(
        code: LoginError.unknown.rawValue,
        message: "Failed to construct OAuth browser url",
        underlyingError: nil
      )

      return loginHandler(false, error)
    }

    if shouldUseSafariViewController {
      // Note based on above, authURL must be a http scheme. If that changes, add a guard, otherwise SFVC can throw
      usedSafariSession = true
      dependencies.urlOpener.openURLWithSafariViewController(
        url: url,
        sender: self,
        from: fromViewController,
        handler: loginHandler
      )
    } else {
      dependencies.urlOpener.open(url, sender: self, handler: loginHandler)
    }
  }

  private func getCancelledResult(from parameters: _LoginCompletionParameters) -> LoginManagerLoginResult {
    var declinedPermissions = Set<String>()
    // swiftformat:disable:next redundantSelf
    if self.accessTokenWallet?.current != nil {
      // Always include the list of declined permissions from this login request
      // if an access token is already cached by the SDK
      declinedPermissions = FBPermission.rawPermissions(from: parameters.declinedPermissions ?? [])
    }

    return LoginManagerLoginResult(
      token: nil,
      authenticationToken: nil,
      isCancelled: true,
      grantedPermissions: [],
      declinedPermissions: declinedPermissions
    )
  }

  private func getSuccessResult(from parameters: _LoginCompletionParameters) -> LoginManagerLoginResult {
    // Recent permissions are largely based on the existence of an access token.
    // Without an access token the 'recent' permissions will match the
    // intersection of the granted permissions and the requested permissions.
    // This is important because we want to create a result that accurately reflects
    // the currently-granted permissions even when there is no access token.
    guard
      let recentlyGrantedPermissions = parameters.permissions.flatMap(getRecentlyGrantedPermissions(from:)),
      !recentlyGrantedPermissions.isEmpty
    else {
      return getCancelledResult(from: parameters)
    }

    let recentlyDeclinedPermissions = getRecentlyDeclinedPermissions(from: parameters.declinedPermissions ?? [])

    let rawGrantedPermissions = FBPermission.rawPermissions(from: parameters.permissions ?? [])
    let rawDeclinedPermissions = FBPermission.rawPermissions(from: parameters.declinedPermissions ?? [])
    let rawRecentlyGrantedPermissions = FBPermission.rawPermissions(from: recentlyGrantedPermissions)
    let rawRecentlyDeclinedPermissions = FBPermission.rawPermissions(from: recentlyDeclinedPermissions)

    var accessToken: AccessToken?
    if let accessTokenString = parameters.accessTokenString {
      accessToken = AccessToken(
        tokenString: accessTokenString,
        permissions: Array(rawGrantedPermissions),
        declinedPermissions: Array(rawDeclinedPermissions),
        expiredPermissions: [],
        appID: parameters.appID ?? "",
        userID: parameters.userID ?? "",
        expirationDate: parameters.expirationDate,
        refreshDate: Date(),
        dataAccessExpirationDate: parameters.dataAccessExpirationDate
      )
    }

    return LoginManagerLoginResult(
      token: accessToken,
      authenticationToken: parameters.authenticationToken,
      isCancelled: false,
      grantedPermissions: rawRecentlyGrantedPermissions,
      declinedPermissions: rawRecentlyDeclinedPermissions
    )
  }

  func getRecentlyGrantedPermissions(from grantedPermissions: Set<FBPermission>) -> Set<FBPermission> {
    guard
      // swiftformat:disable:next redundantSelf
      let previous = self.accessTokenWallet?.current?.permissions,
      !previous.isEmpty,
      let requested = requestedPermissions,
      !requested.isEmpty
    else { return grantedPermissions }

    // If there were no requested permissions for this auth, or no previously granted permissions, treat all
    // permissions as recently granted.
    // Otherwise this is a reauthorization, so recentlyGranted should be a subset of what was requested.
    return grantedPermissions.intersection(requested)
  }

  func getRecentlyDeclinedPermissions(from declinedPermissions: Set<FBPermission>) -> Set<FBPermission> {
    let recentlyDeclinedPermissions = requestedPermissions ?? []
    return recentlyDeclinedPermissions.intersection(declinedPermissions)
  }

  // MARK: - Keychain Storage

  private func storeExpectedChallenge(_ challenge: String?) {
    // swiftformat:disable:next redundantSelf
    guard let keychainStore = self.keychainStore else { return }

    let accessibility = DynamicFrameworkLoaderProxy
      .loadkSecAttrAccessibleAfterFirstUnlockThisDeviceOnly()
      .takeRetainedValue()

    keychainStore.setString(
      challenge,
      forKey: Keys.expectedChallenge,
      accessibility: accessibility
    )
  }

  private func loadExpectedChallenge() -> String? {
    // swiftformat:disable:next redundantSelf
    self.keychainStore?.string(forKey: Keys.expectedChallenge)
  }

  func storeExpectedNonce(_ nonce: String?) {
    let accessibility = DynamicFrameworkLoaderProxy
      .loadkSecAttrAccessibleAfterFirstUnlockThisDeviceOnly()
      .takeRetainedValue()

    // swiftformat:disable:next redundantSelf
    self.keychainStore?.setString(
      nonce,
      forKey: Keys.expectedNonce,
      accessibility: accessibility
    )
  }

  private func loadExpectedNonce() -> String? {
    // swiftformat:disable:next redundantSelf
    self.keychainStore?.string(forKey: Keys.expectedNonce)
  }

  private func storeExpectedCodeVerifier(_ codeVerifier: CodeVerifier?) {
    let accessibility = DynamicFrameworkLoaderProxy
      .loadkSecAttrAccessibleAfterFirstUnlockThisDeviceOnly()
      .takeRetainedValue()

    // swiftformat:disable:next redundantSelf
    self.keychainStore?.setString(
      codeVerifier?.value,
      forKey: Keys.expectedCodeVerifier,
      accessibility: accessibility
    )
  }

  private func loadExpectedCodeVerifier() -> String? {
    // swiftformat:disable:next redundantSelf
    self.keychainStore?.string(forKey: Keys.expectedCodeVerifier)
  }
}

extension LoginManager: URLOpening {

  public static func makeOpener() -> LoginManager {
    LoginManager()
  }

  public func application(
    _ application: UIApplication?,
    open url: URL?,
    sourceApplication: String?,
    annotation: Any?
  ) -> Bool {
    guard let url = url else { return false }

    let isFacebookURL = canOpen(
      url,
      for: application,
      sourceApplication: sourceApplication,
      annotation: annotation
    )

    if !isFacebookURL,
       isPerformingLogin {
      handleImplicitCancelOfLogIn()
    }

    guard
      isFacebookURL,
      let dependencies = try? getDependencies()
    else { return false }

    let urlParameters = LoginUtility.getQueryParameters(from: url) ?? [:]
    let completer = dependencies.loginCompleterFactory.createLoginCompleter(
      urlParameters: urlParameters,
      appID: dependencies.settings.appID ?? ""
    )

    // Any necessary strong reference is maintained by the FBSDKLoginURLCompleter handler
    completer.completeLogin(
      nonce: loadExpectedNonce(),
      codeVerifier: loadExpectedCodeVerifier()
    ) { [self] parameters in
      if let configuration = configuration,
         logger == nil {
        logger = LoginManagerLogger(
          parameters: urlParameters,
          tracking: configuration.tracking
        )
      }

      completeAuthentication(parameters: parameters, expectChallenge: true)
    }

    storeExpectedNonce(nil)
    storeExpectedCodeVerifier(nil)

    return true
  }

  public func canOpen(
    _ url: URL,
    for application: UIApplication?,
    sourceApplication: String?,
    annotation: Any?
  ) -> Bool {
    // Verify the URL is intended as a callback for the SDK's login
    guard
      let scheme = url.scheme,
      let host = url.host
    else { return false }

    // swiftformat:disable:next redundantSelf
    return scheme.hasPrefix("fb\(self.settings?.appID ?? "")")
      && host == "authorize"
  }

  public func applicationDidBecomeActive(_ application: UIApplication) {
    if isPerformingLogin {
      handleImplicitCancelOfLogIn()
    }
  }

  public func isAuthenticationURL(_ url: URL) -> Bool {
    url.path.hasSuffix(Self.oAuthPath)
  }

  public func shouldStopPropagation(of url: URL) -> Bool {
    guard
      let scheme = url.scheme,
      let host = url.host
    else { return false }

    // swiftformat:disable:next redundantSelf
    return scheme.hasPrefix("fb\(self.settings?.appID ?? "")")
      && host == "no-op"
  }
}

extension LoginManager: LoginProviding {}

extension LoginManager: DependentAsObject {
  struct ObjectDependencies {
    var accessTokenWallet: _AccessTokenProviding.Type
    var authenticationTokenWallet: _AuthenticationTokenProviding.Type
    var errorFactory: ErrorCreating
    var graphRequestFactory: GraphRequestFactoryProtocol
    var internalUtility: URLHosting & AppURLSchemeProviding & AppAvailabilityChecker
    var keychainStore: KeychainStoreProtocol
    var loginCompleterFactory: LoginCompleterFactoryProtocol
    var profileProvider: ProfileProviding.Type
    var settings: SettingsProtocol
    var urlOpener: URLOpener
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import FBSDKCoreKit
import FBSDKCoreKit_Basics
import Foundation

/// Describes the result of a login attempt.
@objcMembers
@objc(FBSDKLoginManagerLoginResult)
public final class LoginManagerLoginResult: NSObject {

  /// The access token
  public let token: AccessToken?

  /// The authentication token
  public let authenticationToken: AuthenticationToken?

  /// Whether the login was cancelled by the user
  public let isCancelled: Bool

  /// The set of permissions granted by the user in the associated request.
  /// Inspect the token's permissions set for a complete list.
  public let grantedPermissions: Set<String>

  /// The set of permissions declined by the user in the associated request.
  /// Inspect the token's permissions set for a complete list.
  public let declinedPermissions: Set<String>

  private(set) var loggingExtras = [String: Any]()

  /**
   Creates a new result

   @param token The access token
   @param authenticationToken The authentication token
   @param isCancelled whether The login was cancelled by the user
   @param grantedPermissions The set of granted permissions
   @param declinedPermissions The set of declined permissions
   */
  @objc(initWithToken:authenticationToken:isCancelled:grantedPermissions:declinedPermissions:)
  public init(
    token: AccessToken?,
    authenticationToken: AuthenticationToken?,
    isCancelled: Bool,
    grantedPermissions: Set<String>,
    declinedPermissions: Set<String>
  ) {
    self.token = token
    self.authenticationToken = authenticationToken
    self.isCancelled = isCancelled
    self.grantedPermissions = grantedPermissions
    self.declinedPermissions = declinedPermissions
  }

  func addLoggingExtra(_ object: Any, forKey key: String) {
    loggingExtras[key] = object
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import UIKit

protocol LoginProviding {

  var defaultAudience: DefaultAudience { get set }

  func logIn(
    viewController: UIViewController?,
    configuration: LoginConfiguration?,
    completion: @escaping LoginResultBlock
  )

  func logIn(
    permissions: [String],
    from viewController: UIViewController?,
    handler: LoginManagerLoginResultBlock?
  )

  func logOut()
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import FBSDKCoreKit

/// Login Result Block
public typealias LoginResultBlock = (LoginResult) -> Void

/// Describes the result of a login attempt.
@frozen
public enum LoginResult {
  /// User succesfully logged in. Contains granted, declined permissions and access token.
  case success(granted: Set<Permission>, declined: Set<Permission>, token: AccessToken?)
  /// Login attempt was cancelled by the user.
  case cancelled
  /// Login attempt failed.
  case failed(Error)

  init(result: LoginManagerLoginResult?, error: Error?) {
    guard
      let result = result,
      error == nil
    else {
      self = .failed(error ?? LoginError(LoginError.unknown))
      return
    }

    guard !result.isCancelled else {
      self = .cancelled
      return
    }

    let granted = Set(result.grantedPermissions.map(Permission.init(stringLiteral:)))
    let declined = Set(result.declinedPermissions.map(Permission.init(stringLiteral:)))
    self = .success(granted: granted, declined: declined, token: result.token)
  }

  var loginManagerResult: LoginManagerLoginResult? {
    LoginManagerLoginResult(
      token: accessToken,
      authenticationToken: nil,
      isCancelled: isCancelled,
      grantedPermissions: grantedPermissions,
      declinedPermissions: declinedPermissions
    )
  }

  private var accessToken: AccessToken? {
    switch self {
    case let .success(granted: _, declined: _, token: accessToken):
      return accessToken
    default:
      return nil
    }
  }

  private var isCancelled: Bool {
    switch self {
    case .cancelled: return true
    default: return false
    }
  }

  private var grantedPermissions: Set<String> {
    switch self {
    case let .success(granted: granted, declined: _, token: _):
      return Set(granted.map(\.name))
    default:
      return []
    }
  }

  private var declinedPermissions: Set<String> {
    switch self {
    case let .success(granted: _, declined: declined, token: _):
      return Set(declined.map(\.name))
    default:
      return []
    }
  }

  var error: Error? {
    switch self {
    case let .failed(error): return error
    default: return nil
    }
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

/**
 The `LoginTooltipViewDelegate` protocol defines the methods used to receive event
 notifications from `FBLoginTooltipView` objects.
 */
@objc(FBSDKLoginTooltipViewDelegate)
public protocol LoginTooltipViewDelegate {

  /**
   Asks the delegate if the tooltip view should appear

   @param view The tooltip view.
   @param appIsEligible The value fetched from the server identifying if the app
   is eligible for the new login experience.

   Use this method to customize display behavior.
   */
  @objc(loginTooltipView:shouldAppear:)
  optional func loginTooltipView(_ view: FBLoginTooltipView, shouldAppear appIsEligible: Bool) -> Bool

  /**
   Tells the delegate the tooltip view will appear, specifically after it's been
   added to the super view but before the fade in animation.

   @param view The tooltip view.
   */
  @objc(loginTooltipViewWillAppear:)
  optional func loginTooltipViewWillAppear(_ view: FBLoginTooltipView)

  /**
   Tells the delegate the tooltip view will not appear (i.e., was not
   added to the super view).

   @param view The tooltip view.
   */
  @objc(loginTooltipViewWillNotAppear:)
  optional func loginTooltipViewWillNotAppear(_ view: FBLoginTooltipView)
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

// The login tracking preference to use for a login attempt. For more information on the differences between
/// `enabled` and `limited` see: https://developers.facebook.com/docs/facebook-login/ios/limited-login/
@objc(FBSDKLoginTracking)
public enum LoginTracking: UInt {
  case enabled
  case limited
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import FBSDKCoreKit

extension AppEvents: LoginEventLogging {}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import FBSDKCoreKit

typealias AuthenticationTokenBlock = (AuthenticationToken?) -> Void

protocol AuthenticationTokenCreating {
  func createToken(
    tokenString: String,
    nonce: String,
    graphDomain: String,
    completion: @escaping AuthenticationTokenBlock
  )
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import FBSDKCoreKit_Basics

final class AuthenticationTokenHeader {

  /// Key identifier used in identifying the key to be used to verify the signature.
  let kid: String

  /// Returns a new instance, when one can be created from the parameters given, otherwise `nil`.
  /// - Parameter encodedHeader: Base64-encoded string of the header.
  /// - Returns: An FBAuthenticationTokenHeader object.
  init?(fromEncodedString encodedHeader: String) {
    guard
      let headerData = Base64.decode(asData: Base64.base64(fromBase64Url: encodedHeader)),
      let header = try? JSONSerialization.jsonObject(with: headerData, options: .mutableContainers) as? [String: Any],
      let alg = header["alg"] as? String,
      let typ = header["typ"] as? String,
      let kid = header["kid"] as? String,
      alg == "RS256",
      typ == "JWT",
      !kid.isEmpty
    else {
      return nil
    }

    self.kid = kid
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

@dynamicMemberLookup
protocol DependentAsObject: AnyObject {
  associatedtype ObjectDependencies

  var configuredDependencies: ObjectDependencies? { get set }
  var defaultDependencies: ObjectDependencies? { get }

  func setDependencies(_ dependencies: ObjectDependencies)

  #if DEBUG
  func resetDependencies()
  #endif
}

extension DependentAsObject {
  func setDependencies(_ dependencies: ObjectDependencies) {
    configuredDependencies = dependencies
  }

  #if DEBUG
  func resetDependencies() {
    configuredDependencies = nil
  }
  #endif

  func getDependencies() throws -> ObjectDependencies {
    guard let dependencies = configuredDependencies ?? defaultDependencies else {
      throw MissingDependenciesError(for: Self.self)
    }

    return dependencies
  }

  subscript<Dependency>(dynamicMember keyPath: KeyPath<ObjectDependencies, Dependency>) -> Dependency? {
    try? getDependencies()[keyPath: keyPath]
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

@dynamicMemberLookup
protocol DependentAsType {
  associatedtype TypeDependencies

  static var configuredDependencies: TypeDependencies? { get set }
  static var defaultDependencies: TypeDependencies? { get }

  static func setDependencies(_ dependencies: TypeDependencies)

  #if DEBUG
  static func resetDependencies()
  #endif
}

extension DependentAsType {
  static func setDependencies(_ dependencies: TypeDependencies) {
    configuredDependencies = dependencies
  }

  #if DEBUG
  static func resetDependencies() {
    configuredDependencies = nil
  }
  #endif

  static func getDependencies() throws -> TypeDependencies {
    guard let dependencies = configuredDependencies ?? defaultDependencies else {
      throw MissingDependenciesError(for: Self.self)
    }

    return dependencies
  }

  static subscript<Dependency>(dynamicMember keyPath: KeyPath<TypeDependencies, Dependency>) -> Dependency? {
    try? getDependencies()[keyPath: keyPath]
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

struct DevicePoller: DevicePolling {
  func schedule(interval: UInt, block: @escaping () -> Void) {
    let dispatchTime = DispatchTime.now() + DispatchTimeInterval.seconds(Int(interval))
    DispatchQueue.main.asyncAfter(deadline: dispatchTime, execute: block)
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import FBSDKCoreKit

/// Helper class for device requests mDNS broadcasts. Note this is only intended for internal consumption.
enum DeviceRequestsHelper {

  // We use weak to strong in order to retain the advertisement services
  // without having to pass them back to the delegate that started them
  // Note that in case the delegate is destroyed before it had a chance to
  // stop the service, the service will continue broadcasting until the map
  // resizes itself and releases the service, causing it to stop
  private(set) static var mdnsAdvertisementServices = NSMapTable<NetServiceDelegate, AnyObject>.weakToStrongObjects()

  private enum DeviceInfoKeys {
    static let deviceInfo = "device"
    static let deviceModel = "model"
  }

  private enum NetServiceValues {

    static let header = "fbsdk"

    static let flavor = "ios"

    static let sdkVersion: String = {
      var sdkVersion = Settings.shared.sdkVersion.replacingOccurrences(of: ".", with: "|")

      guard
        sdkVersion.count > 10,
        let firstCharacter = sdkVersion.first,
        firstCharacter.isASCII,
        firstCharacter.isNumber
      else {
        return "dev"
      }

      return sdkVersion
    }()

    static let netServiceType = "_fb._tcp."
  }

  /// Get device info to include with the GraphRequest
  static func getDeviceInfo() -> String {
    var systemInfo = utsname()
    uname(&systemInfo)

    let data = Data(bytes: &systemInfo.machine, count: Int(_SYS_NAMELEN))

    guard
      let device = String(bytes: data, encoding: .ascii)?.trimmingCharacters(in: .controlCharacters)
    else { return "" }

    let model = UIDevice.current.model

    return """
      {"\(DeviceInfoKeys.deviceModel)":"\(model)","\(DeviceInfoKeys.deviceInfo)":"\(device)"}
      """
  }

  /**
   Start the mDNS advertisement service for a device request
   @param loginCode The login code associated with the action for the device request.
   @return True if the service broadcast was successfully started.
   */
  @discardableResult
  static func startAdvertisementService(loginCode: String, delegate: NetServiceDelegate) -> Bool {
    let serviceName = """
      \(NetServiceValues.header)_\(NetServiceValues.flavor)-\(NetServiceValues.sdkVersion)_\(loginCode)
      """

    guard serviceName.count <= 60 else { return false }

    let mdnsAdvertisementService = NetService(
      domain: "local.",
      type: NetServiceValues.netServiceType,
      name: serviceName,
      port: 0
    )
    mdnsAdvertisementService.delegate = delegate
    mdnsAdvertisementService.publish(options: [.noAutoRename, .listenForConnections])
    AppEvents.shared.logInternalEvent(.smartLoginService, parameters: [:], isImplicitlyLogged: true)
    mdnsAdvertisementServices.setObject(mdnsAdvertisementService, forKey: delegate)
    return true
  }

  /**
   Check if a service delegate is registered with particular advertisement service
   @param delegate The delegate to check if registered.
   @param service The advertisement service to check for.
   @return True if the service is the one the delegate registered with.
   */
  static func isDelegate(_ delegate: NetServiceDelegate, forAdvertisementService service: NetService) -> Bool {
    guard
      let mdnsAdvertisementService = mdnsAdvertisementServices.object(forKey: delegate) as? NetService
    else {
      return false
    }
    return mdnsAdvertisementService === service
  }

  /**
   Stop the mDNS advertisement service for a device request
   @param delegate The delegate registered with the service.
   */
  static func cleanUpAdvertisementService(for delegate: NetServiceDelegate) {
    guard
      let mdnsAdvertisementService = mdnsAdvertisementServices.object(forKey: delegate) as? NetService
    else {
      return
    }
    mdnsAdvertisementService.delegate = nil
    mdnsAdvertisementService.stop()
    mdnsAdvertisementServices.removeObject(forKey: delegate)
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import FBSDKCoreKit_Basics

/**
 Internal Type exposed to facilitate transition to Swift.
 API Subject to change or removal without warning. Do not use.
 - Warning INTERNAL:  DO NOT USE
 */
@objcMembers
@objc(FBSDKPermission)
public final class FBPermission: NSObject {

  /// The raw string representation of the permission
  let value: String
  public override var description: String {
    value
  }

  public override var hash: Int {
    value.hash
  }

  /**
   Attempts to initialize a new permission with the given string.
   Creation will fail and return nil if the string is invalid.
   - Parameter string: The raw permission string
   */
  public init?(string: String) {
    let allowedCharacterSet = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyz_")
    guard !string.isEmpty,
          allowedCharacterSet.isSuperset(of: CharacterSet(charactersIn: string))
    else { return nil }
    value = string
  }

  /**
   Returns a set of `FBPermission` from a set of raw permissions strings.
   Will return nil if any of the input permissions is invalid.
   */
  @objc(permissionsFromRawPermissions:)
  public static func permissions(fromRawPermissions rawPermissions: Set<String>) -> Set<FBPermission>? {
    var permissions = Set<FBPermission>()
    for rawPermission in rawPermissions {
      guard let permission = FBPermission(string: rawPermission) else { return nil }

      permissions.insert(permission)
    }
    return permissions
  }

  /**
   Returns a set of string permissions from a set of `FBPermission` by
   extracting the "value" property for each element.
   */
  @objc(rawPermissionsFromPermissions:)
  public static func rawPermissions(from permissions: Set<FBPermission>) -> Set<String> {
    Set(permissions.map(\.value))
  }

  public override func isEqual(_ object: Any?) -> Bool {
    (object as? FBPermission)?.value == value
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Foundation

struct IdentifiedLoginResultHandler: Equatable {
  private let identifier = UUID()
  private let closure: (_ result: LoginManagerLoginResult?, _ error: Error?) -> Void

  init(_ closure: @escaping (_ result: LoginManagerLoginResult?, _ error: Error?) -> Void) {
    self.closure = closure
  }

  func callAsFunction(_ result: LoginManagerLoginResult?, _ error: Error?) {
    closure(result, error)
  }

  static func == (lhs: Self, rhs: Self) -> Bool {
    lhs.identifier == rhs.identifier
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import FBSDKCoreKit

extension InternalUtility: UserInterfaceElementProviding {}

extension InternalUtility: UserInterfaceStringProviding {}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

struct LoginCompleterFactory: LoginCompleterFactoryProtocol {

  func createLoginCompleter(
    urlParameters parameters: [String: Any],
    appID: String
  ) -> LoginCompleting {
    LoginURLCompleter(urlParameters: parameters, appID: appID)
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

protocol LoginCompleterFactoryProtocol {
  func createLoginCompleter(urlParameters parameters: [String: Any], appID: String) -> LoginCompleting
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

protocol LoginCompleting {

  /**
   Invoke handler with the login parameters derived from the authentication result.
   See the implementing class's documentation for whether it completes synchronously or asynchronously.
   */
  func completeLogin(handler: @escaping LoginCompletionParametersBlock)

  /**
   Invoke handler with the login parameters derived from the authentication result.
   See the implementing class's documentation for whether it completes synchronously or asynchronously.
   */
  func completeLogin(
    nonce: String?,
    codeVerifier: String?,
    handler: @escaping LoginCompletionParametersBlock
  )
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import FBSDKCoreKit

protocol LoginEventLogging {
  var flushBehavior: AppEvents.FlushBehavior { get }

  func logInternalEvent(
    _ eventName: AppEvents.Name,
    parameters: [AppEvents.ParameterName: Any]?,
    isImplicitlyLogged: Bool
  )

  func flush()
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import FBSDKCoreKit

final class LoginManagerLogger {
  var identifier: String?
  var extras = [String: Any]()
  var lastResult = ""
  var lastError: NSError?
  var authMethod: String?
  var loggingToken: String?

  private enum ClientStateKeys {
    static let state = "state"
    static let isClientState = "com.facebook.sdk_client_state"
  }

  private enum LoggerParameterKeys {
    static let identifier = AppEvents.ParameterName("0_auth_logger_id")
    static let timestamp = AppEvents.ParameterName("1_timestamp_ms")
    static let result = AppEvents.ParameterName("2_result")
    static let authMethod = AppEvents.ParameterName("3_method")
    static let errorCode = AppEvents.ParameterName("4_error_code")
    static let errorMessage = AppEvents.ParameterName("5_error_message")
    static let extras = AppEvents.ParameterName("6_extras")
    static let loggingToken = AppEvents.ParameterName("7_logging_token")
    static let declinedPermissions = AppEvents.ParameterName("declined_permissions")
  }

  private enum ErrorParameterKeys {
    static let errorMessage = "error_message"
    static let innerErrorMessage = "inner_error_message"
    static let errorLocalizedDescription = "com.facebook.sdk:FBSDKErrorLocalizedDescriptionKey"
    static let graphRequestErrorCode = "com.facebook.sdk:FBSDKGraphRequestErrorGraphErrorCodeKey"
  }

  private enum ErrorDomains: String {
    case core = "com.facebook.sdk.core"
    case login = "com.facebook.sdk.login"
  }

  private enum LoggerResult: String {
    case success
    case cancel = "cancelled"
    case error
  }

  convenience init?(parameters: [String: Any]?, tracking: LoginTracking) {
    guard
      let parameters = parameters,
      let clientStateString = parameters[ClientStateKeys.state] as? String,
      let clientStateData = clientStateString.data(using: .utf8),
      let clientState = try? JSONSerialization.jsonObject(with: clientStateData) as? [String: Any],
      clientState[ClientStateKeys.isClientState] as? Bool == true
    else { return nil }

    self.init(loggingToken: nil, tracking: tracking)

    identifier = clientState[LoggerParameterKeys.identifier.rawValue] as? String
    authMethod = clientState[LoggerParameterKeys.authMethod.rawValue] as? String
    loggingToken = clientState[LoggerParameterKeys.loggingToken.rawValue] as? String
  }

  init?(loggingToken: String?, tracking: LoginTracking) {

    switch tracking {
    case .enabled:
      break
    case .limited:
      return nil
    }

    identifier = UUID().uuidString
    self.loggingToken = loggingToken
  }

  func startSession(for loginManager: LoginManager) {
    let isReauthorize = AccessToken.current != nil
    let willTryNative = false
    let willTryBrowser = true
    let behaviorString = "FBSDKLoginBehaviorBrowser"
    let audience = LoginUtility.stringForAudience(loginManager.defaultAudience)

    var permissionsString = ""
    if let permissions = loginManager.requestedPermissions {
      permissionsString = permissions.reduce("") { result, permission in
        result + (result.isEmpty ? "" : ",") + "\(permission.value)"
      }
    }

    let sessionParameters: [String: Any] = [
      "tryFBAppAuth": willTryNative,
      "trySafariAuth": willTryBrowser,
      "isReauthorize": isReauthorize,
      "login_behavior": behaviorString,
      "default_audience": audience,
      "permissions": permissionsString,
    ]

    extras = extras.merging(sessionParameters) { _, last in last }

    logEvent(.sessionAuthStart, params: parametersForNewEvent())
  }

  func endSession() {
    logEvent(.sessionAuthEnd, result: lastResult, error: lastError)

    if let eventLogger = Self.eventLogger,
       eventLogger.flushBehavior != .explicitOnly {
      eventLogger.flush()
    }
  }

  func start(authenticationMethod: String) {
    authMethod = authenticationMethod
    logEvent(.sessionAuthMethodStart, params: parametersForNewEvent())
  }

  func endLogin(result: LoginManagerLoginResult?, error: NSError?) {
    var resultString = ""

    if error != nil {
      resultString = LoggerResult.error.rawValue
    } else if let isCancelled = result?.isCancelled, isCancelled {
      resultString = LoggerResult.cancel.rawValue
    } else if result?.token != nil {
      resultString = LoggerResult.success.rawValue

      if let declinedPermissions = result?.declinedPermissions,
         !declinedPermissions.isEmpty {
        let declinedPermissionsString = declinedPermissions.reduce("") { result, permission in
          result + (result.isEmpty ? "" : ",") + permission
        }
        extras[LoggerParameterKeys.declinedPermissions.rawValue] = declinedPermissionsString
      }
    }

    lastResult = resultString
    lastError = error

    if let loggingExtras = result?.loggingExtras {
      extras = extras.merging(loggingExtras) { _, last in last }
    }

    logEvent(.sessionAuthMethodEnd, result: resultString, error: error)
  }

  func postLoginHeartbeat() {
    Timer.scheduledTimer(
      timeInterval: 5.0,
      target: self,
      selector: #selector(heartbeatTimerDidFire),
      userInfo: nil,
      repeats: false
    )
  }

  @objc
  func heartbeatTimerDidFire() {
    logEvent(.sessionAuthHeartbeat, result: lastResult, error: lastError)
  }

  func willAttemptAppSwitchingBehavior(urlScheme: String) {
    let isURLSchemeRegistered = InternalUtility.shared.isRegisteredURLScheme(urlScheme)
    let isFacebookAppCanOpenURLSchemeRegistered = InternalUtility.shared.isRegisteredCanOpenURLScheme(
      URLScheme.facebookAPI.rawValue
    )
    let isMessengerAppCanOpenURLSchemeRegistered = InternalUtility.shared.isRegisteredCanOpenURLScheme(
      URLScheme.messengerApp.rawValue
    )

    let urlSchemeParameters: [String: Bool] = [
      "isURLSchemeRegistered": isURLSchemeRegistered,
      "isFacebookAppCanOpenURLSchemeRegistered": isFacebookAppCanOpenURLSchemeRegistered,
      "isMessengerAppCanOpenURLSchemeRegistered": isMessengerAppCanOpenURLSchemeRegistered,
    ]

    extras = extras.merging(urlSchemeParameters) { _, last in last }
  }

  static func getClientState(
    authenticationMethod: String?,
    existingState: [String: Any]?,
    logger: LoginManagerLogger?
  ) -> String? {

    var clientState: [String: Any] = [
      LoggerParameterKeys.authMethod.rawValue: authenticationMethod ?? "",
      LoggerParameterKeys.identifier.rawValue: logger?.identifier ?? UUID().uuidString,
      ClientStateKeys.isClientState: true,
    ]

    if let existingState = existingState {
      var mutableState = clientState
      mutableState = mutableState.merging(existingState) { _, last in last }
      clientState = mutableState
    }

    guard
      let clientStateData = try? JSONSerialization.data(withJSONObject: clientState),
      let clientStateJSONString = String(data: clientStateData, encoding: .utf8)
    else {
      return nil
    }

    return clientStateJSONString
  }

  func parametersForNewEvent() -> [AppEvents.ParameterName: Any] {
    var eventParameters = [AppEvents.ParameterName: Any]()

    // NOTE: We ALWAYS add all params to each event, to ensure predictable mapping on the backend.
    eventParameters[LoggerParameterKeys.identifier] = identifier ?? ""
    eventParameters[LoggerParameterKeys.timestamp] = round(1000 * Date().timeIntervalSince1970)
    eventParameters[LoggerParameterKeys.result] = ""
    eventParameters[LoggerParameterKeys.authMethod] = authMethod
    eventParameters[LoggerParameterKeys.errorCode] = ""
    eventParameters[LoggerParameterKeys.errorMessage] = ""
    eventParameters[LoggerParameterKeys.extras] = ""
    eventParameters[LoggerParameterKeys.loggingToken] = loggingToken ?? ""

    return eventParameters
  }

  func logEvent(_ eventName: AppEvents.Name, params: [AppEvents.ParameterName: Any]?) {
    guard identifier != nil else { return }

    var parameters = params
    if let extrasData = try? JSONSerialization.data(withJSONObject: extras),
       let extrasJSONString = String(data: extrasData, encoding: .utf8),
       var mutableParams = parameters {
      mutableParams[LoggerParameterKeys.extras] = extrasJSONString
      parameters = mutableParams
    }

    extras.removeAll()
    Self.eventLogger?.logInternalEvent(eventName, parameters: parameters, isImplicitlyLogged: true)
  }

  func logEvent(_ eventName: AppEvents.Name, result: String, error: NSError?) {
    var params = parametersForNewEvent()
    params[LoggerParameterKeys.result] = result

    if let error = error,
       error.domain == ErrorDomains.core.rawValue ||
       error.domain == ErrorDomains.login.rawValue {
      // tease apart the structure.

      // first see if there is an explicit message in the error's userInfo. If not, default to the reason,
      // which is less useful.
      var errorMessage = error.userInfo[ErrorParameterKeys.errorMessage] ?? error.userInfo[
        ErrorParameterKeys.errorLocalizedDescription
      ]
      params[LoggerParameterKeys.errorMessage] = errorMessage

      errorMessage = error.userInfo[ErrorParameterKeys.graphRequestErrorCode] ?? "\(error.code)"
      params[LoggerParameterKeys.errorCode] = errorMessage

      if let innerError = error.userInfo[NSUnderlyingErrorKey] as? NSError {
        errorMessage = innerError.userInfo[
          ErrorParameterKeys.errorMessage
        ] ?? innerError.userInfo[NSLocalizedDescriptionKey]
        extras[ErrorParameterKeys.innerErrorMessage] = errorMessage

        errorMessage = innerError.userInfo[
          ErrorParameterKeys.graphRequestErrorCode
        ] ?? "\(innerError.code)"

        extras[ErrorParameterKeys.innerErrorMessage] = errorMessage
      }
    } else if let error = error {
      params[LoggerParameterKeys.errorCode] = error.code
      params[LoggerParameterKeys.errorMessage] = error.localizedDescription
    }

    logEvent(eventName, params: params)
  }
}

extension LoginManagerLogger: DependentAsType {
  struct TypeDependencies {
    var eventLogger: LoginEventLogging
  }

  static var defaultDependencies: TypeDependencies? = TypeDependencies(eventLogger: AppEvents.shared)

  static var configuredDependencies: TypeDependencies?
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

enum LoginManagerState: Int {
  case idle

  // We received a call to start login.
  case start

  // We're calling out to the Facebook app or Safari to perform a log in
  case performingLogin
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import FBSDKCoreKit
import Foundation

final class LoginRecoveryAttempter: ErrorRecoveryAttempting {

  func attemptRecovery(from error: Error, completion: @escaping (Bool) -> Void) {
    guard
      let dependencies = try? Self.getDependencies(),
      let currentPermissions = dependencies.accessTokenProvider.current?.permissions.map(\.name),
      !currentPermissions.isEmpty
    else {
      completion(false)
      return
    }

    dependencies.loginProvider.logIn(permissions: currentPermissions, from: nil) { result, error in
      guard
        error == nil,
        let result = result
      else {
        completion(false)
        return
      }

      // we can only consider a recovery successful if there are no declines
      // (note this could still set an updated currentAccessToken).
      completion(!result.isCancelled && result.declinedPermissions.isEmpty)
    }
  }
}

extension LoginRecoveryAttempter: DependentAsType {
  struct TypeDependencies {
    var loginProvider: LoginProviding
    var accessTokenProvider: _AccessTokenProviding.Type
  }

  static var defaultDependencies: TypeDependencies? = TypeDependencies(
    loginProvider: LoginManager(),
    accessTokenProvider: AccessToken.self
  )

  static var configuredDependencies: TypeDependencies?
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import FBSDKCoreKit
import FBSDKCoreKit_Basics
import Foundation

/**
 Extracts the log in completion parameters from the parameters dictionary,
 which must contain the parsed result of the return URL query string.

 The  user_id key is first used to derive the User ID. If that fails,  signed_request
 is used.

 Completion occurs synchronously.
 */

struct LoginURLCompleter: LoginCompleting {
  let parameters = _LoginCompletionParameters()

  init(urlParameters: [String: Any], appID: String) {
    let nonce = urlParameters["nonce"] as? String ?? ""
    let idToken = urlParameters["id_token"] as? String ?? ""
    let accessToken = urlParameters["access_token"] as? String ?? ""
    let code = urlParameters["code"] as? String ?? ""

    let hasNonEmptyNonceString = !nonce.isEmpty
    let hasNonEmptyIdTokenString = !idToken.isEmpty
    let hasNonEmptyAccessTokenString = !accessToken.isEmpty
    let hasNonEmptyCodeString = !code.isEmpty

    // Code and id token are mutually exclusive parameters
    let hasBothCodeAndIdToken = hasNonEmptyCodeString && hasNonEmptyIdTokenString
    let hasEitherCodeOrIdToken = hasNonEmptyCodeString || hasNonEmptyIdTokenString

    // Nonce and id token are mutually exclusive parameters
    let hasBothNonceAndIdToken = hasNonEmptyNonceString && hasNonEmptyIdTokenString
    let hasEitherNonceOrIdToken = hasNonEmptyNonceString || hasNonEmptyIdTokenString

    if hasNonEmptyAccessTokenString ||
      (hasEitherCodeOrIdToken && !hasBothCodeAndIdToken) ||
      (hasEitherNonceOrIdToken && !hasBothNonceAndIdToken) {
      setParameters(values: urlParameters, appID: appID)
    } else if urlParameters["error"] as? String != nil ||
      urlParameters["error_message"] as? String != nil {
      parameters.error = error(from: urlParameters)
    } else if hasBothCodeAndIdToken,
              let errorFactory = try? Self.getDependencies().errorFactory {
      // If no Access Token is returned with ID Token, we assume that
      // tracking perference is limited. We currently cannot perform
      // code exchange with limit tracking, thus this is not a valid
      // state
      parameters.error = errorFactory.error(
        code: LoginError.unknown.rawValue,
        userInfo: nil,
        message: "Invalid server response. Please try to login again",
        underlyingError: nil
      )
    }
  }

  /// Performs the work needed to populate the login completion parameters before they
  /// are used to determine login success, failure or cancellation.
  func completeLogin(handler: @escaping LoginCompletionParametersBlock) {
    completeLogin(nonce: nil, codeVerifier: nil, handler: handler)
  }

  func completeLogin(
    nonce: String?,
    codeVerifier: String?,
    handler: @escaping LoginCompletionParametersBlock
  ) {
    if parameters.code != nil {
      exchangeCodeForTokensWith(nonce: nonce, codeVerifier: codeVerifier, handler: handler)
    } else if parameters.nonceString != nil {
      exchangeNonceForTokenWith(handler: handler, authenticationNonce: nonce ?? "")
    } else if parameters.authenticationTokenString != nil,
              nonce == nil,
              let errorFactory = try? Self.getDependencies().errorFactory {

      parameters.error = errorFactory.error(
        code: LoginError.unknown.rawValue,
        userInfo: nil,
        message: "Please try to login again",
        underlyingError: nil
      )
      handler(parameters)
    } else if parameters.authenticationTokenString != nil, let nonce = nonce {
      fetchAndSetPropertiesFor(parameters: parameters, nonce: nonce, handler: handler)
    } else {
      handler(parameters)
    }
  }

  func fetchAndSetPropertiesFor(
    parameters: _LoginCompletionParameters,
    nonce: String,
    handler: @escaping LoginCompletionParametersBlock
  ) {
    guard
      let dependencies = try? Self.getDependencies()
    else {
      return
    }

    dependencies.authenticationTokenCreator.createToken(
      tokenString: parameters.authenticationTokenString ?? "",
      nonce: nonce,
      graphDomain: parameters.graphDomain ?? ""
    ) { token in
      if let token = token {
        parameters.authenticationToken = token
        if let claims = token.claims() {
          parameters.profile = self.profile(with: claims)
        }
      } else {
        parameters.error = dependencies.errorFactory.error(
          code: LoginError.invalidIDToken.rawValue,
          userInfo: nil,
          message: "Invalid ID token from login response.",
          underlyingError: nil
        )
      }

      handler(parameters)
    }
  }

  func setParameters(values: [String: Any], appID: String) {
    parameters.accessTokenString = values["access_token"] as? String
    parameters.nonceString = values["nonce"] as? String
    parameters.authenticationTokenString = values["id_token"] as? String
    parameters.code = values["code"] as? String

    let grantedPermissionsString = values["granted_scopes"] as? String ?? ""
    let declinedPermissionsString = values["denied_scopes"] as? String ?? ""

    let grantedPermissionSet = FBPermission.permissions(
      fromRawPermissions: Set(grantedPermissionsString.components(separatedBy: ","))
    )
    let declinedPermissionSet = FBPermission.permissions(
      fromRawPermissions: Set(declinedPermissionsString.components(separatedBy: ","))
    )

    // check the string length so that we assign an empty set rather than a set with an empty string
    parameters.permissions = grantedPermissionsString.isEmpty ? Set<FBPermission>() : grantedPermissionSet
    parameters.declinedPermissions = declinedPermissionsString.isEmpty ? Set<FBPermission>() : declinedPermissionSet
    parameters.expiredPermissions = []
    parameters.appID = appID

    let userID = values["user_id"] as? String

    if let userID = userID,
       userID.isEmpty,
       let signedRequest = values["signed_request"] as? String,
       !signedRequest.isEmpty {
      parameters.userID = LoginUtility.getUserID(from: signedRequest)
    } else {
      parameters.userID = userID
    }

    if let domain = values["graph_domain"] as? String,
       !domain.isEmpty {
      parameters.graphDomain = domain
    }

    parameters.expirationDate = expirationDateFrom(parameters: values)
    parameters.dataAccessExpirationDate = dataAccessExpirationDateFrom(parameters: values)
    parameters.challenge = challenge(from: values)
  }

  func exchangeNonceForTokenWith(handler: @escaping LoginCompletionParametersBlock, authenticationNonce: String) {
    let nonce = parameters.nonceString ?? ""
    let appID = parameters.appID ?? ""

    guard let dependencies = try? Self.getDependencies() else {
      return
    }

    guard
      !nonce.isEmpty,
      !appID.isEmpty
    else {
      parameters.error = dependencies.errorFactory.error(
        code: CoreError.errorInvalidArgument.rawValue,
        userInfo: nil,
        message: "Missing required parameters to exchange nonce for access token.",
        underlyingError: nil
      )
      handler(parameters)
      return
    }

    let request = dependencies.graphRequestFactory.createGraphRequest(
      withGraphPath: "oauth/access_token",
      parameters: [
        "grant_type": "fb_exchange_nonce",
        "fb_exchange_nonce": nonce,
        "client_id": appID,
        "fields": "",
      ],
      flags: [.doNotInvalidateTokenOnError, .disableErrorRecovery]
    )

    request.start { [self] _, result, graphRequestError in
      guard
        graphRequestError == nil
      else {
        parameters.error = graphRequestError
        handler(parameters)
        return
      }

      if let result = result as? [String: Any] {
        parameters.accessTokenString = result["access_token"] as? String
        parameters.expirationDate = expirationDateFrom(parameters: result)
        parameters.dataAccessExpirationDate = dataAccessExpirationDateFrom(parameters: result)
        parameters.authenticationTokenString = result["id_token"] as? String
      }

      if parameters.authenticationTokenString != nil {
        fetchAndSetPropertiesFor(parameters: parameters, nonce: authenticationNonce, handler: handler)
        return
      }

      handler(parameters)
    }
  }

  func exchangeCodeForTokensWith(
    nonce: String?,
    codeVerifier: String?,
    handler: @escaping LoginCompletionParametersBlock
  ) {
    let code = parameters.code ?? ""
    let appID = parameters.appID ?? ""

    guard let dependencies = try? Self.getDependencies() else {
      return
    }

    guard
      !code.isEmpty,
      !appID.isEmpty,
      let codeVerifier = codeVerifier,
      !codeVerifier.isEmpty
    else {
      parameters.error = dependencies.errorFactory.error(
        code: CoreError.errorInvalidArgument.rawValue,
        userInfo: nil,
        message: "Missing required parameters to exchange nonce for access token.",
        underlyingError: nil
      )
      handler(parameters)
      return
    }

    let redirectURL = try? dependencies.internalUtility.appURL(withHost: "authorize", path: "", queryParameters: [:])
    let request = dependencies.graphRequestFactory.createGraphRequest(
      withGraphPath: "oauth/access_token",
      parameters: [
        "client_id": appID,
        "redirect_uri": redirectURL?.absoluteString as Any,
        "code_verifier": codeVerifier,
        "code": code,
      ],
      flags: [.doNotInvalidateTokenOnError, .disableErrorRecovery]
    )

    request.start { [self] _, result, graphRequestError in
      parameters.code = nil

      guard
        graphRequestError == nil,
        let result = result as? [String: Any]
      else {
        parameters.error = graphRequestError
        completeLogin(nonce: nonce, codeVerifier: nil, handler: handler)
        return
      }

      if result["error"] != nil,
         let errorFactory = try? Self.getDependencies().errorFactory {
        parameters.error = errorFactory.error(
          code: CoreError.errorInvalidArgument.rawValue,
          userInfo: nil,
          message: "Failed to exchange code for Access Token",
          underlyingError: nil
        )
      } else {
        parameters.accessTokenString = result["access_token"] as? String
        parameters.expirationDate = expirationDateFrom(parameters: result)
        parameters.authenticationTokenString = result["id_token"] as? String
      }

      completeLogin(nonce: nonce, codeVerifier: nil, handler: handler)
    }
  }

  func profile(with claims: AuthenticationTokenClaims) -> Profile? {
    guard
      !claims.sub.isEmpty,
      let profileFactory = try? Self.getDependencies().profileFactory
    else {
      return nil
    }

    var imageURL: URL?
    if let picture = claims.picture {
      imageURL = URL(string: picture)
    }

    var birthday: Date?
    if let userBirthday = claims.userBirthday {
      let dateFormatter = DateFormatter()
      dateFormatter.dateFormat = "MM/dd/yyyy"
      birthday = dateFormatter.date(from: userBirthday)
    }

    return profileFactory.createProfile(
      userID: claims.sub,
      firstName: claims.givenName,
      middleName: claims.middleName,
      lastName: claims.familyName,
      name: claims.name,
      linkURL: URL(string: claims.userLink ?? ""),
      refreshDate: nil,
      imageURL: imageURL,
      email: claims.email,
      friendIDs: claims.userFriends,
      birthday: birthday,
      ageRange: UserAgeRange(from: claims.userAgeRange ?? [:]),
      hometown: Location(from: claims.userHometown ?? [:]),
      location: Location(from: claims.userLocation ?? [:]),
      gender: claims.userGender,
      isLimited: true
    )
  }

  func expirationDateFrom(parameters: [String: Any]) -> Date {
    let expires = parameters["expires"] as? TimeInterval
    let expiresAt = parameters["expires_at"] as? TimeInterval
    let expiresIn = parameters["expires_in"] as? TimeInterval

    let expirationDate = expires ?? expiresAt

    if let expirationDate = expirationDate,
       expirationDate > 0 {
      return Date(timeIntervalSince1970: expirationDate)
    } else if let expiresIn = expiresIn,
              expiresIn > 0 {
      return Date(timeIntervalSinceNow: expiresIn)
    } else {
      return Date.distantFuture
    }
  }

  func dataAccessExpirationDateFrom(parameters: [String: Any]) -> Date {
    guard
      let dataAccessExpirationDate = parameters["data_access_expiration_time"] as? Double,
      dataAccessExpirationDate > 0
    else {
      return Date.distantFuture
    }

    return Date(timeIntervalSince1970: dataAccessExpirationDate)
  }

  func challenge(from parameters: [String: Any]) -> String? {
    guard
      let stateString = parameters["state"] as? String,
      let state = try? BasicUtility.object(forJSONString: stateString) as? [String: Any],
      let challenge = state["challenge"] as? String,
      !challenge.isEmpty
    else {
      return nil
    }

    return Utility.decode(urlString: challenge)
  }

  func error(from urlParameters: [String: Any]) -> Error? {
    guard let errorMessage = urlParameters["error_message"] as? String else {
      return nil
    }

    var userInfo = [String: Any]()
    userInfo[ErrorDeveloperMessageKey] = errorMessage

    if let error = urlParameters["error"] as? String {
      userInfo[ErrorDeveloperMessageKey] = error
    }

    if let errorCode = urlParameters["error_code"] as? String {
      userInfo[GraphRequestErrorGraphErrorCodeKey] = errorCode
    }

    if userInfo[ErrorDeveloperMessageKey] == nil,
       let errorReason = urlParameters["error_reason"] as? String {
      userInfo[ErrorDeveloperMessageKey] = errorReason
    }

    userInfo[GraphRequestErrorKey] = GraphRequestError.other

    return NSError(
      domain: ErrorDomain,
      code: CoreError.errorGraphRequestGraphAPI.rawValue,
      userInfo: userInfo
    )
  }
}

extension LoginURLCompleter: DependentAsType {
  struct TypeDependencies {
    var profileFactory: ProfileCreating
    var authenticationTokenCreator: AuthenticationTokenCreating
    var graphRequestFactory: GraphRequestFactoryProtocol
    var internalUtility: URLHosting
    var errorFactory: ErrorCreating
  }

  static var configuredDependencies: TypeDependencies?
  static var defaultDependencies: TypeDependencies? = TypeDependencies(
    profileFactory: ProfileFactory(),
    authenticationTokenCreator: AuthenticationTokenFactory(),
    graphRequestFactory: GraphRequestFactory(),
    internalUtility: InternalUtility.shared,
    errorFactory: _ErrorFactory()
  )
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import FBSDKCoreKit
import FBSDKCoreKit_Basics

enum LoginUtility {
  static func stringForAudience(_ audience: DefaultAudience) -> String {
    switch audience {
    case .onlyMe:
      return "only_me"
    case .friends:
      return "friends"
    case .everyone:
      return "everyone"
    @unknown default:
      return ""
    }
  }

  static func getQueryParameters(from url: URL) -> [String: Any]? {
    let appURL = try? InternalUtility.shared.appURL(
      withHost: "authorize",
      path: "",
      queryParameters: [:]
    )

    if let prefix = appURL?.absoluteString,
       !url.absoluteString.hasPrefix(prefix),
       url.host != "authorize" { // Don't have an App ID, just verify path.
      return nil
    }
    var params = InternalUtility.shared.parameters(fromFBURL: url)
    if let userID = Self.getUserID(from: params["signed_request"] as? String) {
      params["user_id"] = userID
    }

    return params
  }

  static func getUserID(from signedRequest: String?) -> String? {
    guard let signedRequest = signedRequest else {
      return nil
    }
    let signatureAndPayload = signedRequest.components(separatedBy: ".")
    var userID: String?

    if signatureAndPayload.count == 2 {
      let payload = signatureAndPayload[1]
      if let data = Base64.decode(asData: payload),
         let dictionary = try? TypeUtility.jsonObject(with: data) as? [String: Any] {
        userID = dictionary["user_id"] as? String
      }
    }

    return userID
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

struct MissingDependenciesError<Dependent>: Error, CustomStringConvertible {
  private let dependentType: Dependent.Type

  init(for dependentType: Dependent.Type) {
    self.dependentType = dependentType
  }

  var description: String {
    "The dependencies for the type '\(dependentType)' or an instance of it have not been set"
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Foundation

struct MonotonicTimer {
  private static let timebaseInfo: mach_timebase_info = {
    var info = mach_timebase_info()
    guard mach_timebase_info(&info) == 0 else {
      fatalError("Unable to get timebase information for monotomic timing")
    }

    return info
  }()

  private func getNanoseconds() -> UInt64 {
    (mach_absolute_time() * UInt64(Self.timebaseInfo.numer)) / UInt64(Self.timebaseInfo.denom)
  }

  func getCurrentSeconds() -> TimeInterval {
    Double(getNanoseconds()) / 1_000_000_000.0
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Foundation

enum NonceValidator {
  static func isValid(nonce: String) -> Bool {
    guard !nonce.isEmpty else { return false }

    return nonce.rangeOfCharacter(from: .whitespaces) == nil
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import FBSDKCoreKit

protocol ProfileCreating {

  // swiftlint:disable:next function_parameter_count
  func createProfile(
    userID: UserIdentifier,
    firstName: String?,
    middleName: String?,
    lastName: String?,
    name: String?,
    linkURL: URL?,
    refreshDate: Date?,
    imageURL: URL?,
    email: String?,
    friendIDs: [String]?,
    birthday: Date?,
    ageRange: UserAgeRange?,
    hometown: Location?,
    location: Location?,
    gender: String?,
    isLimited: Bool
  ) -> Profile
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import FBSDKCoreKit

final class ProfileFactory: NSObject, ProfileCreating {

  // swiftlint:disable:next function_parameter_count
  func createProfile(
    userID: UserIdentifier,
    firstName: String?,
    middleName: String?,
    lastName: String?,
    name: String?,
    linkURL: URL?,
    refreshDate: Date?,
    imageURL: URL?,
    email: String?,
    friendIDs: [String]?,
    birthday: Date?,
    ageRange: UserAgeRange?,
    hometown: Location?,
    location: Location?,
    gender: String?,
    isLimited: Bool
  ) -> Profile {
    Profile(
      userID: userID,
      firstName: firstName,
      middleName: middleName,
      lastName: lastName,
      name: name,
      linkURL: linkURL,
      refreshDate: refreshDate,
      imageURL: imageURL,
      email: email,
      friendIDs: friendIDs,
      birthday: birthday,
      ageRange: ageRange,
      hometown: hometown,
      location: location,
      gender: gender,
      isLimited: isLimited
    )
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import FBSDKCoreKit

extension ServerConfigurationProvider: ServerConfigurationProviding {}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import FBSDKCoreKit

protocol ServerConfigurationProviding {
  func loadServerConfiguration(completion: LoginTooltipBlock?)
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import UIKit

protocol UserInterfaceElementProviding {
  func topMostViewController() -> UIViewController?
  func viewController(for view: UIView) -> UIViewController?
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Foundation

protocol UserInterfaceStringProviding {
  var bundleForStrings: Bundle { get }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import FBSDKCoreKit
import Foundation

/**
 Internal Type exposed to facilitate transition to Swift.
 API Subject to change or removal without warning. Do not use.
 - Warning INTERNAL:  DO NOT USE

 Structured interface for accessing the parameters used to complete a log in request.
 If `authenticationTokenString` is non-`nil`, the authentication succeeded. If `error` is
 non-`nil` the request failed. If both are `nil`, the request was cancelled.
 */
@objcMembers
@objc(FBSDKLoginCompletionParameters)
public final class _LoginCompletionParameters: NSObject {
  public var authenticationToken: AuthenticationToken?
  public var profile: Profile?
  public var accessTokenString: String?
  public var nonceString: String?
  public var authenticationTokenString: String?
  public var code: String?
  public var permissions: Set<FBPermission>?
  public var declinedPermissions: Set<FBPermission>?
  public var expiredPermissions: Set<FBPermission>?
  public var appID: String?
  public var userID: String?
  public var error: Error?
  public var expirationDate: Date?
  public var dataAccessExpirationDate: Date?
  public var challenge: String?
  public var graphDomain: String?
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

@testable import FBSDKLoginKit

import FBSDKCoreKit_Basics
import TestTools
import XCTest

final class AuthenticationTokenHeaderTests: XCTestCase {

  // swiftlint:disable implicitly_unwrapped_optional
  var headerDictionary: [String: Any]!
  var header: AuthenticationTokenHeader!
  // swiftlint:enable implicitly_unwrapped_optional

  override func setUp() {
    super.setUp()

    headerDictionary = [
      "alg": "RS256",
      "typ": "JWT",
      "kid": "abcd1234",
    ]

    guard
      let headerData = try? JSONSerialization.data(withJSONObject: headerDictionary as Any, options: []),
      let encodedHeader = try? base64URLEncoded(headerData)
    else {
      return
    }

    header = AuthenticationTokenHeader(fromEncodedString: encodedHeader)
  }

  override func tearDown() {
    header = nil
    headerDictionary = nil

    super.tearDown()
  }

  // MARK: - Decoding Header

  func testDecodeValidHeader() throws {
    XCTAssertEqual(header.kid, "abcd1234")
  }

  func testDecodeInvalidFormatHeader() throws {
    let headerData = try XCTUnwrap("invalid_header".data(using: .utf8))
    let encodedHeader = try base64URLEncoded(headerData)

    XCTAssertNil(AuthenticationTokenHeader(fromEncodedString: encodedHeader))
  }

  func testDecodeInvalidHeader() throws {
    try assertDecodeHeaderFailWithInvalidEntry(key: "kid", value: nil)
    try assertDecodeHeaderFailWithInvalidEntry(key: "kid", value: "")
  }

  func testDecodeEmptyHeader() throws {
    let header = [String: Any]()
    let headerData = try JSONSerialization.data(withJSONObject: header, options: [])
    let encodedHeader = try base64URLEncoded(headerData)

    XCTAssertNil(AuthenticationTokenHeader(fromEncodedString: encodedHeader))
  }

  func testDecodeRandomHeader() throws {
    try (1 ..< 100).forEach { _ in
      let randomizedHeader = Fuzzer.randomize(json: headerDictionary as Any)
      guard JSONSerialization.isValidJSONObject(randomizedHeader) else { return }

      let headerData = try JSONSerialization.data(withJSONObject: randomizedHeader, options: [])
      let encodedHeader = try base64URLEncoded(headerData)
      _ = AuthenticationTokenHeader(fromEncodedString: encodedHeader)
    }
  }

  // MARK: - Helpers

  func base64URLEncoded(_ data: Data) throws -> String {
    data.base64EncodedString()
      .replacingOccurrences(of: "+", with: "-")
      .replacingOccurrences(of: "/", with: "_")
      .replacingOccurrences(of: "=", with: "")
  }

  func assertDecodeHeaderFailWithInvalidEntry(
    key: String,
    value: Any?,
    _ file: StaticString = #file,
    _ line: UInt = #line
  ) throws {
    headerDictionary[key] = value

    let headerData = try JSONSerialization.data(withJSONObject: headerDictionary as Any, options: [])
    let encodedHeader = try base64URLEncoded(headerData)

    XCTAssertNil(
      AuthenticationTokenHeader(fromEncodedString: encodedHeader),
      """
      Should not be able to create a token from a dictionary
      with the value: \(String(describing: value)) for the key: \(key)
      """,
      file: file,
      line: line
    )
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

@testable import FBSDKLoginKit

import XCTest

final class CodeVerifierTests: XCTestCase {

  func testDefaultInitialization() {
    let codeVerifier = CodeVerifier()
    XCTAssertNotNil(codeVerifier.value)
  }

  func testInvalidCodeVerifers() {
    [
      "",
      "abcdefg", // too short
      String(repeating: "a", count: 129), // too long
      "OB6imf0pt3YLueBtNlljrdJZXdFk8gKJ97UQEpz1ojlEUL4p=", // invalid character
    ].forEach { codeVerifier in
      XCTAssertNil(
        CodeVerifier(string: codeVerifier),
        "Should not consider: \(codeVerifier) to be a valid code verifier"
      )
    }
  }

  func testValidCodeVerifers() {
    [
      String(repeating: "a", count: 43),
      String(repeating: "a", count: 128),
      "OB6imf0pt3YLueBtNlljrdJZXdFk8gKJ97UQEpz1ojlEUL4p",
      "OB6imf0pt3YLueBtNlljrdJZXdFk8gKJ97UQEpz1ojlEUL4p-._~",
    ].forEach { codeVerifier in
      XCTAssertNotNil(
        CodeVerifier(string: codeVerifier),
        "Should consider: \(codeVerifier) to be a valid code verifier"
      )
    }
  }

  func testCodeChallenge() throws {
    let codeVerifier = CodeVerifier(string: "OB6imf0pt3YLueBtNlljrdJZXdFk8gKJ97UQEpz1ojlEUL4p")
    let challenge = try XCTUnwrap(codeVerifier?.challenge)
    XCTAssertEqual(challenge, "4oJe_Hdk_xC_YCWjumgE5RjIkW-YVmFm_nYnIckxR0c")
  }
}