import BackendEnvironmentClient
import Clerk
import Combine
import ComposableArchitecture
import Foundation
import Localization
import PhoneNumberKit
import StatsigClient
import UIKit
import UserDefaultsClient
import Utilities

let log = Logger(category: "ClerkClient")

/// Time window in seconds to consider a user as "just signed up" based on their creation time
private let firstTimeUserThresholdSeconds: TimeInterval = 45.0

public enum ClerkClientError: LocalizedError {
    case invalidToken
    case invalidSession
    case unexpectedInvalidSession
    case clientNotLoaded
    case noToken
    case userMissingForPhoneValidation
    case phoneNumberNotFoundForDeletion
    case userMissingAfterAuthentication

    case noValidSessionToVerify
    case formIdentifierNotFound
    case notAllowedAccess

    /// Wraps an underlying ClerkAPIError to preserve trace IDs and other metadata
    case apiError(ClerkAPIError)

    /// Wraps any other unexpected error types
    case unexpectedError(Error)

    public var errorDescription: String? {
        /// we don't want to mention "Clerk, sessions, or tokens" to the user. we can use the underlying error case ourselves to debug. but always show a generic error to the user
        L10n.APIClient.genericSigninError
    }

    /// Wraps any error in the appropriate ClerkClientError case
    public static func wrap(_ error: Error) -> ClerkClientError {
        if let clerkError = error as? ClerkClientError {
            // Already wrapped, return as-is
            return clerkError
        } else if let clerkAPIError = error as? ClerkAPIError {
            // Wrap Clerk API errors to preserve trace IDs
            return .apiError(clerkAPIError)
        } else {
            // Wrap any other unexpected errors
            return .unexpectedError(error)
        }
    }
}

extension ClerkClientError: Equatable {
    public static func == (lhs: ClerkClientError, rhs: ClerkClientError) -> Bool {
        switch (lhs, rhs) {
        case (.apiError(let lhsAPIError), .apiError(let rhsAPIError)):
            // Compare ClerkAPIError by their code and clerkTraceId
            return lhsAPIError.code == rhsAPIError.code &&
                lhsAPIError.clerkTraceId == rhsAPIError.clerkTraceId

        case (.unexpectedError(let lhsError), .unexpectedError(let rhsError)):
            // Compare unexpected errors by their localized description
            return lhsError.localizedDescription == rhsError.localizedDescription

        case (.invalidToken, .invalidToken),
             (.invalidSession, .invalidSession),
             (.unexpectedInvalidSession, .unexpectedInvalidSession),
             (.clientNotLoaded, .clientNotLoaded),
             (.noToken, .noToken),
             (.userMissingForPhoneValidation, .userMissingForPhoneValidation),
             (.phoneNumberNotFoundForDeletion, .phoneNumberNotFoundForDeletion),
             (.userMissingAfterAuthentication, .userMissingAfterAuthentication),
             (.noValidSessionToVerify, .noValidSessionToVerify),
             (.formIdentifierNotFound, .formIdentifierNotFound),
             (.notAllowedAccess, .notAllowedAccess):
            return true

        default:
            return false
        }
    }
}

public enum ClerkAPIErrorCode: String, CaseIterable {
    // Authentication
    case formIdentifierNotFound = "form_identifier_not_found"
    case notAllowedAccess = "not_allowed_access"
    case userLocked = "user_locked"
    case userBanned = "user_banned"
    case invalidCredentials = "form_password_incorrect"
    case sessionExists = "session_exists"

    // Verification
    case verificationExpired = "verification_expired"
    case verificationFailed = "verification_failed"
    case verificationAlreadyVerified = "verification_already_verified"
    case verificationCodeTooManyRequests = "verification_code_too_many_requests"

    // Sign Up
    case signUpModeRestricted = "sign_up_mode_restricted"
    case signUpRestrictedWaitlist = "sign_up_restricted_waitlist"

    // General
    case resourceNotFound = "resource_not_found"
    case resourceForbidden = "resource_forbidden"

    // Phone/SMS
    case devMonthlySMSLimitExceeded = "dev_monthly_sms_limit_exceeded"
    case smsSignInCodeAttemptFailed = "sms_sign_in_code_attempt_failed"

    // Session
    case sessionTokenMissing = "session_token_missing"
    case sessionTokenInvalid = "session_token_invalid"
}

public extension ClerkAPIError {
    var errorCode: ClerkAPIErrorCode? {
        ClerkAPIErrorCode(rawValue: self.code)
    }
}

public enum PhoneNumberValidationStatus {
    case oneTimeCodeSent(PhoneNumberEnvelope)
    case validated
}

public struct PhoneNumberEnvelope: Equatable {
    public let raw: String
    public let e164Formatted: String
    public let clerk: ClerkPhoneNumber
}

public enum AuthenticationResult {
    case success(AuthenticationInfo)
    case failure(ClerkClientError)
}

extension AuthenticationResult: Equatable {
    public static func == (lhs: AuthenticationResult, rhs: AuthenticationResult) -> Bool {
        switch (lhs, rhs) {
        case (.success(let lhsInfo), .success(let rhsInfo)):
            return lhsInfo == rhsInfo

        case (.failure(let lhsError), .failure(let rhsError)):
            switch (lhsError, rhsError) {
            case (.apiError(let lhsAPIError), .apiError(let rhsAPIError)):
                return lhsAPIError.code == rhsAPIError.code &&
                    lhsAPIError.clerkTraceId == rhsAPIError.clerkTraceId

            case (.unexpectedError(let lhsError), .unexpectedError(let rhsError)):
                return lhsError.localizedDescription == rhsError.localizedDescription

            case (.invalidToken, .invalidToken),
                 (.invalidSession, .invalidSession),
                 (.unexpectedInvalidSession, .unexpectedInvalidSession),
                 (.clientNotLoaded, .clientNotLoaded),
                 (.noToken, .noToken),
                 (.userMissingForPhoneValidation, .userMissingForPhoneValidation),
                 (.phoneNumberNotFoundForDeletion, .phoneNumberNotFoundForDeletion),
                 (.userMissingAfterAuthentication, .userMissingAfterAuthentication),
                 (.noValidSessionToVerify, .noValidSessionToVerify),
                 (.formIdentifierNotFound, .formIdentifierNotFound),
                 (.notAllowedAccess, .notAllowedAccess):
                return true

            default:
                return false
            }

        default:
            return false
        }
    }
}

public struct AuthenticationInfo: Equatable {
    public let user: ClerkUser
    public let isFirstTimeUser: Bool

    public var fullName: String? {
        let parts = [user.firstName, user.lastName].compactMap { $0 }
        if parts.isEmpty {
            return nil
        }
        return parts.joined(separator: " ")
    }
}

@DependencyClient
public struct ClerkClient {
    public var configure: () async throws -> Void
    public var isLoggedIn: () async throws -> Bool
    /// `skipCache` will fetch a new token from the server. Should be `true` only when the token has expired
    public var jwt: (_ skipCache: Bool) async throws -> String
    public var user: () async throws -> ClerkUser?
    public var userWithAuthInfo: () async throws -> AuthenticationInfo?
    public var waitUntilReady: () async throws -> Void
    public var isReady: () async -> Bool = { false }
    /// Automatically starts phone number validation
    public var signInWithPhone: (_ phone: String) async throws -> Void
    public var validatePhoneCode: (_ code: String) async throws -> AuthenticationInfo
    public var addPhoneNumberToAccount: (_ phone: String) async throws -> PhoneNumberValidationStatus
    public var verifyAddPhoneNumberToAccount: (_ code: String, _ phoneNumber: ClerkPhoneNumber) async throws -> Void
    public var deletePhoneNumber: (_ phone: String) async throws -> Void
    public var signInWithApple: () async -> AuthenticationResult = { .failure(.noValidSessionToVerify) }
    public var signInWithGoogle: () async -> AuthenticationResult = { .failure(.noValidSessionToVerify) }
    public var signInWithMicrosoft: () async -> AuthenticationResult = { .failure(.noValidSessionToVerify) }
    public var signInWithDiscord: () async -> AuthenticationResult = { .failure(.noValidSessionToVerify) }
    public var signOut: () async throws -> Void
}

extension ClerkClient: DependencyKey {
    public static let liveValue: Self = {
        @Dependency(BackendEnvironmentClient.self) var backendEnvironment
        @Dependency(UserDefaultsClient.self) var userDefaults

        let environmentConfiguration = backendEnvironment.configuration()
        let publishableKey = environmentConfiguration.clerkPublishableKey
        guard publishableKey.isEmpty == false else {
            fatalError("CLERK_PUBLISHABLE_KEY is not defined for this configuration")
        }

        let readinessCoordinator = ClerkReadinessCoordinator()

        /// Helper to determine if a user is a first-time user based on creation time
        @Sendable
        func isFirstTimeUser(_ user: ClerkUser) -> Bool {
            // Check feature flag - if disabled, always return false (legacy behavior)
            guard FeatureFlag.general.enhancedAuthenticationFlow else {
                return false
            }

            let timeSinceCreation = Date.now.timeIntervalSince(user.createdAt)
            return timeSinceCreation <= firstTimeUserThresholdSeconds
        }

        /// Helper to wrap all errors in our ClerkClientError type consistently
        @Sendable
        func wrapClerkAPIError<T>(_ closure: () async throws -> T) async throws -> T {
            do {
                return try await closure()
            } catch {
                let isClerkApiError = error is ClerkAPIError
                let isClientError = error is ClerkClientError
                let isUnexpectedError = !isClerkApiError && !isClientError
                if isUnexpectedError {
                    log.telemetry.error(error, message: "Unexpected error type encountered: \(type(of: error))")
                }
                throw ClerkClientError.wrap(error)
            }
        }

        @Sendable
        func signIn(with phoneNumber: String) async throws {
            do {
                let signIn = try await SignIn.create(strategy: .identifier(phoneNumber))

                // Find the phoneNumberId from all the available first factors for the current sign-in
                guard
                    let phoneNumberFactor = signIn.supportedFirstFactors?.first(where: { factor in
                        factor.strategy == "phone_code" && factor.safeIdentifier == signIn.identifier
                    }),
                    let phoneNumberId = phoneNumberFactor.phoneNumberId
                else {
                    log.telemetry.assertionFailure("Failed to find phone number factor for sign-in with phone: \(phoneNumber)")
                    return
                }

                // Send the OTP code to the user.
                try await signIn.prepareFirstFactor(strategy: .phoneCode(phoneNumberId: phoneNumberId))
            } catch {
                guard let clerkError = error as? ClerkAPIError else {
                    log.telemetry.error(error, message: "Received unexpected error type during phone sign-in: \(type(of: error))")
                    throw ClerkClientError.wrap(error)
                }

                switch clerkError.errorCode {
                case .formIdentifierNotFound:
                    do {
                        let signUp = try await SignUp.create(strategy: .standard(phoneNumber: phoneNumber))

                        // Start the verification - a SMS message will be sent to the
                        // number with a one-time code.
                        try await signUp.prepareVerification(strategy: .phoneCode)
                    } catch {
                        log.telemetry.error(clerkError, message: "Failed to create sign-up after phone number not found")
                        throw ClerkClientError.formIdentifierNotFound
                    }

                case .notAllowedAccess:
                    log.telemetry.error(clerkError, message: "Access not allowed for phone number: \(phoneNumber)")
                    throw ClerkClientError.notAllowedAccess

                default:
                    let errorCode = clerkError.errorCode?.rawValue ?? "unknown_error_code"
                    log.telemetry.error(clerkError, message: "Unhandled Clerk API error during phone sign-in: \(errorCode)")
                    throw ClerkClientError.apiError(clerkError)
                }
            }
        }

        @Sendable
        func signInWithAppleAndGetAuthResult() async -> AuthenticationResult {
            do {
                let appleCredential = try await SignInWithAppleHelper.getAppleIdCredential(requestedScopes: [.email, .fullName])
                guard let idTokenData = appleCredential.identityToken,
                      let idTokenString = String(data: idTokenData, encoding: .utf8)
                else {
                    return .failure(ClerkClientError.unexpectedError(
                        NSError(domain: "Clerk", code: 1001, userInfo: [NSLocalizedDescriptionKey: "Failed to get Apple ID credential"])
                    ))
                }

                let signIn = try await SignIn.create(strategy: .idToken(provider: .apple, idToken: idTokenString))
                try await signIn.authenticateWithIdToken()

                try await waitUntilClerkReady()
                guard let user = await Clerk.shared.user else {
                    return .failure(ClerkClientError.userMissingAfterAuthentication)
                }

                let isFirstTimeUser = if FeatureFlag.general.enhancedAuthenticationFlow {
                    isFirstTimeUser(user)
                } else {
                    false
                }

                return .success(AuthenticationInfo(
                    user: user,
                    isFirstTimeUser: isFirstTimeUser
                ))
            } catch {
                return .failure(ClerkClientError.wrap(error))
            }
        }

        @Sendable
        func startPhoneNumberValidation(with phone: String) async throws -> PhoneNumberValidationStatus {
            guard let user = await Clerk.shared.user else { throw ClerkClientError.userMissingForPhoneValidation }

            do {
                let phoneNumberKit = PhoneNumberKit.PhoneNumberUtility()
                let e164PhoneNumber = try phoneNumberKit.format(phoneNumberKit.parse(phone), toType: .e164)
                // If we have an existing phone number in Clerk, we are unable to verify the one-time code sent
                // so we shouldn't start the verificaton pocess
                var phoneNumber: ClerkPhoneNumber
                if let existingNumber = user.phoneNumbers.first(where: { $0.phoneNumber == e164PhoneNumber }) {
                    // Return without starting the validation process
                    if existingNumber.verification?.status == .verified {
                        return .validated
                    }
                    // If the existing number is unverified, we can still attempt the verification process
                    phoneNumber = existingNumber
                } else {
                    // Create a new number otherwise
                    phoneNumber = try await user.createPhoneNumber(phone)
                }
                let phoneNumberResponse = try await phoneNumber.prepareVerification()

                return .oneTimeCodeSent(PhoneNumberEnvelope(raw: phone, e164Formatted: e164PhoneNumber, clerk: phoneNumber))
            } catch {
                log.telemetry.error(error, message: "Failed to start phone number validation for: \(phone)")
                throw ClerkClientError.wrap(error)
            }
        }

        @Sendable
        func validatePhoneCode(_ code: String) async throws -> AuthenticationInfo {
            if let inProgressSignIn = await Clerk.shared.client?.signIn {
                do {
                    let signIn = try await inProgressSignIn.attemptFirstFactor(strategy: .phoneCode(code: code))

                    try await waitUntilClerkReady()
                    guard let user = await Clerk.shared.user else {
                        throw ClerkClientError.userMissingAfterAuthentication
                    }

                    let isFirstTimeUser = isFirstTimeUser(user)
                    return AuthenticationInfo(
                        user: user,
                        isFirstTimeUser: isFirstTimeUser
                    )
                } catch {
                    log.telemetry.error(error, message: "Failed to validate phone code for sign-in, attempting sign-up")
                    /// When we attempt to send an OTP to a phone number, and we get a `form_identifier_not_found` error from trying to `signIn` (i.e. they don't have an account on clerk), Clerk doesn't clean up and remove the `Clerk.shared.client?.signIn`
                    /// so if that signIn attempt fails, try signing up with that phone number instead
                    if let inProgressSignUp = await Clerk.shared.client?.signUp {
                        do {
                            let signUp = try await inProgressSignUp.attemptVerification(strategy: .phoneCode(code: code))

                            try await waitUntilClerkReady()
                            guard let user = await Clerk.shared.user else {
                                throw ClerkClientError.userMissingAfterAuthentication
                            }

                            let isFirstTimeUser = isFirstTimeUser(user)
                            return AuthenticationInfo(
                                user: user,
                                isFirstTimeUser: isFirstTimeUser
                            )
                        } catch {
                            log.telemetry.error(error, message: "Failed to validate phone code for sign-up")
                            throw ClerkClientError.wrap(error)
                        }
                    } else {
                        log.telemetry.assertionFailure("No valid sign-up session found after sign-in failed")
                        throw ClerkClientError.noValidSessionToVerify
                    }
                }
            } else if let inProgressSignUp = await Clerk.shared.client?.signUp {
                do {
                    let signUp = try await inProgressSignUp.attemptVerification(strategy: .phoneCode(code: code))

                    try await waitUntilClerkReady()
                    guard let user = await Clerk.shared.user else {
                        throw ClerkClientError.userMissingAfterAuthentication
                    }

                    let isFirstTimeUser = isFirstTimeUser(user)
                    return AuthenticationInfo(
                        user: user,
                        isFirstTimeUser: isFirstTimeUser
                    )
                } catch {
                    log.telemetry.error(error, message: "Failed to validate phone code for sign-up")
                    throw ClerkClientError.wrap(error)
                }
            } else {
                log.telemetry.assertionFailure("No existing sign in or sign up in progress when attempting to validate phone code")
                assertionFailure("no existing sign in or sign up in progress")
                throw ClerkClientError.noValidSessionToVerify
            }
        }

        @Sendable
        func deletePhoneNumber(with phone: String) async throws {
            guard let user = await Clerk.shared.user else { throw ClerkClientError.userMissingForPhoneValidation }

            do {
                let phoneNumberKit = PhoneNumberKit.PhoneNumberUtility()
                let e164PhoneNumber = try phoneNumberKit.format(phoneNumberKit.parse(phone), toType: .e164)
                guard let existingNumber = user.phoneNumbers.first(where: { $0.phoneNumber == e164PhoneNumber }) else {
                    throw ClerkClientError.phoneNumberNotFoundForDeletion
                }
                try await existingNumber.delete()
            } catch {
                log.telemetry.error(error, message: "Failed to delete phone number: \(phone)")
                throw ClerkClientError.wrap(error)
            }
        }

        @Sendable
        func waitUntilClerkReady() async throws {
            try await readinessCoordinator.waitUntilReady()
        }

        return Self(
            configure: {
                await Clerk.shared.configure(publishableKey: publishableKey, settings: .init(debugMode: true))
                try await Clerk.shared.load()
            },
            isLoggedIn: {
                try await waitUntilClerkReady()
                return await Clerk.shared.session != nil
            },
            jwt: { skipCache in
                try await waitUntilClerkReady()

                guard let session = await Clerk.shared.session else {
                    if userDefaults.shouldHaveClerkSesssion {
                        // User had a valid session (ie, was logged in)
                        // but the session became nil without first signing out
                        // Purely used to disambiguate logs
                        throw ClerkClientError.unexpectedInvalidSession
                    } else {
                        throw ClerkClientError.invalidSession
                    }
                }

                // Write to user defaults to catch session becoming unexpectedly nil above
                if !userDefaults.shouldHaveClerkSesssion {
                    await userDefaults.setShouldHaveClerkSesssion(true)
                }

                do {
                    let tokenResource = try await session.getToken(.init(skipCache: skipCache))
                    guard let tokenResource else { throw ClerkClientError.noToken }
                    return tokenResource.jwt
                } catch {
                    if error is ClerkAPIError {
                        log.telemetry.error(error, message: "Failed to get token from session")
                    } else if !(error is ClerkClientError) {
                        log.telemetry.error(error, message: "Unexpected error getting token: \(type(of: error))")
                    }
                    throw ClerkClientError.wrap(error)
                }
            },
            user: {
                try await waitUntilClerkReady()
                guard let session = await Clerk.shared.session else { throw ClerkClientError.invalidToken }
                return session.user
            },
            userWithAuthInfo: {
                try await waitUntilClerkReady()
                guard await Clerk.shared.session != nil else { throw ClerkClientError.invalidToken }

                guard let user = await Clerk.shared.user else { return nil }

                let isFirstTimeUser = isFirstTimeUser(user)

                return AuthenticationInfo(
                    user: user,
                    isFirstTimeUser: isFirstTimeUser
                )
            },
            waitUntilReady: {
                try await waitUntilClerkReady()
            },
            isReady: {
                await Clerk.shared.isLoaded
            },
            signInWithPhone: { phone in
                try await signIn(with: phone)
            },
            validatePhoneCode: { code in
                try await validatePhoneCode(code)
            },
            addPhoneNumberToAccount: { phone in
                try await startPhoneNumberValidation(with: phone)
            },
            verifyAddPhoneNumberToAccount: { code, phoneNumber in
                /// https://clerk.com/docs/custom-flows/add-phone
                try await phoneNumber.attemptVerification(code: code)
            },
            deletePhoneNumber: { phone in
                try await deletePhoneNumber(with: phone)
            },
            signInWithApple: {
                await signInWithAppleAndGetAuthResult()
            },
            signInWithGoogle: {
                do {
                    let authAttempt = try await SignIn.authenticateWithRedirect(strategy: .oauth(provider: .google))

                    try await waitUntilClerkReady()
                    guard let user = await Clerk.shared.user else {
                        return .failure(ClerkClientError.userMissingAfterAuthentication)
                    }

                    let isFirstTimeUser = if FeatureFlag.general.enhancedAuthenticationFlow {
                        authAttempt.isFirstTimeUser || isFirstTimeUser(user)
                    } else {
                        false
                    }

                    return .success(AuthenticationInfo(
                        user: user,
                        isFirstTimeUser: isFirstTimeUser
                    ))
                } catch {
                    return .failure(ClerkClientError.wrap(error))
                }
            },
            signInWithMicrosoft: {
                do {
                    let authAttempt = try await SignIn.authenticateWithRedirect(strategy: .oauth(provider: .microsoft))

                    try await waitUntilClerkReady()
                    guard let user = await Clerk.shared.user else {
                        return .failure(ClerkClientError.userMissingAfterAuthentication)
                    }

                    let isFirstTimeUser = if FeatureFlag.general.enhancedAuthenticationFlow {
                        authAttempt.isFirstTimeUser || isFirstTimeUser(user)
                    } else {
                        false
                    }

                    return .success(AuthenticationInfo(
                        user: user,
                        isFirstTimeUser: isFirstTimeUser
                    ))
                } catch {
                    return .failure(ClerkClientError.wrap(error))
                }
            },
            signInWithDiscord: {
                do {
                    let authAttempt = try await SignIn.authenticateWithRedirect(strategy: .oauth(provider: .discord))

                    try await waitUntilClerkReady()
                    guard let user = await Clerk.shared.user else {
                        return .failure(ClerkClientError.userMissingAfterAuthentication)
                    }

                    let isFirstTimeUser = if FeatureFlag.general.enhancedAuthenticationFlow {
                        authAttempt.isFirstTimeUser || isFirstTimeUser(user)
                    } else {
                        false
                    }

                    return .success(AuthenticationInfo(
                        user: user,
                        isFirstTimeUser: isFirstTimeUser
                    ))
                } catch {
                    return .failure(ClerkClientError.wrap(error))
                }
            },
            signOut: {
                try await Clerk.shared.signOut()
                await userDefaults.setShouldHaveClerkSesssion(false)
            }
        )
    }()
}

extension ClerkClient: TestDependencyKey {
    public static let testValue = Self(
        configure: {},
        isLoggedIn: { false },
        jwt: { _ in "test_jwt_token" },
        user: { nil },
        userWithAuthInfo: { nil },
        waitUntilReady: {},
        isReady: { true },
        signInWithPhone: { _ in },
        validatePhoneCode: { _ in
            throw ClerkClientError.noValidSessionToVerify
        },
        addPhoneNumberToAccount: { _ in .validated },
        verifyAddPhoneNumberToAccount: { _, _ in },
        deletePhoneNumber: { _ in },
        signInWithApple: {
            .failure(ClerkClientError.noValidSessionToVerify)
        },
        signInWithGoogle: {
            .failure(ClerkClientError.noValidSessionToVerify)
        },
        signInWithMicrosoft: {
            .failure(ClerkClientError.noValidSessionToVerify)
        },
        signInWithDiscord: {
            .failure(ClerkClientError.noValidSessionToVerify)
        },
        signOut: {}
    )
}

public extension ClerkClient {
    static let noop = Self()

    static let previewValue = Self.noop
}

// MARK: - ClerkReadinessCoordinator

final actor ClerkReadinessCoordinator {
    private var waitingTask: Task<Void, Error>?

    func waitUntilReady() async throws {
        if let existingTask = waitingTask {
            do {
                return try await existingTask.value
            } catch is CancellationError {
                waitingTask = nil
            } catch {
                throw error
            }
        }

        let task = Task { () throws in
            // Use exponential backoff instead of fixed polling
            let initialDelay: TimeInterval = 0.025 // Start with 25ms
            let maxDelay: TimeInterval = 0.5 // Cap at 500ms
            let timeout: TimeInterval = 10.0 // 10 second timeout
            let startTime = Date()
            var currentDelay = initialDelay

            while true {
                // Check for task cancellation
                try Task.checkCancellation()

                if await Clerk.shared.isLoaded {
                    return
                }

                // Check for timeout
                if Date().timeIntervalSince(startTime) > timeout {
                    throw ClerkClientError.clientNotLoaded
                }

                // Wait with exponential backoff
                try await Task.sleep(for: .seconds(currentDelay))
                currentDelay = min(currentDelay * 1.5, maxDelay) // Exponential backoff with cap
            }
        }

        waitingTask = task
        defer { waitingTask = nil }

        return try await task.value
    }
}

// MARK: - TransferFlowResult

private extension TransferFlowResult {
    var isFirstTimeUser: Bool {
        switch self {
        case .signIn: false
        case .signUp: true
        }
    }
}
