import AdamantiumClient
import AttributionAndDeeplinkingClient
import BrazeClient
import ClerkClient
import DebugFeatureClient
import FirebaseClient
import Foundation
import PaywallClient
import ShareAssetClient
import StatsigClient
import Utilities

// MARK: - SDKManagerProtocol

protocol SDKManagerProtocol {
    func initialize(_ sdk: SDKIdentifier, onMainThread: Bool) async throws
}

// MARK: - SDKManagerError

enum SDKManagerError: Error {
    case initializationFailed(
        sdk: SDKIdentifier,
        underlying: Error
    )
}

private let log = Logger(category: "DependencySDKManager")

// MARK: - DependencySDKManager

final class DependencySDKManager: SDKManagerProtocol {
    private let firebaseClient: FirebaseClient
    private let brazeClient: BrazeClient
    private let clerkClient: ClerkClient
    private let paywallClient: PaywallClient
    private let statsigClient: StatsigClient
    private let adamantiumClient: AdamantiumClient
    private let shareAssetClient: ShareAssetClient
    private let attributionClient: AttributionAndDeeplinkingClient
    private let debugFeatureClient: DebugFeatureClient

    public init(
        firebaseClient: FirebaseClient,
        brazeClient: BrazeClient,
        clerkClient: ClerkClient,
        paywallClient: PaywallClient,
        statsigClient: StatsigClient,
        adamantiumClient: AdamantiumClient,
        shareAssetClient: ShareAssetClient,
        attributionClient: AttributionAndDeeplinkingClient,
        debugFeatureClient: DebugFeatureClient
    ) {
        self.firebaseClient = firebaseClient
        self.brazeClient = brazeClient
        self.clerkClient = clerkClient
        self.paywallClient = paywallClient
        self.statsigClient = statsigClient
        self.adamantiumClient = adamantiumClient
        self.shareAssetClient = shareAssetClient
        self.attributionClient = attributionClient
        self.debugFeatureClient = debugFeatureClient
    }

    public func initialize(
        _ sdk: SDKIdentifier,
        onMainThread: Bool
    ) async throws {
        let shouldUseMainThread = onMainThread || sdk.requiresMainThread

        if shouldUseMainThread {
            try await initializeOnMainThread(sdk)
        } else {
            try await initializeOffMainThread(sdk)
        }
    }

    /*
        Note - the duplication below is on purpose because of Swift's actor isolation context.

        The TLDR is we can't simply call the other async method not marked with @MainActor inside
        the one that is marked with it - if we want something on the @MainActor, we need to call it
        explicitly inside the method @MainActor is marking.
     */
    @MainActor
    private func initializeOnMainThread(
        _ sdk: SDKIdentifier
    ) async throws {
        switch sdk {
        case .firebase:
            firebaseClient.configure()

        case .braze:
            try brazeClient.configure()

        case .clerk:
            try await clerkClient.configure()
            try await clerkClient.waitUntilReady()

        case .revenueCat:
            paywallClient.configure()

        case .statsig:
            try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
                let lock = NSLock()
                var didResume = false

                func resumeOnce() {
                    lock.lock()
                    let shouldResume = !didResume
                    if shouldResume {
                        didResume = true
                    }
                    lock.unlock()

                    guard shouldResume else { return }
                    continuation.resume()
                }

                statsigClient.startStatsig() { error in
                    guard let error else { return }
                    log.telemetry.error(error, message: "Statsig initialization failed")
                }

                Task {
                    // We want to continue as soon as we receive an initializing state from StatsigClient - this indicates
                    // that we have called the underlying Statsig SDK initialization method

                    // We don't want to wait for the async initialization to complete - cached Statsig values are ready
                    // as soon as the init method is called
                    for await state in statsigClient.state() {
                        switch state {
                        case .initializing:
                            resumeOnce()

                        case .initialized,
                             .initializedWithUser,
                             .uninitialized:
                            break
                        }
                    }
                }
            }

        case .adamantium:
            await adamantiumClient.configure()

        case .shareAsset:
            await shareAssetClient.configure()

        case .attribution:
            attributionClient.configure()

        case .rageshake:
            let isAllowed = debugFeatureClient.isDebugMenuAvailable()
            debugFeatureClient.setRageshakeEnabled(isAllowed)
        }
    }

    private func initializeOffMainThread(
        _ sdk: SDKIdentifier
    ) async throws {
        switch sdk {
        case .firebase:
            firebaseClient.configure()

        case .braze:
            try await brazeClient.configure()

        case .clerk:
            try await clerkClient.configure()
            try await clerkClient.waitUntilReady()

        case .revenueCat:
            paywallClient.configure()

        case .statsig:
            try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
                let lock = NSLock()
                var didResume = false

                func resumeOnce() {
                    lock.lock()
                    let shouldResume = !didResume
                    if shouldResume {
                        didResume = true
                    }
                    lock.unlock()
                    
                    guard shouldResume else { return }
                    continuation.resume()
                }

                statsigClient.startStatsig() { error in
                    guard let error else { return }
                    log.telemetry.error(error, message: "Statsig initialization failed")
                }

                Task {
                    // We want to continue as soon as we receive an initializing state from StatsigClient - this indicates
                    // that we have called the underlying Statsig SDK initialization method

                    // We don't want to wait for the async initialization to complete - cached Statsig values are ready
                    // as soon as the init method is called
                    for await state in statsigClient.state() {
                        switch state {
                        case .initializing:
                            resumeOnce()

                        case .initialized,
                             .initializedWithUser,
                             .uninitialized:
                            break
                        }
                    }
                }
            }

        case .adamantium:
            await adamantiumClient.configure()

        case .shareAsset:
            await shareAssetClient.configure()

        case .attribution:
            attributionClient.configure()

        case .rageshake:
            let isAllowed = debugFeatureClient.isDebugMenuAvailable()
            debugFeatureClient.setRageshakeEnabled(isAllowed)
        }
    }
}

#if DEBUG

    // MARK: - Mock

    extension DependencySDKManager {
        final class Mock: SDKManagerProtocol {
            var initializeCalls: [SDKIdentifier] = []
            var shouldFailSDKs: Set<SDKIdentifier> = []
            var shouldTimeoutSDKs: Set<SDKIdentifier> = []
            var initializeDelay: TimeInterval = 0

            func initialize(
                _ sdk: SDKIdentifier,
                onMainThread _: Bool
            ) async throws {
                initializeCalls.append(sdk)

                if initializeDelay > 0 {
                    try await Task.sleep(for: .seconds(initializeDelay))
                }

                if shouldTimeoutSDKs.contains(sdk) {
                    try await Task.sleep(for: .seconds(10)) // Long delay to trigger timeout
                }

                if shouldFailSDKs.contains(sdk) {
                    throw SDKManagerError.initializationFailed(
                        sdk: sdk,
                        underlying: NSError(
                            domain: "DependencySDKManager.Mock",
                            code: -1
                        )
                    )
                }
            }
        }
    }
#endif

