//
//  BridgeBus.swift
//  ylgamehall
//
//  BridgeProtocol 的具体实现 + WKScriptMessageHandler 适配。
//  详见 docs/H5-Native-Implementation-Design.md §3.3。
//

import Foundation
import WebKit
import os.log

/// 桥诊断日志。文件级 `let`（Logger 是 Sendable）以便在 evaluateJavaScript
/// 完成回调等非 MainActor 上下文里也能直接用。
private let bridgeLog = Logger(subsystem: "ylgamehall", category: "Bridge")

/// ── 临时诊断输出（排查「子游戏收不到 getlocationinfo」，结束后整体移除）──
///
/// 真机上 `Logger.debug` 会被系统日志级别过滤掉：devicectl console / Xcode
/// console 都只能看到 `.error` 及以上。`print` 直写 stderr 必达，所以诊断期
/// 统一走这里；带毫秒时间戳以便与 CoreLocation 的 os_log 行对齐时序。
/// 同时落盘到 `Documents/diag.log`：用 Xcode 跑真机时 print 只进 Xcode console，
/// 外部拿不到；落盘后可用
///   xcrun devicectl device copy from --domain-type appDataContainer \
///     --domain-identifier com.skyapp.ylgamehall --source Documents/diag.log ...
/// 把整轮日志取出来分析。
nonisolated func diagLog(_ message: String) {
    let line = "[DIAG \(diagTimestamp())] \(message)"
    print(line)
    diagFileSink.append(line)
}

private nonisolated let diagFormatter: DateFormatter = {
    let f = DateFormatter()
    f.dateFormat = "HH:mm:ss.SSS"
    return f
}()

private nonisolated func diagTimestamp() -> String {
    diagFormatter.string(from: Date())
}

/// 串行写入 `Documents/diag.log`。每个进程首次写入前插一行 session 分隔，
/// 便于区分多次启动。纯诊断设施，排查结束随 diagLog 一并移除。
// 工程默认 SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor，需显式 nonisolated 才能在
// @Sendable 桥回调 / evaluateJavaScript 完成块里同步调用。
private nonisolated final class DiagFileSink: @unchecked Sendable {
    private let queue = DispatchQueue(label: "ylgamehall.diag.log")
    private let url: URL
    private var wroteHeader = false

    init() {
        let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
        url = dir.appendingPathComponent("diag.log")
    }

    func append(_ line: String) {
        queue.async { [self] in
            var text = line + "\n"
            if !wroteHeader {
                wroteHeader = true
                let stamp = ISO8601DateFormatter().string(from: Date())
                text = "\n===== session \(stamp) pid=\(ProcessInfo.processInfo.processIdentifier) =====\n" + text
            }
            guard let data = text.data(using: .utf8) else { return }
            let fm = FileManager.default
            if !fm.fileExists(atPath: url.path) {
                fm.createFile(atPath: url.path, contents: nil)
            }
            guard let handle = try? FileHandle(forWritingTo: url) else { return }
            handle.seekToEndOfFile()
            handle.write(data)
            try? handle.close()
        }
    }
}

private nonisolated let diagFileSink = DiagFileSink()


/// H5 ↔ Native 消息总线。
///
/// **运行时约束**：必须挂在 WKWebView 上、@MainActor 单线程访问。
/// **生命周期**：构造时自动向 `WKUserContentController` 注册 `WVJBHandler` 通道，
///   销毁时由 WebView 链路自然释放（无需手动 remove）。
@MainActor
public final class BridgeBus: NSObject, BridgeProtocol, WKScriptMessageHandler {

    /// JS → Native 的 WKScriptMessageHandler 通道名。
    /// JS 端通过 `window.webkit.messageHandlers.WVJBHandler.postMessage({...})` 发送。
    public static let scriptMessageHandlerName = "WVJBHandler"

    /// JS 端注入的全局桥对象名（约定 `window.WebViewJavascriptBridge`）。
    public static let jsBridgeName = "WebViewJavascriptBridge"

    /// 容器标识（`lobby` / `subGame` / …），仅用于诊断日志区分是哪个 WebView 的桥。
    /// 不参与任何契约行为。
    public let label: String

    private weak var webView: WKWebView?
    private var handlers: [String: BridgeHandler] = [:]
    private var pendingCallbacks: [String: BridgeCallback] = [:]
    private var nativeCallbackCounter: UInt64 = 0

    /// 桥就绪前的消息暂存队列。等价 msext `WebViewJavascriptBridgeBase.startupMessageQueue`：
    /// `_queueMessage` 在 bridge 尚未 loaded 时入队，`injectJavascriptFile` 时按序 flush。
    ///
    /// 早前的实现在 `window.WebViewJavascriptBridge` 不存在时**直接丢弃**消息 ——
    /// 真机日志里每次启动都有一条 `sendToJS 'getnetwork' 未送达 H5：no-bridge`，
    /// msext 里这条是会补发到 H5 的。子游戏 WebView 是全新建的、H5 很早就调
    /// `startlocation`，反向 `getlocationinfo` 落在这个窗口里就会被静默吞掉。
    private var startupMessageQueue: [[String: Any]] = []

    /// H5 侧 WVJB.js 初始化完成后会 postMessage `{bridgeReady:true}`，收到即置 true 并 flush。
    private var bridgeReady = false

    /// 构造时绑定 WebView + 用户内容控制器，自动注册 `WVJBHandler` 消息处理器。
    public init(webView: WKWebView,
                controller: WKUserContentController,
                label: String = "webview") {
        self.webView = webView
        self.label = label
        super.init()
        controller.add(self, name: Self.scriptMessageHandlerName)
    }

    // MARK: - BridgeProtocol

    public func register(_ name: String, handler: @escaping BridgeHandler) {
        handlers[name] = handler
        diagLog("[\(self.label)] register handler '\(name)'")
    }

    public func call(_ name: String, data: BridgeData?, callback: BridgeCallback?) {
        var payload: [String: Any] = ["handlerName": name]
        if let data {
            payload["data"] = data.jsonObject
        }
        if let callback {
            nativeCallbackCounter += 1
            let cbId = "objc_cb_\(nativeCallbackCounter)"
            pendingCallbacks[cbId] = callback
            payload["callbackId"] = cbId
        }
        // 诊断：打出实际序列化后的字节，确认字段集合 / 类型 / 引号与 msext 一致
        let wire = (try? JSONSerialization.data(withJSONObject: payload, options: [.sortedKeys]))
            .flatMap { String(data: $0, encoding: .utf8) } ?? "<encode-failed>"
        diagLog("[\(self.label)] → H5 callHandler '\(name)' wire=\(wire)")
        sendToJS(payload: payload)
    }

    // MARK: - WKScriptMessageHandler

    public func userContentController(_ userContentController: WKUserContentController,
                                       didReceive message: WKScriptMessage) {
        // body 可能是单条 dict 或一批 dict array（WVJB 支持批处理）
        if let batch = message.body as? [[String: Any]] {
            for msg in batch { handleIncoming(msg) }
        } else if let single = message.body as? [String: Any] {
            handleIncoming(single)
        } else {
            print("[BridgeBus] unrecognized message body type: \(type(of: message.body))")
        }
    }

    // MARK: - 私有

    private func handleIncoming(_ msg: [String: Any]) {
        // H5 侧 WVJB.js 初始化完成信号（等价 msext 的 `__bridge_loaded__`）。
        // 每次导航都会重新注入 user script，因此会重复收到；每次都要重新 flush。
        if msg["bridgeReady"] as? Bool == true {
            bridgeReady = true
            let queued = startupMessageQueue
            startupMessageQueue.removeAll()
            if !queued.isEmpty {
                diagLog("[\(self.label)] 桥就绪，flush \(queued.count) 条暂存消息")
            }
            for payload in queued { dispatchToJS(payload: payload) }
            return
        }

        // 诊断上报：H5 侧派发结果（有没有对应 handler、handler 有没有抛错）
        if let dispatched = msg["diagDispatched"] as? String {
            let had = msg["hadHandler"] as? Bool ?? false
            let threw = msg["threw"] as? String
            diagLog("[\(self.label)] H5 派发 '\(dispatched)'：hadHandler=\(had) threw=\(threw ?? "nil")")
            return
        }

        // 优先判 responseId（JS 响应 Native 早前的 callHandler）
        if let responseId = msg["responseId"] as? String {
            let respData = msg["responseData"].flatMap { BridgeData(jsonObject: $0) }
            if let cb = pendingCallbacks.removeValue(forKey: responseId) {
                cb(respData)
            } else {
                print("[BridgeBus] stale responseId: \(responseId)")
            }
            return
        }

        // 否则是 JS → Native handler 调用
        guard let name = msg["handlerName"] as? String else {
            print("[BridgeBus] message missing handlerName: \(msg)")
            return
        }
        let data = msg["data"].flatMap { BridgeData(jsonObject: $0) }
        let callbackId = msg["callbackId"] as? String

        // 构造一个回调闭包，handler 调用即把 responseData 异步派回 JS
        let responseCallback: BridgeCallback? = makeResponseCallback(for: callbackId)

        diagLog("[\(self.label)] ← H5 call '\(name)' data=\(String(describing: msg["data"]))")

        guard let handler = handlers[name] else {
            // msext `WebViewJavascriptBridgeBase.flushMessageQueue`：
            //   if (!handler) { NSLog(@"WVJBNoHandlerException, ..."); continue; }
            // `continue` 意味着 **responseCallback 一次都不会被调用**。
            //
            // 早前这里调了 `responseCallback?(nil)`，会给 H5 回一条 {responseId:...}。
            // 现网 H5 的 `Func.getlocation()` 就是带 callback 调 `getlocationinfo` 的
            // （`bridge.callHandler('getlocationinfo',"",function(resp){})`，而
            // `getlocationinfo` 两边都没有对应的原生 handler），于是这条在原工程里
            // 永不触发的 JS 回调在我们这儿会被触发一次 —— 链条不一致。改为不回。
            diagLog("[\(self.label)] no NATIVE handler registered for '\(name)'（msext 同款：不回 responseCallback）")
            return
        }

        // handler 是 async，独立 Task 内 await；本方法保持 sync 返回
        Task {
            await handler(data, responseCallback)
        }
    }

    private func makeResponseCallback(for callbackId: String?) -> BridgeCallback? {
        guard let id = callbackId else { return nil }
        return { [weak self] respData in
            Task { @MainActor in
                self?.sendResponseToJS(responseId: id, data: respData)
            }
        }
    }

    private func sendResponseToJS(responseId: String, data: BridgeData?) {
        var payload: [String: Any] = ["responseId": responseId]
        if let data {
            payload["responseData"] = data.jsonObject
        }
        sendToJS(payload: payload)
    }

    /// 等价 msext `WebViewJavascriptBridgeBase._queueMessage:`：
    /// 桥未就绪则入 `startupMessageQueue`，就绪则直接派发。
    private func sendToJS(payload: [String: Any]) {
        guard bridgeReady else {
            let what = (payload["handlerName"] as? String) ?? (payload["responseId"] as? String) ?? "?"
            diagLog("[\(self.label)] '\(what)' 桥未就绪 → 入队（msext startupMessageQueue 等价，队列长 \(startupMessageQueue.count + 1)）")
            startupMessageQueue.append(payload)
            return
        }
        dispatchToJS(payload: payload)
    }

    /// 等价 msext `_dispatchMessage:`：序列化 → 注入 `_handleMessageFromObjC`。
    /// H5 侧派发是 `setTimeout` 异步的（msext dispatchMessagesWithTimeoutSafety=true），
    /// 因此 evaluateJavaScript 的返回值**不代表** H5 handler 是否执行，不要据此判断送达。
    private func dispatchToJS(payload: [String: Any]) {
        guard let webView else {
            diagLog("[\(self.label)] dispatchToJS 丢弃：webView 已释放")
            return
        }
        guard let data = try? JSONSerialization.data(withJSONObject: payload),
              let json = String(data: data, encoding: .utf8)
        else {
            diagLog("[\(self.label)] JSON encode failed for payload: \(String(describing: payload))")
            return
        }
        // base64 包装避免 JSON 内单引号 / 反斜杠扰乱 JS 字符串字面量
        // （msext 是逐个 escape 反斜杠/引号/换行/U+2028/U+2029，等价）
        let base64 = Data(json.utf8).base64EncodedString()
        let js = "window.\(Self.jsBridgeName)._handleMessageFromObjC('\(base64)');"
        let tag = label
        let what = (payload["handlerName"] as? String) ?? (payload["responseId"] as? String) ?? "?"
        webView.evaluateJavaScript(js) { _, error in
            if let error {
                diagLog("[\(tag)] dispatchToJS '\(what)' evaluateJavaScript 失败: \(error.localizedDescription)")
            }
        }
    }
}
