import UIKit
import UserNotifications

/// App 桌面图标角标设置能力，便于业务同步未读消息数量并进行测试替换。
@MainActor
protocol ApplicationIconBadgeSetting: AnyObject {
    /// 将桌面图标角标更新为指定非负数量。
    func setBadgeCount(_ count: Int) async
}

/// 使用 UserNotifications 更新 App 桌面图标角标的系统实现。
@MainActor
final class SystemApplicationIconBadgeSetter: ApplicationIconBadgeSetting {
    /// 全局角标设置器。
    static let shared = SystemApplicationIconBadgeSetter()

    private init() {}

    func setBadgeCount(_ count: Int) async {
        do {
            try await UNUserNotificationCenter.current().setBadgeCount(max(count, 0))
        } catch {
            #if DEBUG
            print("Update application icon badge failed: \(error.localizedDescription)")
            #endif
        }
    }
}

/// 极光 SDK 的最小能力边界，便于推送生命周期逻辑使用测试替身。
@MainActor
protocol PushSDKProviding: AnyObject {
    /// 初始化极光长连接与 APNs 代理服务。
    func initialize(launchOptions: [UIApplication.LaunchOptionsKey: Any]?, isProduction: Bool)

    /// 请求系统通知权限并注册极光通知代理。
    func requestAuthorization(delegate: JPUSHRegisterDelegate)

    /// 将 APNs Device Token 注册给极光。
    func registerDeviceToken(_ deviceToken: Data)

    /// 获取极光 Registration ID。
    func fetchRegistrationID(completion: @escaping (Int32, String?) -> Void)
}

/// 跨越极光 `@Sendable` 回调边界的只读闭包容器。
private final class RegistrationIDCompletionBox: @unchecked Sendable {
    let completion: (Int32, String?) -> Void

    init(completion: @escaping (Int32, String?) -> Void) {
        self.completion = completion
    }
}

/// 生产环境极光 SDK 适配器，集中保存 AppKey 和渠道配置。
@MainActor
final class JPushSDKAdapter: PushSDKProviding {
    private static let appKey = "194a633c2a6b79c2f1033c40"
    private static let channel = "App Store"

    /// 初始化极光 SDK；Debug 输出详细日志，Release 只保留告警和错误。
    func initialize(launchOptions: [UIApplication.LaunchOptionsKey: Any]?, isProduction: Bool) {
        JPUSHService.setup(
            withOption: launchOptions,
            appKey: Self.appKey,
            channel: Self.channel,
            apsForProduction: isProduction
        )
        #if DEBUG
        JPUSHService.setDebugMode()
        #else
        JPUSHService.setLogOFF()
        #endif
    }

    /// 请求 alert、badge、sound 三类通知权限。
    func requestAuthorization(delegate: JPUSHRegisterDelegate) {
        let entity = JPUSHRegisterEntity()
        entity.types = 1 | 2 | 4
        JPUSHService.register(forRemoteNotificationConfig: entity, delegate: delegate)
    }

    /// 将 APNs Device Token 交给极光。
    func registerDeviceToken(_ deviceToken: Data) {
        JPUSHService.registerDeviceToken(deviceToken)
    }

    /// 异步获取当前设备 Registration ID。
    func fetchRegistrationID(completion: @escaping (Int32, String?) -> Void) {
        let box = RegistrationIDCompletionBox(completion: completion)
        JPUSHService.registrationIDCompletionHandler { resultCode, registrationID in
            box.completion(resultCode, registrationID)
        }
    }
}

/// 推送生命周期管理器，负责隐私门禁、权限、Registration ID 绑定及通知点击分发。
@MainActor
final class PushNotificationManager: NSObject {
    /// 全局推送管理器。
    static let shared = PushNotificationManager()

    /// 推送相关的本地持久化键。
    private enum Key {
        static let registrationID = "key_jpush_registration_id"
        static let uploadedRegistrationIDSuffix = "jpush_uploaded_registration_id"
    }

    private let sdk: any PushSDKProviding
    private let api: any PushRegistrationServing
    private let appStore: AppStore
    private let defaults: UserDefaults
    private let router: any PushRouting
    private let applicationIconBadgeSetter: any ApplicationIconBadgeSetting

    private var isInitialized = false
    private var didRequestAuthorization = false
    private var uploadTask: Task<Void, Never>?
    private var queuedForcedUpload = false
    private var isFetchingRegistrationID = false
    private var queuedForcedFetch = false

    /// 创建推送管理器；生产环境使用共享依赖，测试可注入替身。
    init(
        sdk: (any PushSDKProviding)? = nil,
        api: (any PushRegistrationServing)? = nil,
        appStore: AppStore = .shared,
        defaults: UserDefaults = .standard,
        router: (any PushRouting)? = nil,
        applicationIconBadgeSetter: (any ApplicationIconBadgeSetting)? = nil
    ) {
        self.sdk = sdk ?? JPushSDKAdapter()
        self.api = api ?? NetworkServices.shared.pushAPI
        self.appStore = appStore
        self.defaults = defaults
        self.router = router ?? PushRouteCoordinator.shared
        self.applicationIconBadgeSetter = applicationIconBadgeSetter ?? SystemApplicationIconBadgeSetter.shared
        super.init()
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
    }

    /// 绑定当前 Scene 的主窗口。
    func attach(window: UIWindow) {
        router.attach(window: window)
    }

    /// 仅在用户已同意隐私协议后初始化极光 SDK。
    func initializeIfPrivacyAccepted(
        launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
    ) {
        guard appStore.session.privacyAgreementAccepted, !isInitialized else { return }
        isInitialized = true
        sdk.initialize(launchOptions: launchOptions, isProduction: Self.isProductionBuild)
        observeJPushNetworkLogin()
        refreshRegistrationID(forceUpload: false)
    }

    /// 登录成功后请求通知权限，并强制绑定当前账号。
    func handleLoginCompleted() {
        initializeIfPrivacyAccepted()
        guard isInitialized, appStore.session.isLoggedIn else { return }
        if !didRequestAuthorization {
            didRequestAuthorization = true
            sdk.requestAuthorization(delegate: self)
        }
        bindCurrentAccount()
    }

    /// 账号切换后把同一设备重新绑定到新的业务账号。
    func handleAccountSwitched() {
        guard appStore.session.isLoggedIn else { return }
        bindCurrentAccount()
    }

    /// 退出登录时停止账号上报并清除未完成的业务路由。
    func handleLogout() {
        uploadTask?.cancel()
        uploadTask = nil
        queuedForcedUpload = false
        router.resetPendingRoute()
        Task { await updateApplicationIconBadgeCount(0) }
    }

    /// 将桌面 App Icon 角标更新为最新未读消息数量。
    func updateApplicationIconBadgeCount(_ count: Int) async {
        await applicationIconBadgeSetter.setBadgeCount(max(count, 0))
    }

    /// App 回到前台时补偿失败或尚未完成的 Registration ID 上报。
    func retryPendingRegistrationUpload() {
        guard appStore.session.isLoggedIn else { return }
        uploadCachedRegistrationID(force: false)
        refreshRegistrationID(forceUpload: false)
    }

    /// 登录根页面建立后继续执行通知点击暂存的路由。
    func routePendingNotificationIfPossible() {
        router.routePendingIfPossible()
    }

    /// APNs 注册成功后把 Device Token 注册给极光。
    func handleDeviceToken(_ deviceToken: Data) {
        guard isInitialized else { return }
        sdk.registerDeviceToken(deviceToken)
        refreshRegistrationID(forceUpload: false)
    }

    /// APNs 注册失败只记录调试信息，不阻断登录和主业务。
    func handleRegistrationError(_ error: Error) {
        #if DEBUG
        print("JPush APNs registration failed: \(error.localizedDescription)")
        #endif
    }

    /// 处理冷启动或运行中通知点击。
    func handleNotificationTap(
        payload: PushPayload,
        requestIdentifier: String
    ) {
        router.handle(
            destination: payload.destination,
            requestIdentifier: requestIdentifier
        )
    }

    /// 处理 Scene 冷启动携带的系统通知响应，并向极光上报点击。
    func handleNotificationResponse(_ response: UNNotificationResponse) {
        let userInfo = response.notification.request.content.userInfo
        Self.logReceivedPush(userInfo, source: "tap(cold-start)")
        JPUSHService.handleRemoteNotification(userInfo)
        handleNotificationTap(
            payload: PushPayload(userInfo: userInfo),
            requestIdentifier: response.notification.request.identifier
        )
    }

    /// 处理后台静默通知并向极光上报到达，不触发页面跳转。
    func handleRemoteNotification(_ userInfo: [AnyHashable: Any]) {
        Self.logReceivedPush(userInfo, source: "background")
        JPUSHService.handleRemoteNotification(userInfo)
        NotificationCenter.default.post(name: NotificationName.unreadMessageCountDidChange, object: nil)
    }

    /// 将收到的推送 payload 打印到控制台，便于联调排查。
    private nonisolated static func logReceivedPush(
        _ userInfo: [AnyHashable: Any],
        source: String
    ) {
        #if DEBUG
        let payloadText: String
        let normalized = Dictionary(uniqueKeysWithValues: userInfo.compactMap { key, value in
            (key as? String).map { ($0, value) }
        })
        if JSONSerialization.isValidJSONObject(normalized),
           let data = try? JSONSerialization.data(
            withJSONObject: normalized,
            options: [.prettyPrinted, .sortedKeys]
           ),
           let text = String(data: data, encoding: .utf8) {
            payloadText = text
        } else {
            payloadText = String(describing: userInfo)
        }
        print("[Push] received (\(source)):\n\(payloadText)")
        #endif
    }

    private func observeJPushNetworkLogin() {
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(handleJPushNetworkLogin),
            name: .jpfNetworkDidLogin,
            object: nil,
        )
    }

    @objc private func handleJPushNetworkLogin() {
        refreshRegistrationID(forceUpload: false)
    }

    private func refreshRegistrationID(forceUpload: Bool) {
        guard isInitialized else { return }
        queuedForcedFetch = queuedForcedFetch || forceUpload
        guard !isFetchingRegistrationID else { return }
        isFetchingRegistrationID = true
        sdk.fetchRegistrationID { [weak self] resultCode, registrationID in
            Task { @MainActor in
                guard let self else { return }
                let shouldForceUpload = self.queuedForcedFetch
                self.queuedForcedFetch = false
                self.isFetchingRegistrationID = false
                guard resultCode == 0 else { return }
                let normalized = registrationID?
                    .trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
                guard !normalized.isEmpty else { return }
                self.defaults.set(normalized, forKey: Key.registrationID)
                self.upload(registrationID: normalized, force: shouldForceUpload)
            }
        }
    }

    private func uploadCachedRegistrationID(force: Bool) {
        let registrationID = cachedRegistrationID
        guard !registrationID.isEmpty else { return }
        upload(registrationID: registrationID, force: force)
    }

    private func bindCurrentAccount() {
        if cachedRegistrationID.isEmpty {
            refreshRegistrationID(forceUpload: true)
        } else {
            uploadCachedRegistrationID(force: true)
            refreshRegistrationID(forceUpload: false)
        }
    }

    private var cachedRegistrationID: String {
        defaults.string(forKey: Key.registrationID)?
            .trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
    }

    private func upload(registrationID: String, force: Bool) {
        guard appStore.session.isLoggedIn,
              let uploadedKey = appStore.session.accountScopedKey(Key.uploadedRegistrationIDSuffix)
        else { return }

        if !force, defaults.string(forKey: uploadedKey) == registrationID {
            return
        }
        if uploadTask != nil {
            queuedForcedUpload = queuedForcedUpload || force
            return
        }

        let accountScope = appStore.session.accountCachePrefix
        uploadTask = Task { [weak self] in
            guard let self else { return }
            var succeeded = false
            do {
                try await self.api.registerJPushID(registrationID)
                if self.appStore.session.accountCachePrefix == accountScope {
                    self.defaults.set(registrationID, forKey: uploadedKey)
                }
                succeeded = true
            } catch is CancellationError {
                // 退出登录时取消，不视为失败重试。
            } catch {
                #if DEBUG
                print("JPush Registration ID upload failed: \(error.localizedDescription)")
                #endif
            }

            self.uploadTask = nil
            let shouldForceAgain = self.queuedForcedUpload
            self.queuedForcedUpload = false
            if succeeded, shouldForceAgain {
                self.uploadCachedRegistrationID(force: true)
            }
        }
    }

    private nonisolated static var isProductionBuild: Bool {
        #if DEBUG
        false
        #else
        true
        #endif
    }
}

extension PushNotificationManager: JPUSHRegisterDelegate {
    /// 前台通知只展示系统 UI，不自动执行业务路由。
    nonisolated func jpushNotificationCenter(
        _ center: UNUserNotificationCenter,
        willPresent notification: UNNotification,
        withCompletionHandler completionHandler: @escaping (Int) -> Void
    ) {
        let userInfo = notification.request.content.userInfo
        Self.logReceivedPush(userInfo, source: "foreground")
        JPUSHService.handleRemoteNotification(userInfo)
        Task { @MainActor in
            NotificationCenter.default.post(name: NotificationName.unreadMessageCountDidChange, object: nil)
        }
        let options: UNNotificationPresentationOptions = [.banner, .list, .sound, .badge]
        completionHandler(Int(options.rawValue))
    }

    /// 用户点击通知后上报极光统计，并在主 Actor 执行业务路由。
    nonisolated func jpushNotificationCenter(
        _ center: UNUserNotificationCenter,
        didReceive response: UNNotificationResponse,
        withCompletionHandler completionHandler: @escaping () -> Void
    ) {
        let userInfo = response.notification.request.content.userInfo
        Self.logReceivedPush(userInfo, source: "tap")
        JPUSHService.handleRemoteNotification(userInfo)
        let payload = PushPayload(userInfo: userInfo)
        let requestIdentifier = response.notification.request.identifier
        Task { @MainActor [weak self] in
            self?.handleNotificationTap(
                payload: payload,
                requestIdentifier: requestIdentifier
            )
        }
        completionHandler()
    }

    /// 系统通知设置入口回调，首期无需额外应用内页面。
    nonisolated func jpushNotificationCenter(
        _ center: UNUserNotificationCenter,
        openSettingsFor notification: UNNotification
    ) {}

    /// 极光通知授权状态回调，权限变化由系统设置管理。
    nonisolated func jpushNotificationAuthorization(
        _ status: JPAuthorizationStatus,
        withInfo info: [AnyHashable: Any]?
    ) {}
}
