// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import Foundation

#if os(iOS) || os(macOS)

  @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  extension MultiFactor: NSSecureCoding {}

  /// The interface defining the multi factor related properties and operations pertaining to a
  /// user.
  ///
  /// This class is available on iOS and macOS.
  @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  @objc(FIRMultiFactor) open class MultiFactor: NSObject {
    @objc open var enrolledFactors: [MultiFactorInfo]

    /// Get a session for a second factor enrollment operation.
    ///
    /// This is used to identify the current user trying to enroll a second factor.
    /// - Parameter completion: A block with the session identifier for a second factor enrollment
    /// operation.
    @objc(getSessionWithCompletion:)
    open func getSessionWithCompletion(_ completion: ((MultiFactorSession?, Error?) -> Void)?) {
      let session = MultiFactorSession.session(for: user)
      if let completion {
        completion(session, nil)
      }
    }

    /// Get a session for a second factor enrollment operation.
    ///
    /// This is used to identify the current user trying to enroll a second factor.
    @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
    open func session() async throws -> MultiFactorSession {
      return try await withCheckedThrowingContinuation { continuation in
        self.getSessionWithCompletion { session, error in
          if let session {
            continuation.resume(returning: session)
          } else {
            continuation.resume(throwing: error!)
          }
        }
      }
    }

    /// Enrolls a second factor as identified by the `MultiFactorAssertion` parameter for the
    /// current user.
    /// - Parameter assertion: The `MultiFactorAssertion`.
    /// - Parameter displayName: An optional display name associated with the multi factor to
    /// enroll.
    /// - Parameter completion: The block invoked when the request is complete, or fails.
    @objc(enrollWithAssertion:displayName:completion:)
    open func enroll(with assertion: MultiFactorAssertion,
                     displayName: String?,
                     completion: ((Error?) -> Void)?) {
      // TODO: Refactor classes so this duplicated code isn't necessary for phone and totp.

      guard
        assertion.factorID == PhoneMultiFactorInfo.TOTPMultiFactorID ||
        assertion.factorID == PhoneMultiFactorInfo.PhoneMultiFactorID
      else {
        return
      }

      guard let user, let auth = user.auth else {
        fatalError("Internal Auth error: failed to get user enrolling in MultiFactor")
      }

      let request = Self.enrollmentFinalizationRequest(
        with: assertion,
        displayName: displayName,
        user: user,
        auth: auth
      )

      Task {
        do {
          let response = try await auth.backend.call(with: request)
          let user = try await auth.completeSignIn(withAccessToken: response.idToken,
                                                   accessTokenExpirationDate: nil,
                                                   refreshToken: response.refreshToken,
                                                   anonymous: false)
          try auth.updateCurrentUser(user, byForce: false, savingToDisk: true)
          if let completion {
            DispatchQueue.main.async {
              completion(nil)
            }
          }
        } catch {
          if let completion {
            DispatchQueue.main.async {
              completion(error)
            }
          }
        }
      }
    }

    private static func enrollmentFinalizationRequest(with assertion: MultiFactorAssertion,
                                                      displayName: String?,
                                                      user: User,
                                                      auth: Auth) -> FinalizeMFAEnrollmentRequest {
      var request: FinalizeMFAEnrollmentRequest? = nil
      if assertion.factorID == PhoneMultiFactorInfo.TOTPMultiFactorID {
        guard let totpAssertion = assertion as? TOTPMultiFactorAssertion else {
          fatalError("Auth Internal Error: Failed to find TOTPMultiFactorAssertion")
        }
        switch totpAssertion.secretOrID {
        case .enrollmentID: fatalError("Missing secret in totpAssertion")
        case let .secret(secret):
          let finalizeMFATOTPRequestInfo =
            AuthProtoFinalizeMFATOTPEnrollmentRequestInfo(sessionInfo: secret.sessionInfo,
                                                          verificationCode: totpAssertion
                                                            .oneTimePassword)
          request = FinalizeMFAEnrollmentRequest(idToken: user.rawAccessToken(),
                                                 displayName: displayName,
                                                 totpVerificationInfo: finalizeMFATOTPRequestInfo,
                                                 requestConfiguration: user
                                                   .requestConfiguration)
        }
      } else if assertion.factorID == PhoneMultiFactorInfo.PhoneMultiFactorID {
        let phoneAssertion = assertion as? PhoneMultiFactorAssertion
        guard let credential = phoneAssertion?.authCredential else {
          fatalError("Internal Error: Missing credential")
        }
        switch credential.credentialKind {
        case .phoneNumber: fatalError("Internal Error: Missing verificationCode")
        case let .verification(verificationID, code):
          let finalizeMFAPhoneRequestInfo =
            AuthProtoFinalizeMFAPhoneRequestInfo(
              sessionInfo: verificationID,
              verificationCode: code
            )
          request = FinalizeMFAEnrollmentRequest(
            idToken: user.rawAccessToken(),
            displayName: displayName,
            phoneVerificationInfo: finalizeMFAPhoneRequestInfo,
            requestConfiguration: user.requestConfiguration
          )
        }
      }

      guard let request else {
        // Assertion is not a phone assertion or TOTP assertion.
        fatalError("Internal Error: Unsupported assertion with factor ID: \(assertion.factorID).")
      }

      return request
    }

    /// Enrolls a second factor as identified by the `MultiFactorAssertion` parameter for the
    /// current user.
    /// - Parameter assertion: The `MultiFactorAssertion`.
    /// - Parameter displayName: An optional display name associated with the multi factor to
    /// enroll.
    @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
    open func enroll(with assertion: MultiFactorAssertion, displayName: String?) async throws {
      return try await withCheckedThrowingContinuation { continuation in
        self.enroll(with: assertion, displayName: displayName) { error in
          if let error {
            continuation.resume(throwing: error)
          } else {
            continuation.resume()
          }
        }
      }
    }

    /// Unenroll the given multi factor.
    /// - Parameter factorInfo: The second factor instance to unenroll.
    /// - Parameter completion: The block invoked when the request to send the verification email is
    /// complete, or fails.
    @objc(unenrollWithInfo:completion:)
    open func unenroll(with factorInfo: MultiFactorInfo,
                       completion: ((Error?) -> Void)?) {
      unenroll(withFactorUID: factorInfo.uid, completion: completion)
    }

    /// Unenroll the given multi factor.
    @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
    open func unenroll(with factorInfo: MultiFactorInfo) async throws {
      try await unenroll(withFactorUID: factorInfo.uid)
    }

    /// Unenroll the given multi factor.
    /// - Parameter factorUID: The unique identifier corresponding to the
    /// second factor being unenrolled.
    /// - Parameter completion: The block invoked when the request to send the verification email is
    /// complete, or fails.
    @objc(unenrollWithFactorUID:completion:)
    open func unenroll(withFactorUID factorUID: String,
                       completion: ((Error?) -> Void)?) {
      guard let user = user, let auth = user.auth else {
        fatalError("Internal Auth error: failed to get user unenrolling in MultiFactor")
      }
      let request = WithdrawMFARequest(idToken: user.rawAccessToken(),
                                       mfaEnrollmentID: factorUID,
                                       requestConfiguration: user.requestConfiguration)
      Task {
        do {
          let response = try await auth.backend.call(with: request)
          do {
            let user = try await auth.completeSignIn(withAccessToken: response.idToken,
                                                     accessTokenExpirationDate: nil,
                                                     refreshToken: response.refreshToken,
                                                     anonymous: false)
            try auth.updateCurrentUser(user, byForce: false, savingToDisk: true)
            if let completion {
              DispatchQueue.main.async {
                completion(nil)
              }
            }
          } catch {
            DispatchQueue.main.async {
              try? auth.signOut()
              if let completion {
                completion(error)
              }
            }
          }
        } catch {
          if let completion {
            completion(error)
          }
        }
      }
    }

    /// Unenroll the given multi factor.
    @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
    open func unenroll(withFactorUID factorUID: String) async throws {
      return try await withCheckedThrowingContinuation { continuation in
        self.unenroll(withFactorUID: factorUID) { error in
          if let error {
            continuation.resume(throwing: error)
          } else {
            continuation.resume()
          }
        }
      }
    }

    weak var user: User?

    convenience init(withMFAEnrollments mfaEnrollments: [AuthProtoMFAEnrollment]) {
      self.init()
      var multiFactorInfoArray: [MultiFactorInfo] = []
      for enrollment in mfaEnrollments {
        if enrollment.phoneInfo != nil {
          let multiFactorInfo = PhoneMultiFactorInfo(proto: enrollment)
          multiFactorInfoArray.append(multiFactorInfo)
        } else if enrollment.totpInfo != nil {
          let multiFactorInfo = TOTPMultiFactorInfo(proto: enrollment)
          multiFactorInfoArray.append(multiFactorInfo)
        }
      }
      enrolledFactors = multiFactorInfoArray
    }

    override init() {
      enrolledFactors = []
    }

    // MARK: - NSSecureCoding

    private let kEnrolledFactorsCodingKey = "enrolledFactors"

    public static let supportsSecureCoding = true

    public func encode(with coder: NSCoder) {
      coder.encode(enrolledFactors, forKey: kEnrolledFactorsCodingKey)
      // Do not encode `user` weak property.
    }

    public required init?(coder: NSCoder) {
      let classes = [NSArray.self, MultiFactorInfo.self, PhoneMultiFactorInfo.self,
                     TOTPMultiFactorInfo.self]
      let enrolledFactors = coder
        .decodeObject(of: classes, forKey: kEnrolledFactorsCodingKey) as? [MultiFactorInfo]
      self.enrolledFactors = enrolledFactors ?? []
      // Do not decode `user` weak property.
    }
  }
#endif
