import APIClient
import Foundation
import HostRegistry
import OSLog
import UIKit
import UserNotifications
import WireProtocol

/// T-iOS-21 · 通知动作处理：锁屏 Allow/Deny（系统**后台拉起主 App** 并送达
/// `UNUserNotificationCenterDelegate.didReceive`——本工程无 notification
/// extension target，Service Extension 也收不到 action tap）与默认点按路由。
///
/// 安全纪律：
/// - push payload 是**不可信外部输入**——sessionId 经 `DeepLinkRouter.route(from:)`
///   （冻结的 v4 白名单，不再造第二个解析器），capability token 校验 v4 形状
///   （服务器 `randomUUID()` 生成，src/server.ts:445）后**原样透传**（服务器做
///   字节级比对），任一非法 → `.invalidPayload` 丢弃计数，绝不部分应用；
/// - token 用后即弃：只存在于解析值 → `hookDecision` 调用参数，绝不落盘、
///   绝不进任何兜底文案/日志；
/// - POST 全程包在 begin/endBackgroundTask 里，didReceive 的 async 返回
///  （= completionHandler）在 POST settle 之后；
/// - 失败必须可见：403（token 过期/已用）与传输失败都补一条本地通知，绝不静默吞。

// MARK: - Seams

/// Seam over `UIApplication.beginBackgroundTask`/`endBackgroundTask`.
@MainActor
protocol BackgroundTaskRunning: AnyObject {
    /// Returns an opaque token for `end(_:)`（生产映射 UIBackgroundTaskIdentifier）。
    func begin(name: String) -> Int
    func end(_ token: Int)
}

@MainActor
final class UIApplicationBackgroundTaskRunner: BackgroundTaskRunning {
    private var active: [Int: UIBackgroundTaskIdentifier] = [:]

    func begin(name: String) -> Int {
        var identifier = UIBackgroundTaskIdentifier.invalid
        identifier = UIApplication.shared.beginBackgroundTask(withName: name) {
            // 系统到期回调在主线程同步调用（UIKit 文档保证）。
            MainActor.assumeIsolated { self.expire(identifier) }
        }
        active[identifier.rawValue] = identifier
        return identifier.rawValue
    }

    func end(_ token: Int) {
        guard let identifier = active.removeValue(forKey: token) else { return }
        UIApplication.shared.endBackgroundTask(identifier)
    }

    private func expire(_ identifier: UIBackgroundTaskIdentifier) {
        guard identifier != .invalid, active.removeValue(forKey: identifier.rawValue) != nil else {
            return
        }
        UIApplication.shared.endBackgroundTask(identifier)
    }
}

/// Seam for the local-notification fallback（决策失败可见性）。
@MainActor
protocol LocalNoticePosting: AnyObject {
    func post(title: String, body: String) async
}

extension UNNotificationCenterAdapter: LocalNoticePosting {
    func post(title: String, body: String) async {
        let content = UNMutableNotificationContent()
        content.title = title
        content.body = body
        let request = UNNotificationRequest(
            identifier: UUID().uuidString, content: content, trigger: nil
        )
        do {
            try await add(request)
        } catch {
            // 最后的兜底也失败：只剩日志（不能再用通知报告通知失败）。
            Logger(subsystem: PushLog.subsystem, category: PushLog.actionHandler)
                .error("fallback local notice failed: \(error)")
        }
    }
}

/// 用户可见兜底文案（payload 最小化：不含 token、不含会话内容）。
enum PushDecisionCopy {
    static let expiredTitle = "审批已失效"
    static let expiredBody = "该次批准/拒绝已过期或已被处理，请打开 App 在会话中查看。"
    static let failedTitle = "审批发送失败"
    static let failedBody = "无法联系主机，请打开 App 在会话中处理。"
}

/// `parse` 的白名单产物（Sendable——didReceive 在 nonisolated 上下文解析后
/// 才跨进 MainActor，UN 对象绝不跨 isolation）。
enum ParsedNotificationAction: Sendable, Equatable {
    /// Allow/Deny 动作按钮 + 合法 `{sessionId, token}`。
    case decision(HookDecision, sessionId: UUID, token: String)
    /// 默认点按（含 done 类通知）+ 合法 sessionId。
    case openSession(sessionId: UUID)
    /// 目标动作的 payload 校验失败 → 丢弃计数（镜像"非法帧静默丢弃"）。
    case invalidPayload
    /// dismiss / 未知动作 id → no-op。
    case dismissed
}

// MARK: - Handler

@MainActor
final class NotificationActionHandler: NSObject {
    /// Coordinator 提供的路由效果（点按 → 打开会话）。
    struct Actions {
        let openSession: @MainActor (HostRegistry.Host, UUID) async -> Void
    }

    private enum TaskName {
        static let decision = "webterm.hook-decision"
    }

    private let hostStore: any HostStore
    private let http: any HTTPTransport
    private let backgroundTasks: any BackgroundTaskRunning
    private let notices: any LocalNoticePosting
    private let actions: Actions
    private let logger = Logger(subsystem: PushLog.subsystem, category: PushLog.actionHandler)

    private(set) var invalidPayloadCount = 0

    init(
        hostStore: any HostStore,
        http: any HTTPTransport,
        backgroundTasks: any BackgroundTaskRunning,
        notices: any LocalNoticePosting,
        actions: Actions
    ) {
        self.hostStore = hostStore
        self.http = http
        self.backgroundTasks = backgroundTasks
        self.notices = notices
        self.actions = actions
    }

    // MARK: - Parse（纯函数，payload 级可测核心）

    nonisolated static func parse(
        actionIdentifier: String, userInfo: [AnyHashable: Any]
    ) -> ParsedNotificationAction {
        switch actionIdentifier {
        case GateNotificationCategory.allowActionId:
            return decisionAction(.allow, userInfo: userInfo)
        case GateNotificationCategory.denyActionId:
            return decisionAction(.deny, userInfo: userInfo)
        case UNNotificationDefaultActionIdentifier:
            guard case let .gateSession(sessionId) = DeepLinkRouter.route(from: userInfo) else {
                return .invalidPayload
            }
            return .openSession(sessionId: sessionId)
        default:
            return .dismissed
        }
    }

    private nonisolated static func decisionAction(
        _ decision: HookDecision, userInfo: [AnyHashable: Any]
    ) -> ParsedNotificationAction {
        guard case let .gateSession(sessionId) = DeepLinkRouter.route(from: userInfo),
              let token = userInfo[PayloadKey.token] as? String,
              Validation.isValidSessionId(token) // 服务器 token = randomUUID() → 同一 v4 白名单
        else { return .invalidPayload }
        return .decision(decision, sessionId: sessionId, token: token)
    }

    private enum PayloadKey {
        /// 根级 capability token（T-iOS-20 payload 定稿，仅 gate 类携带）。
        static let token = "token"
    }

    // MARK: - Handle

    func handle(_ action: ParsedNotificationAction) async {
        switch action {
        case .dismissed:
            return
        case .invalidPayload:
            invalidPayloadCount += 1
            // 不可信输入：只记计数，不回显 payload（日志注入/隐私卫生）。
            logger.notice("dropped invalid push payload (total: \(self.invalidPayloadCount))")
        case let .openSession(sessionId):
            await routeToSession(sessionId)
        case let .decision(decision, sessionId, token):
            await postDecision(decision, sessionId: sessionId, token: token)
        }
    }

    // MARK: - Decision（Allow/Deny → POST /hook/decision）

    private func postDecision(_ decision: HookDecision, sessionId: UUID, token: String) async {
        let taskToken = backgroundTasks.begin(name: TaskName.decision)
        defer { backgroundTasks.end(taskToken) }
        do {
            guard let host = try await resolveHost(sessionId: sessionId) else {
                logger.error("hook decision: no paired host lists session — cannot deliver")
                await notices.post(
                    title: PushDecisionCopy.failedTitle, body: PushDecisionCopy.failedBody
                )
                return
            }
            try await APIClient(endpoint: host.endpoint, http: http)
                .hookDecision(sessionId: sessionId, decision: decision, token: token)
        } catch APIClientError.decisionRejected {
            // 403：token 过期/已用/不匹配（SEC-C1/M1）→ 引导进 App 处理。
            await notices.post(
                title: PushDecisionCopy.expiredTitle, body: PushDecisionCopy.expiredBody
            )
        } catch {
            logger.error("hook decision POST failed: \(error)")
            await notices.post(
                title: PushDecisionCopy.failedTitle, body: PushDecisionCopy.failedBody
            )
        }
    }

    // MARK: - Default tap（→ DeepLinkRouter 已验证的 sessionId → 会话）

    private func routeToSession(_ sessionId: UUID) async {
        do {
            guard let host = try await resolveHost(sessionId: sessionId) else {
                // App 已被点按拉起：落在会话列表即可，无需报错弹窗。
                logger.notice("push tap: session not found on any paired host")
                return
            }
            await actions.openSession(host, sessionId)
        } catch {
            logger.error("push tap: host store read failed: \(error)")
        }
    }

    // MARK: - Host resolution（payload 不含 host 身份——本 handler 拥有解析策略）

    /// 单主机 → 直取（离线也能把 POST 打出去，错了会显式失败）；多主机 →
    /// 逐主机 RO 查 `/live-sessions`（无 Origin）找到持有该会话的主机。
    private func resolveHost(sessionId: UUID) async throws -> HostRegistry.Host? {
        let hosts = try await hostStore.loadAll()
        if hosts.count <= 1 { return hosts.first }
        for host in hosts {
            let sessions = (try? await APIClient(endpoint: host.endpoint, http: http)
                .liveSessions()) ?? []
            if sessions.contains(where: { $0.id == sessionId }) { return host }
        }
        return nil
    }
}

// MARK: - UNUserNotificationCenterDelegate（薄胶水；UNNotificationResponse 无法单测构造）

extension NotificationActionHandler: UNUserNotificationCenterDelegate {
    /// async 变体 = 系统的 completionHandler 在本方法 return 时才触发——
    /// 任务要求"completionHandler only after the POST settles"由此保证。
    /// UN 对象只在 nonisolated 入口同步读取，跨进 MainActor 的只有 Sendable
    /// 的 `ParsedNotificationAction`。
    nonisolated func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        didReceive response: UNNotificationResponse
    ) async {
        let parsed = Self.parse(
            actionIdentifier: response.actionIdentifier,
            userInfo: response.notification.request.content.userInfo
        )
        await handle(parsed)
    }
}

// MARK: - AppCoordinator wiring（与 DeepLinkRouter.swift 的 makeDeepLinkHandler 同款先例）

extension AppCoordinator {
    /// PushAppDelegate.didFinishLaunching 的组装点（生产依赖图单点：
    /// hostStore/http 都来自 AppEnvironment，无 ad-hoc URLSession/keychain）。
    func makePushWiring() -> (registrar: PushRegistrar, handler: NotificationActionHandler) {
        let center = UNNotificationCenterAdapter()
        let registrar = PushRegistrar(
            hostStore: environment.hostStore,
            http: environment.http,
            center: center,
            remote: UIApplicationRemoteRegistrar()
        )
        let handler = NotificationActionHandler(
            hostStore: environment.hostStore,
            http: environment.http,
            backgroundTasks: UIApplicationBackgroundTaskRunner(),
            notices: center,
            actions: NotificationActionHandler.Actions(
                openSession: { [weak self] host, sessionId in
                    await self?.openPushedSession(host: host, sessionId: sessionId)
                }
            )
        )
        return (registrar, handler)
    }

    /// 通知点按 → 直达会话。先 `bootstrap()`（幂等，`route == .loading` 才
    /// 生效）确保冷启动点按不早于根路由建立；再镜像 deep-link 的
    /// close→open（openDeepLinkedSession 是 T-iOS-22 文件的 private——
    /// 三行复刻以尊重文件所有权）。
    func openPushedSession(host: HostRegistry.Host, sessionId: UUID) async {
        await bootstrap()
        if terminalController != nil {
            closeTerminal()
        }
        open(SessionListViewModel.OpenRequest(id: UUID(), host: host, sessionId: sessionId))
    }
}
