import BackendEnvironmentClient
import ComposableArchitecture
import Foundation
import os.log
import Shake
import Utilities

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

@DependencyClient
public struct RageshakeClient {
    public var setIsEnabled: (_ isEnabled: Bool) -> Void

    public var setUser: (User) -> Void

    public var registerNetworkingInterception: (URLSessionConfiguration) -> Void

    public var show: () -> Void
}

public extension RageshakeClient {
    enum User: Equatable {
        case authenticated(AuthenticatedUser)
        case anonymous

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

    struct AuthenticatedUser: Equatable {
        var id: String
        var email: String
        var displayName: String?
        var handle: String

        public init(id: String, email: String, displayName: String? = nil, handle: String) {
            self.id = id
            self.email = email
            self.displayName = displayName
            self.handle = handle
        }

        fileprivate var shakeMetadata: [String: any Hashable] {
            var metadata = ["email": email, "handle": handle]
            if let displayName {
                metadata["displayName"] = displayName
            }
            return metadata
        }
    }
}

extension RageshakeClient: DependencyKey {
    public static let liveValue: RageshakeClient = {
        @Dependency(BackendEnvironmentClient.self) var backendEnvironmentClient

        guard backendEnvironmentClient.isStaffBuild() else {
            return Self(
                setIsEnabled: { _ in },
                setUser: { _ in },
                registerNetworkingInterception: { _ in },
                show: {}
            )
        }

        return Self(
            setIsEnabled: { isEnabled in
                guard isEnabled, !Shake.isStarted else { return }

                guard let info = Bundle.main.infoDictionary,
                      let apiKey = info["SHAKE_API_KEY"] as? String,
                      !apiKey.isEmpty else {
                    return assertionFailure("SHAKE_API_KEY not set in Info.plist")
                }

                /// We already use shake to show the `DebugMenu`, so disabling it from also showing the shake-to-report-bug view
                Shake.configuration.isInvokedByShakeDeviceEvent = false

                Shake.configuration.isInvokedByScreenshot = true
                Shake.start(apiKey: apiKey)
            },
            setUser: { user in
                performAfterStart { [user] in
                    switch user {
                    case .anonymous:
                        Shake.unregisterUser()

                    case .authenticated(let user):
                        Shake.registerUser(userId: user.id)
                        Shake.updateUserMetadata(user.shakeMetadata)
                    }
                }
            },
            registerNetworkingInterception: { session in
                performAfterStart { [session] in
                    Shake.registerSessionConfiguration(session)
                }
            },
            show: {
                guard Shake.isStarted else { return }
                Shake.show()
            }
        )
    }()
}

private func performAfterStart(_ block: @escaping () -> Void, expireAfter: TimeInterval = 10.0) {
    @Dependency(\.continuousClock) var continuousClock
    @Dependency(\.date) var date

    Task { @MainActor in
        guard Shake.isStarted == false else { block(); return }

        log.debug("Attemping to configure Shake but Shake hasn't started. Waiting for start...")
        let startTime = date.now

        // Check every second to see if we've started yet.
        for await _ in continuousClock.timer(interval: .seconds(1)) {
            guard Shake.isStarted == false else {
                log.debug("Shake has started. Proceeding with Shake configuration.")
                block(); return
            }

            let elapsedTime = startTime.distance(to: date.now)
            guard elapsedTime < expireAfter else {
                log.debug("Shake did not start within \(elapsedTime) seconds. Abandoning configuration attempt.")
                return
            }
        }
    }
}

extension RageshakeClient: TestDependencyKey {
    public static let previewValue = Self.noop

    public static let testValue = Self(
        setIsEnabled: { _ in },
        setUser: { _ in },
        registerNetworkingInterception: { _ in },
        show: {}
    )
}

public extension RageshakeClient {
    static var noop: RageshakeClient {
        return Self(
            setIsEnabled: unimplemented("\(Self.self).setIsEnabled", placeholder: { _ in }),
            setUser: unimplemented("\(Self.self).setUser", placeholder: { _ in }),
            registerNetworkingInterception: unimplemented("\(Self.self).registerNetworkingInterception", placeholder: { _ in }),
            show: unimplemented("\(Self.self).show", placeholder: {})
        )
    }
}
