import APIClient
import BackendEnvironmentClient
import BrazeClient
import ComposableArchitecture
import DebugFeatureClient
import DebugUtilities
import FirebaseAnalytics
import Foundation
import InstanceIDClient
import Segment
import SwiftUI
import Utilities

public extension AnalyticsClient {
    /// Use this helper function that sends events to the default places. use @DependencyClient's `track` function when you want to send the event to `.all` destinations (like Braze)
    func track(_ event: AnalyticsEvent) {
        self.track(event: event, destination: .standard)
    }

    /// Use this helper function that sends events to the default places. use @DependencyClient's `trackV2` function when you want to send the event to `.all` destinations (like Braze)
    func trackV2(event: Event, source: String?) {
        self.trackV2(event: event, source: source, destination: .standard)
    }
}

@DependencyClient
public struct AnalyticsClient {
    public var track: @Sendable (_ event: AnalyticsEvent, _ destination: Set<Destination>) -> Void
    public var trackV2: @Sendable (_ event: Event, _ source: String?, _ destination: Set<Destination>) -> Void

    /// Identifies the current user, allowing us to avoid having to pass `userId` around
    /// Uses `Me` as a good faith requirement
    /// Call this every time a user logs in, changes their information, or signs up
    /// Calls Segment under the hood https://segment.com/docs/connections/spec/identify/
    /// Note that we fire off an "identify" event for this call. This is currently not a
    /// whitelisted event for us, but we may handle it down the line
    /// We can get around this if we look into hoisting our current user session `Me`,
    /// as it is currently passed around in memory.
    public var identify: @Sendable (_ me: User) -> Void

    public var anonymousId: @Sendable () -> String = { "" }

    /// Resets the current user
    /// Call this every time we log out
    /// Note that we fire off an "reset" event for this call. This is currently not a
    /// whitelisted event for us, but we may handle it down the line
    public var resetIdentity: @Sendable () -> Void
    /// Calls `trackV2` under the hood

    public var setScreen: @Sendable (_ screen: Screen?) async -> Void
}

public extension AnalyticsClient {
    enum User: Equatable, CustomStringConvertible {
        case authenticated(me: Me)
        case anonymous

        public var me: Me? {
            switch self {
            case .authenticated(me: let me): me
            case .anonymous: nil
            }
        }

        public var description: String {
            switch self {
            case .authenticated(let me):
                return "User: \(me.user.username) (ID \(me.user.id))"
            case .anonymous:
                return "User: anonymous"
            }
        }
    }
}

public extension AnalyticsClient {
    enum Destination: CaseIterable, Hashable {
        case segment
        case firebase
        case braze
    }
}

public extension Set where Element == AnalyticsClient.Destination {
    static let standard = Self([.segment])
    static let all = Self(AnalyticsClient.Destination.allCases)
}

extension AnalyticsClient: DependencyKey {
    private static let flushCount: Int = 50
    private static let flushInterval: Double = 30

    public static let liveValue: Self = {
        @Dependency(BackendEnvironmentClient.self) var backendEnvironment
        @Dependency(DebugFeatureClient.self) var debugFeatureClient
        let environmentConfiguration = backendEnvironment.configuration()

        guard let info = Bundle.main.infoDictionary,
              let writeKey = info["SEGMENT_WRITE_KEY"] as? String,
              !writeKey.isEmpty else {
            fatalError("SEGMENT_WRITE_KEY is not defined in Info.plist")
        }

        #if DEBUG
            // No sense making a dev schema just for analytics
            let proxyEndpoint = "bkz9921ndc.execute-api.us-east-2.amazonaws.com/dev-testing/metrics/v1"
        #else
            let proxyEndpoint = environmentConfiguration.segmentProxyEndpoint
            guard proxyEndpoint.isEmpty == false else {
                fatalError("SEGMENT_PROXY_ENDPOINT is not defined for this configuration")
            }
        #endif

        @Dependency(\.instanceIdClient) var instanceIdClient

        // Bridge Segment's anonymous ID system to use our instance ID instead.
        // This ensures consistent user identification across analytics platforms
        // by using the same instance ID that we use elsewhere in the app.
        let instanceIdOverridingGenerator: AnonymousIdGenerator = AnonymousIdBridgeToInstanceId(
            instanceId: instanceIdClient.instanceId()
        )

        let configuration = Configuration(writeKey: writeKey)
            .apiHost(proxyEndpoint)
            .cdnHost(proxyEndpoint) // Pulls in a settings.json for configuration
            .flushAt(flushCount)
            .flushInterval(flushInterval)
            .anonymousIdGenerator(instanceIdOverridingGenerator)

        let analytics = Analytics(configuration: configuration)

        @Dependency(BrazeClient.self) var brazeClient
        let context: Context = Context()

        @Sendable
        @discardableResult
        nonisolated func prepareAnalyticEventWithContextV2(event: Event, source: String?) async -> Event {
            // Mutable capture
            var eventWithContext = event
            eventWithContext.userId = analytics.userId
            eventWithContext.anonymousId = analytics.anonymousId
            let currentScreen = await context.currentScreen
            eventWithContext.screenName = currentScreen?.screenName
            eventWithContext.screenElementId = currentScreen?.screenElementId
            eventWithContext.source = source

            /// Must be declared in this concurrency context due to this function being `Sendable nonisolated`
            @Shared(.inMemory(.analyticsSessionId)) var sessionId: String?
            eventWithContext.sessionId = sessionId

            return eventWithContext
        }

        return Self(
            track: { event, destination in
                do {
                    let payload = try event.getSegmentPayload(
                        anonymousId: analytics.anonymousId,
                        userId: analytics.userId
                    )

                    // For on device debug only
                    let debugEvent = DebugAnalyticsEvent(name: payload.eventName, properties: payload.properties)
                    debugFeatureClient.postAnalyticsEvent(debugEvent)

                    if destination.contains(.segment) {
                        analytics.track(name: payload.eventName, properties: payload.properties)
                    }

                    if destination.contains(.firebase) {
                        /// Skipping sending properties for V1. hard to pull the values out from the `event.getSegmentPayload`
                        FirebaseAnalytics.Analytics.logEvent(payload.eventName, parameters: [:])
                    }

                    if destination.contains(.braze) {
                        brazeClient.sendEvent(payload.eventName)
                    }
                } catch {
                    log.telemetry.error(error)
                }
            },
            trackV2: { event, source, destination in
                /// Detached since we don't want to cancel if a callee called from with a Task that is cancelled
                Task.detached(priority: .utility) {
                    let eventWithContext = await prepareAnalyticEventWithContextV2(event: event, source: source)

                    // For on device debug only
                    let debugEvent = DebugAnalyticsEvent(name: eventWithContext.source ?? "event source missing", properties: eventWithContext)
                    debugFeatureClient.postAnalyticsEvent(debugEvent)

                    if destination.contains(.segment) {
                        /// Whitelisted names that act as event action categories
                        /// https://us-east-2.console.aws.amazon.com/lambda/home?region=us-east-2#/functions/parse_data_batched?tab=code
                        let whitelistName = "App-Event"
                        analytics.track(name: whitelistName, properties: eventWithContext)
                    }

                    if destination.contains(.firebase) {
                        FirebaseAnalytics.Analytics.logEvent(event.actionName.rawValue, parameters: eventWithContext.asDictionary)
                    }

                    if destination.contains(.braze) {
                        brazeClient.sendEvent(event.actionName.rawValue)
                    }
                }
            },
            identify: { user in
                let userId = switch user {
                case .authenticated(me: let me): me.user.id
                case .anonymous: instanceIdClient.instanceId()
                }

                analytics.identify(userId: userId)
                FirebaseAnalytics.Analytics.setUserID(userId)
            },
            anonymousId: { analytics.anonymousId },
            resetIdentity: {
                analytics.reset()
            },
            setScreen: { screen in
                guard await screen != context.currentScreen else { return }
                await context.setScreen(screen: screen)
                guard let _ = screen else { return }
                let eventWithContext = await prepareAnalyticEventWithContextV2(event: Event.General.newScreen(), source: .none)

                /// Whitelisted names that act as event action categories
                /// https://us-east-2.console.aws.amazon.com/lambda/home?region=us-east-2#/functions/parse_data_batched?tab=code
                let whitelistName = "App-Event"
                analytics.track(name: whitelistName, properties: eventWithContext)
            }
        )
    }()
}

extension AnalyticsClient: TestDependencyKey {
    public static var testValue: AnalyticsClient = Self.noop
    public static var previewValue: AnalyticsClient = Self.noop
}

public extension AnalyticsClient {
    /// Breadcrumbs in console are still useful
    /// Also allows for noops during testing instead of `unimplemented` errors
    static let noop: AnalyticsClient = Self(
        track: { event, _ in
            log.debug("AnalyticsClient(track:) called for event: \(event.eventName)")
        },
        trackV2: { event, source, _ in
            log.debug("AnalyticsClient(trackV2:) called for event: \(event.actionName) and source: \(String(describing: source))")
        },
        identify: { user in
            log.debug("AnalyticsClient(identifyUser:) called for me: \(user)")
        },
        anonymousId: unimplemented(placeholder: ""),
        resetIdentity: {
            log.debug("AnalyticsClient(resetIdentity:) called")
        },
        setScreen: { screen in
            log.debug("AnalyticsClient(setRootScreen:) called for screen: \(String(describing: screen))")
        }
    )
}

public extension DependencyValues {
    var analyticsClient: AnalyticsClient {
        get { self[AnalyticsClient.self] }
        set { self[AnalyticsClient.self] = newValue }
    }
}

// MARK: - AnonymousIdBridgeToInstanceId

/// A bridge class that implements Segment's AnonymousIdGenerator protocol
/// to return our app's instance ID instead of Segment's default anonymous ID.
/// This provides consistent user identification across different analytics platforms
/// and ensures we use the same identifier throughout the app's analytics stack.
private final class AnonymousIdBridgeToInstanceId: AnonymousIdGenerator, Codable {
    private let instanceId: String

    init(
        instanceId: String
    ) {
        self.instanceId = instanceId
    }

    func newAnonymousId() -> String {
        instanceId
    }
}
