import Combine
import ComposableArchitecture
import FirebaseAnalytics
import FirebaseCore
import FirebaseCrashlytics
import FirebasePerformance
import Foundation
import InstanceIDClient
import Utilities

@DependencyClient
public struct FirebaseClient {
    public var configure: @Sendable () -> Void
    public var isPerformanceReady: @Sendable () -> Bool = { false }
    public var setUserID: @Sendable ((userID: String, email: String)?) -> Void
    public var setUser: @Sendable (User) -> Void

    public var logMessage: (String) -> Void
    public var record: (Error, _ message: String?) -> Void

    /// Cancelling the cancellable returned from here ends the trace
    public var trace: (TracePerformanceSpan) -> AnyCancellable = { _ in AnyCancellable {} }
}

public extension FirebaseClient {
    enum User: Equatable {
        case authenticated(userId: String, email: String)
        case anonymous

        public var userId: String? {
            switch self {
            case .authenticated(userId: let id, email: _): id
            case .anonymous: nil
            }
        }
    }
}

extension FirebaseClient: DependencyKey {
    public static let liveValue = {
        @Dependency(\.instanceIdClient) var instanceIdClient

        return Self(
            configure: {
                FirebaseApp.configure()
                FirebaseConfiguration.shared.setLoggerLevel(.notice)
            },
            isPerformanceReady: {
                FirebaseApp.app() != nil
            },
            setUserID: {
                if let (userID, email) = $0 {
                    Crashlytics.crashlytics().setUserID(userID)
                    Analytics.initiateOnDeviceConversionMeasurement(emailAddress: email)
                } else {
                    Crashlytics.crashlytics().setUserID(.none)
                }
            },
            setUser: { user in
                switch user {
                case .authenticated(userId: let userId, email: let email):
                    Crashlytics.crashlytics().setUserID(userId)
                    Analytics.initiateOnDeviceConversionMeasurement(emailAddress: email)

                case .anonymous:
                    // Use device-stable ID for logged-out users
                    let deviceStableId = instanceIdClient.instanceId()
                    Crashlytics.crashlytics().setUserID(deviceStableId)
                }
            },
            logMessage: {
                Crashlytics.crashlytics().log($0)
            },
            record: { error, message in
                #if DEBUG
                    let isDEBUG = true
                #else
                    let isDEBUG = false
                #endif

                Crashlytics.crashlytics().record(error: error as NSError, userInfo: [
                    "is_DEBUG": isDEBUG,
                    "bundle": Bundle.main.bundleIdentifier ?? "nil",
                    "message": message as Any,
                    "localized_description": error.localizedDescription,
                ])
            },
            trace: { trace in
                let trace = FirebasePerformance.Performance.startTrace(name: trace.rawValue)

                return AnyCancellable {
                    trace?.stop()
                }
            }
        )
    }()
}

extension FirebaseClient: TelemetryClientProtocol {
    public func record(error: any Error, message: String?) {
        record(error, message: message)
    }

    public func record(message: String) {
        logMessage(message)
    }

    public func trace(_ named: String) -> AnyCancellable {
        guard let trace = TracePerformanceSpan(rawValue: named) else {
            assertionFailure()
            return AnyCancellable {}
        }

        return self.trace(trace)
    }
}

public enum TracePerformanceSpan: String {
    case timeToInteractive
    case startupOrchestration

    case sdksInitialization
    case sdk_Braze_Init
    case sdk_RevenueCat_Init
    case sdk_Statsig_Init
    case sdk_Statsig_NetworkFetch

    case clerkLoggedInCheck
    case fetchMeOnAppOpen

    // Phase completion spans
    case essential_phase_completion
    case critical_phase_completion
    case important_phase_completion
    case optional_phase_completion
    case app_start_completed

    // Additional SDK spans
    case sdk_Firebase_init
    case sdk_Clerk_init
    case sdk_Adamantium_init
    case sdk_ShareAsset_init
    case sdk_Attribution_init
    case sdk_Rageshake_init

    // Dependency spans
    case sdk_dependency_wait_time
}

public extension TelemetryClientProtocol {
    func withSpan<T>(
        _ traceName: TracePerformanceSpan,
        _ block: () async throws -> T
    ) async rethrows -> T {
        let trace = self.trace(traceName.rawValue)
        defer {
            // this marks the end of the measurement to Firebase
            trace.cancel()
        }

        return try await block()
    }
}

extension FirebaseClient: TestDependencyKey {
    public static let testValue = Self.noop
}

public extension FirebaseClient {
    static let noop = Self(
        configure: {},
        isPerformanceReady: { true },
        setUserID: { _ in },
        setUser: { _ in },
        logMessage: { _ in },
        record: { _, _ in },
        trace: { _ in AnyCancellable {} }
    )

    static let performanceIsReady = Self.noop

    static let performanceNotReady = Self(
        configure: {},
        isPerformanceReady: { false },
        setUserID: { _ in },
        setUser: { _ in },
        logMessage: { _ in },
        record: { _, _ in },
        trace: { _ in AnyCancellable {} }
    )
}

public extension DependencyValues {
    var firebaseClient: FirebaseClient {
        get { self[FirebaseClient.self] }
        set { self[FirebaseClient.self] = newValue }
    }
}
