import AppAuth
import CryptoKit
import Foundation
import GoogleSignIn
import GTMAppAuth
import NitroModules
import UIKit

enum GoogleSignInNativeError: Error, LocalizedError {
  case playServicesNotAvailable(String)
  case oneTapStartFailed(String)
  case signInRequired(String)
  case notConfigured
  case noActivity

  var errorDescription: String? {
    switch self {
    case .playServicesNotAvailable(let msg),
         .oneTapStartFailed(let msg),
         .signInRequired(let msg):
      return msg
    case .notConfigured:
      return "Google Sign-In is not configured. Call configure() first."
    case .noActivity:
      return "No view controller available to present sign-in."
    }
  }

  var code: String {
    switch self {
    case .playServicesNotAvailable:
      return "PLAY_SERVICES_NOT_AVAILABLE"
    case .oneTapStartFailed:
      return "ONE_TAP_START_FAILED"
    case .signInRequired:
      return "SIGN_IN_REQUIRED"
    case .notConfigured, .noActivity:
      return "IN_PROGRESS"
    }
  }
}

class HybridNitroGoogleSignin: HybridNitroGoogleSigninSpec {
  private var configured = false
  private var webClientId: String?
  private var offlineAccess = false
  private var configuredNonce: String?
  private var configuredScopes: [String] = []

  func configure(params: OneTapConfigureParams) throws {
    let resolvedWeb = try Self.resolveWebClientId(params.webClientId)
    webClientId = resolvedWeb

    let iosClientId = Self.variantToString(params.iosClientId)
      ?? Self.readPlistString(key: "CLIENT_ID")
    guard let iosClientId, !iosClientId.isEmpty else {
      throw GoogleSignInNativeError.notConfigured
    }

    offlineAccess = params.offlineAccess ?? false
    configuredNonce = Self.variantToString(params.nonce)
    configuredScopes = Self.variantToStringArray(params.scopes)
    let hostedDomain = Self.variantToString(params.hostedDomain)

    let config = GIDConfiguration(
      clientID: iosClientId,
      serverClientID: resolvedWeb,
      hostedDomain: hostedDomain,
      openIDRealm: nil
    )
    if Thread.isMainThread {
      GIDSignIn.sharedInstance.configuration = config
    } else {
      DispatchQueue.main.sync {
        GIDSignIn.sharedInstance.configuration = config
      }
    }
    configured = true
  }

  func checkPlayServices(showErrorResolutionDialog: Bool?) throws -> Promise<Void> {
    // Play Services are Android-only; always succeed on Apple platforms.
    return Promise.resolved()
  }

  func signIn() throws -> Promise<OneTapResponse> {
    try ensureConfigured()
    return Promise.async {
      let user = await MainActor.run {
        GIDSignIn.sharedInstance.currentUser
      }
      if let user {
        return Self.success(from: user, serverAuthCode: nil)
      }
      return try await self.restorePreviousSignIn()
    }
  }

  func createAccount() throws -> Promise<OneTapResponse> {
    try ensureConfigured()
    return Promise.async {
      try await self.interactiveSignIn()
    }
  }

  func presentExplicitSignIn() throws -> Promise<OneTapResponse> {
    try ensureConfigured()
    return Promise.async {
      try await self.interactiveSignIn()
    }
  }

  func signOut() throws -> Promise<Void> {
    return Promise.async {
      await MainActor.run {
        GIDSignIn.sharedInstance.signOut()
      }
    }
  }

  func revokeAccess(emailOrUniqueId: String) throws -> Promise<Void> {
    return Promise.async {
      try await MainActor.run {
        guard let currentUser = GIDSignIn.sharedInstance.currentUser else {
          throw GoogleSignInNativeError.signInRequired(
            "No signed-in Google user. Sign in before calling revokeAccess()."
          )
        }
        let userId = currentUser.userID ?? ""
        let email = currentUser.profile?.email ?? ""
        let matches =
          emailOrUniqueId == userId
          || (!email.isEmpty && emailOrUniqueId == email)
        guard matches else {
          throw GoogleSignInNativeError.oneTapStartFailed(
            "emailOrUniqueId does not match the current signed-in user on iOS. " +
              "Only the active session can be revoked; call signIn() first or use signOut()."
          )
        }
      }
      try await withCheckedThrowingContinuation {
        (continuation: CheckedContinuation<Void, Error>) in
        DispatchQueue.main.async {
          GIDSignIn.sharedInstance.disconnect { error in
            if let error {
              continuation.resume(throwing: error)
            } else {
              continuation.resume()
            }
          }
        }
      }
    }
  }

  func requestScopes(scopes: [String]) throws -> Promise<OneTapAuthorizationResult> {
    try ensureConfigured()
    return Promise.async {
      try await self.requestAdditionalScopes(scopes)
    }
  }

  func getCurrentUser() throws -> Variant_NullType_OneTapSuccessData {
    if let user = GIDSignIn.sharedInstance.currentUser {
      return .second(Self.successData(from: user, serverAuthCode: nil))
    }
    return .first(NullType.null)
  }

  func getTokens() throws -> Promise<GetTokensResponse> {
    try ensureConfigured()
    return Promise.async {
      try await self.fetchTokens()
    }
  }

  func clearCachedAccessToken(accessTokenString: String) throws -> Promise<Void> {
    try ensureConfigured()
    return Promise.async {
      try await self.invalidateCachedAccessToken(accessTokenString)
    }
  }

  // MARK: - Sign-in flows

  @MainActor
  private func restorePreviousSignIn() async throws -> OneTapResponse {
    try await withCheckedThrowingContinuation { continuation in
      DispatchQueue.main.async {
        GIDSignIn.sharedInstance.restorePreviousSignIn { user, error in
          if let error = error as NSError? {
            if let response = Self.response(forSignInError: error) {
              continuation.resume(returning: response)
              return
            }
            continuation.resume(
              throwing: GoogleSignInNativeError.oneTapStartFailed(error.localizedDescription)
            )
            return
          }
          guard let user else {
            continuation.resume(returning: Self.noSavedCredential())
            return
          }
          continuation.resume(returning: Self.success(from: user, serverAuthCode: nil))
        }
      }
    }
  }

  @MainActor
  private func interactiveSignIn() async throws -> OneTapResponse {
    guard let presenting = Self.topViewController() else {
      throw GoogleSignInNativeError.noActivity
    }

    let additionalScopes = configuredScopes.isEmpty ? nil : configuredScopes
    let nonce = Self.resolveNonce(configuredNonce)

    return try await withCheckedThrowingContinuation { continuation in
      DispatchQueue.main.async {
        Self.signIn(
          presenting: presenting,
          additionalScopes: additionalScopes,
          nonce: nonce
        ) { result, error in
          if let error = error as NSError? {
            if let response = Self.response(forSignInError: error) {
              continuation.resume(returning: response)
              return
            }
            continuation.resume(
              throwing: GoogleSignInNativeError.oneTapStartFailed(error.localizedDescription)
            )
            return
          }
          guard let user = result?.user else {
            continuation.resume(returning: Self.cancelled())
            return
          }
          continuation.resume(
            returning: Self.success(from: user, serverAuthCode: result?.serverAuthCode)
          )
        }
      }
    }
  }

  // MARK: - Mapping

  private static func success(from user: GIDGoogleUser, serverAuthCode: String?) -> OneTapResponse {
    let data = successData(from: user, serverAuthCode: serverAuthCode)
    return OneTapResponse(type: .success, data: .second(data))
  }

  private static func successData(from user: GIDGoogleUser, serverAuthCode: String?) -> OneTapSuccessData {
    let profile = user.profile
    let oneTapUser = OneTapUser(
      id: user.userID ?? "",
      email: optionalStringVariant(profile?.email),
      name: optionalStringVariant(profile?.name),
      givenName: optionalStringVariant(profile?.givenName),
      familyName: optionalStringVariant(profile?.familyName),
      photo: optionalStringVariant(profile?.imageURL(withDimension: 320)?.absoluteString)
    )
    let scopes = user.grantedScopes ?? []
    return OneTapSuccessData(
      user: oneTapUser,
      scopes: scopes,
      idToken: user.idToken?.tokenString ?? "",
      serverAuthCode: optionalStringVariant(serverAuthCode)
    )
  }

  private static func noSavedCredential() -> OneTapResponse {
    OneTapResponse(type: .nosavedcredentialfound, data: nil)
  }

  private static func cancelled() -> OneTapResponse {
    OneTapResponse(type: .cancelled, data: nil)
  }

  /// Maps known `GIDSignIn` errors to API responses. Returns `nil` if the error should be thrown.
  private static func response(forSignInError error: NSError) -> OneTapResponse? {
    switch error.code {
    case GIDSignInError.hasNoAuthInKeychain.rawValue:
      return noSavedCredential()
    case GIDSignInError.canceled.rawValue:
      return cancelled()
    case GIDSignInError.scopesAlreadyGranted.rawValue:
      guard let user = GIDSignIn.sharedInstance.currentUser else {
        return nil
      }
      return success(from: user, serverAuthCode: nil)
    default:
      return nil
    }
  }

  private static func optionalStringVariant(_ value: String?) -> Variant_NullType_String? {
    guard let value else {
      return .first(NullType.null)
    }
    return .second(value)
  }

  private static func variantToString(_ value: Variant_NullType_String?) -> String? {
    guard let value else { return nil }
    switch value {
    case .first:
      return nil
    case .second(let string):
      return string
    }
  }

  private static func variantToStringArray(_ value: Variant_NullType__String_?) -> [String] {
    guard let value else { return [] }
    switch value {
    case .first:
      return []
    case .second(let strings):
      return strings
    }
  }

  @MainActor
  private func invalidateCachedAccessToken(_ accessTokenString: String) async throws {
    guard let user = GIDSignIn.sharedInstance.currentUser else {
      throw GoogleSignInNativeError.signInRequired(
        "No signed-in Google user. Sign in before clearing a cached access token."
      )
    }

    if !accessTokenString.isEmpty, user.accessToken.tokenString != accessTokenString {
      throw GoogleSignInNativeError.oneTapStartFailed(
        "The provided access token does not match the current user's access token."
      )
    }

    guard let authSession = user.fetcherAuthorizer as? AuthSession else {
      throw GoogleSignInNativeError.oneTapStartFailed(
        "Unable to access the Google Sign-In token session."
      )
    }

    authSession.authState.setNeedsTokenRefresh()
  }

  @MainActor
  private func fetchTokens() async throws -> GetTokensResponse {
    guard let user = GIDSignIn.sharedInstance.currentUser else {
      throw GoogleSignInNativeError.signInRequired(
        "No signed-in Google user. Sign in before calling getTokens()."
      )
    }

    if let authSession = user.fetcherAuthorizer as? AuthSession {
      return try await fetchTokens(using: authSession.authState)
    }

    return try await fetchTokensViaRefreshIfNeeded(user: user)
  }

  @MainActor
  private func fetchTokens(using authState: OIDAuthState) async throws -> GetTokensResponse {
    try await withCheckedThrowingContinuation { continuation in
      authState.performAction(freshTokens: { accessToken, idToken, error in
        if let error {
          continuation.resume(
            throwing: GoogleSignInNativeError.oneTapStartFailed(error.localizedDescription)
          )
          return
        }

        guard let accessToken, !accessToken.isEmpty else {
          continuation.resume(
            throwing: GoogleSignInNativeError.oneTapStartFailed(
              "No access token returned from Google Sign-In."
            )
          )
          return
        }

        guard let idToken, !idToken.isEmpty else {
          continuation.resume(
            throwing: GoogleSignInNativeError.oneTapStartFailed(
              "No ID token returned from Google Sign-In."
            )
          )
          return
        }

        continuation.resume(
          returning: GetTokensResponse(idToken: idToken, accessToken: accessToken)
        )
      })
    }
  }

  @MainActor
  private func fetchTokensViaRefreshIfNeeded(user: GIDGoogleUser) async throws
    -> GetTokensResponse
  {
    try await withCheckedThrowingContinuation { continuation in
      user.refreshTokensIfNeeded { refreshedUser, error in
        if let error {
          continuation.resume(
            throwing: GoogleSignInNativeError.oneTapStartFailed(error.localizedDescription)
          )
          return
        }

        guard
          let refreshedUser,
          let idToken = refreshedUser.idToken?.tokenString,
          !idToken.isEmpty
        else {
          continuation.resume(
            throwing: GoogleSignInNativeError.signInRequired(
              "No signed-in Google user. Sign in before calling getTokens()."
            )
          )
          return
        }

        let accessToken = refreshedUser.accessToken.tokenString
        guard !accessToken.isEmpty else {
          continuation.resume(
            throwing: GoogleSignInNativeError.oneTapStartFailed(
              "No access token returned from Google Sign-In."
            )
          )
          return
        }

        continuation.resume(
          returning: GetTokensResponse(idToken: idToken, accessToken: accessToken)
        )
      }
    }
  }

  @MainActor
  private func requestAdditionalScopes(_ scopes: [String]) async throws -> OneTapAuthorizationResult {
    guard let presenting = Self.topViewController() else {
      throw GoogleSignInNativeError.noActivity
    }
    guard GIDSignIn.sharedInstance.currentUser != nil else {
      throw GoogleSignInNativeError.oneTapStartFailed(
        "No signed-in Google user. Sign in before requesting additional scopes."
      )
    }

    // When offline access is enabled, use the shared GIDSignIn configuration (includes
    // serverClientID) instead of GIDGoogleUser.addScopes, which reads the user's
    // snapshot configuration and may omit serverClientID from an earlier session.
    if offlineAccess {
      return try await requestAdditionalScopesWithOfflineAccess(
        scopes: scopes,
        presenting: presenting
      )
    }

    guard let user = GIDSignIn.sharedInstance.currentUser else {
      throw GoogleSignInNativeError.oneTapStartFailed(
        "No signed-in Google user. Sign in before requesting additional scopes."
      )
    }

    return try await withCheckedThrowingContinuation { continuation in
      DispatchQueue.main.async {
        user.addScopes(scopes, presenting: presenting) { result, error in
          Self.completeScopeRequest(
            continuation: continuation,
            result: result,
            error: error
          )
        }
      }
    }
  }

  @MainActor
  private func requestAdditionalScopesWithOfflineAccess(
    scopes: [String],
    presenting: UIViewController
  ) async throws -> OneTapAuthorizationResult {
    let hint = GIDSignIn.sharedInstance.currentUser?.profile?.email
    let nonce = Self.resolveNonce(configuredNonce)

    return try await withCheckedThrowingContinuation { continuation in
      DispatchQueue.main.async {
        GIDSignIn.sharedInstance.signIn(
          withPresenting: presenting,
          hint: hint,
          additionalScopes: scopes,
          nonce: nonce
        ) { result, error in
          Self.completeScopeRequest(
            continuation: continuation,
            result: result,
            error: error
          )
        }
      }
    }
  }

  private static func completeScopeRequest(
    continuation: CheckedContinuation<OneTapAuthorizationResult, Error>,
    result: GIDSignInResult?,
    error: Error?
  ) {
    if let error = error as NSError? {
      if error.code == GIDSignInError.canceled.rawValue {
        continuation.resume(
          returning: OneTapAuthorizationResult(
            accessToken: optionalStringVariant(nil),
            serverAuthCode: optionalStringVariant(nil)
          )
        )
        return
      }
      if error.code == GIDSignInError.scopesAlreadyGranted.rawValue {
        let user = result?.user ?? GIDSignIn.sharedInstance.currentUser
        continuation.resume(
          returning: OneTapAuthorizationResult(
            accessToken: optionalStringVariant(user?.accessToken.tokenString),
            serverAuthCode: optionalStringVariant(result?.serverAuthCode)
          )
        )
        return
      }
      continuation.resume(
        throwing: GoogleSignInNativeError.oneTapStartFailed(error.localizedDescription)
      )
      return
    }
    continuation.resume(
      returning: OneTapAuthorizationResult(
        accessToken: optionalStringVariant(result?.user.accessToken.tokenString),
        serverAuthCode: optionalStringVariant(result?.serverAuthCode)
      )
    )
  }

  private static func resolveNonce(_ configured: String?) -> String {
    if let configured, !configured.isEmpty {
      return configured
    }
    return generateNonce()
  }

  private static func generateNonce() -> String {
    let raw = UUID().uuidString
    let digest = SHA256.hash(data: Data(raw.utf8))
    return digest.map { String(format: "%02x", $0) }.joined()
  }

  private static func signIn(
    presenting: UIViewController,
    additionalScopes: [String]?,
    nonce: String,
    completion: @escaping (GIDSignInResult?, Error?) -> Void
  ) {
    if let additionalScopes, !additionalScopes.isEmpty {
      GIDSignIn.sharedInstance.signIn(
        withPresenting: presenting,
        hint: nil,
        additionalScopes: additionalScopes,
        nonce: nonce,
        completion: completion
      )
    } else {
      GIDSignIn.sharedInstance.signIn(
        withPresenting: presenting,
        hint: nil,
        additionalScopes: nil,
        nonce: nonce,
        completion: completion
      )
    }
  }

  private func ensureConfigured() throws {
    guard configured, webClientId != nil else {
      throw GoogleSignInNativeError.notConfigured
    }
  }

  private static func resolveWebClientId(_ configuredId: String) throws -> String {
    if configuredId != "autoDetect" {
      return configuredId
    }
    if let fromPlist = readPlistString(key: "WEB_CLIENT_ID") {
      return fromPlist
    }
    throw GoogleSignInNativeError.notConfigured
  }

  private static func readPlistString(key: String) -> String? {
    guard
      let path = Bundle.main.path(forResource: "GoogleService-Info", ofType: "plist"),
      let dict = NSDictionary(contentsOfFile: path) as? [String: Any],
      let value = dict[key] as? String
    else {
      return nil
    }
    return value
  }

  @MainActor
  private static func topViewController() -> UIViewController? {
    let scenes = UIApplication.shared.connectedScenes
      .compactMap { $0 as? UIWindowScene }
    let activeScene = scenes.first { $0.activationState == .foregroundActive } ?? scenes.first
    let window: UIWindow?
    if #available(iOS 15.0, *) {
      window = activeScene?.keyWindow
        ?? activeScene?.windows.first { $0.isKeyWindow }
        ?? scenes.flatMap(\.windows).first { $0.isKeyWindow }
        ?? activeScene?.windows.first
    } else {
      window = activeScene?.windows.first { $0.isKeyWindow }
        ?? scenes.flatMap(\.windows).first { $0.isKeyWindow }
        ?? activeScene?.windows.first
    }
    guard var top = window?.rootViewController else { return nil }
    while let presented = top.presentedViewController {
      top = presented
    }
    return top
  }
}