//
//  SubGameViewController.swift
//  ylgamehall
//
//  子游戏 H5 承载容器：BridgedWebView 全屏铺满 + Splash + boot pipeline。
//  与 WebContainerViewController（大厅）共享同款 BridgedWebView + SplashOverlay 样式，
//  但 boot 流程更短（不拉远端配置、不做 IPA 升级、不做 lobby zip 升级），仅：
//    1) 确保子游戏 H5 已就绪（commit B: SubGameDownloader.ensureReady）
//    2) 写 app_*.js（containerRole = .subGame）
//    3) loadFileURL 子游戏 index.html
//
//  详见 docs/H5-Native-Implementation-Design.md §2.4.3 / §6.4。
//

import UIKit
import WebKit

public final class SubGameViewController: UIViewController {

    // MARK: - 入参（从 SwitchOverGameData 解析）

    private let request: SubGameRequest

    // MARK: - 实际生效的子游戏目录名
    //
    // init 时从 SubGameDirectoryStore 同步读：首次 = request.gameDir，已升级 = 末尾追加
    // 若干 "1" 的持久化值（msext gameController.m:1414 等价的 advance 链）。
    // boot pipeline 内 SubGameDownloader.ensureReady 若触发升级（远端 game_version >
    // 本地 version.xml），返回的 actualDirectory 会更新本属性。所有资源路径（loadFileURL /
    // AppDataWriter / srcIsloop assets/wav）必须走它，而不是 request.gameDir。

    private var effectiveGameDir: String

    // MARK: - UI

    private let bridgedWebView = BridgedWebView(label: "subGame")

    /// 子游戏私有的定位服务（msext gameController.m:1230 `configLocationManager`
    /// 每次进子游戏都 alloc 一个自己的 AMapLocationManager）。随本 VC 一起析构。
    private let locationService = LocationService(owner: "subGame")
    private let splash = SplashOverlay()

    // MARK: - Handlers

    private let shakeHandler = ShakeHandler()

    // MARK: - .subGameDidReturn 观察者（overlay pop 回子游戏时反向 callback getWebdata）

    private var subGameReturnObserver: NSObjectProtocol?

    // MARK: - Init

    public init(request: SubGameRequest) {
        self.request = request
        // 同步读持久化目录名，registerBridgeHandlers 的 closure 捕获 self 即可读到当前值
        self.effectiveGameDir = SubGameDirectoryStore.current(forKey: request.gameDir)
        super.init(nibName: nil, bundle: nil)
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("SubGameViewController requires SubGameRequest")
    }

    // MARK: - Lifecycle

    public override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .black

        setupBridgedWebView()
        setupSplash()

        bridgedWebView.webView.navigationDelegate = self
        registerBridgeHandlers()

        Task { @MainActor in
            await runBootPipeline()
        }
    }

    /// 注册子游戏 handler。大部分与大厅共用同一注册器（无状态 enum register），
    /// 但 SwitchOverGameData 不再注册（子游戏不能 push 更深一层，符合 §2.4.3 栈深 ≤ 2）。
    /// backgameData 在 commit C 接入。
    private func registerBridgeHandlers() {
        let bridge = bridgedWebView.bridge

        VibratorHandler.register(on: bridge)
        ClipboardHandler.register(on: bridge)
        shakeHandler.register(on: bridge)
        VoicePlayingHandler.register(on: bridge)
        DeviceInfoHandler.register(on: bridge)
        BrowserHandler.register(on: bridge)
        OpenSaomaHandler.register(on: bridge)
        StartLocationHandler.register(on: bridge, label: "subGame", location: locationService)

        // assetsRoot 闭包 = 子游戏 H5 根目录（msext gameController.m:364 等价）。
        // 用 closure 是因为 effectiveGameDir 在 boot pipeline 升级路径下会变化，
        // 必须运行时读最新值，否则升级后 srcIsloop 仍指旧目录。
        LocalAudioHandler.register(
            on: bridge,
            assetsRoot: { [weak self] in
                guard let self else {
                    return SandboxPaths.caches
                }
                return SandboxPaths.subGameIndex(self.effectiveGameDir, self.request.gameStart)
                    .deletingLastPathComponent()
            }
        )
        // 子游戏 isSubGame=true：录音上传成功后额外触发 recordSuccess 反向 callback
        // （对齐 daoqi gameController.m:2310；大厅 NewRootVC 无此 callHandler）
        RemoteAudioHandler.register(
            on: bridge,
            hostVC: { [weak self] in self },
            isSubGame: true
        )

        AccreditLoginHandler.register(on: bridge)
        FriendsShareHandler.register(on: bridge, mode: .subGame) // 子游戏路径：SharePanel 三选一 + 朋友圈兜底（对齐 msext gameController.m 活路径）

        // OpenurlTitleData 在子游戏中也可能调（threeView 弹层 push 自子游戏页），保留 stub
        OpenurlTitleDataHandler.register(on: bridge)

        // 子游戏专属：backgameData（退出回大厅 + 反向 callback getWebdata）
        BackGameDataHandler.register(on: bridge, location: locationService)

        // 视频房间 3 件套 stub（业务暂未启用，仅维持桥契约不让 H5 报 "no handler"）
        // 未来接 Agora 时把 VideoRoomHandlers 内 3 个 stub 展开实现，注册点不变。
        VideoRoomHandlers.register(on: bridge)
    }

    // MARK: - 摇一摇支持

    public override var canBecomeFirstResponder: Bool { true }

    public override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        setupExternalSubscriptions()
    }

    public override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        becomeFirstResponder()
    }

    public override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        teardownExternalSubscriptions()
    }

    public override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
        shakeHandler.handleMotionEnded(motion)
    }

    public override var supportedInterfaceOrientations: UIInterfaceOrientationMask { .landscape }
    public override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation { .landscapeRight }
    public override var prefersStatusBarHidden: Bool { false }

    // MARK: - WebView 全屏布局（与大厅同款，等价 daoqi msext gameController.m
    // 全屏 WKWebView 路径）

    private func setupBridgedWebView() {
        bridgedWebView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(bridgedWebView)

        NSLayoutConstraint.activate([
            bridgedWebView.topAnchor.constraint(equalTo: view.topAnchor),
            bridgedWebView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            bridgedWebView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            bridgedWebView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
        ])
    }

    private func setupSplash() {
        splash.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(splash)
        NSLayoutConstraint.activate([
            splash.topAnchor.constraint(equalTo: view.topAnchor),
            splash.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            splash.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            splash.trailingAnchor.constraint(equalTo: view.trailingAnchor)
        ])
        splash.update(text: "加载子游戏...", progress: nil)
    }

    // MARK: - Boot pipeline

    private func runBootPipeline() async {
        splash.isHidden = false
        splash.update(text: "加载子游戏...", progress: nil)

        do {
            try await runBootPipelineSteps()
        } catch {
            showFatalAlert(error: error)
        }
    }

    private func runBootPipelineSteps() async throws {
        // 0. H5 传过来的 SwitchOverGameData.gamedownloadurl **字段名是 url 但语义是 gameid**
        //    （msext gameController.m:544 注释明确写"游戏ID"）。真实 zip 下载地址要按
        //    gameid 去启动期缓存的 RemoteConfig 里查 `game_zip` 字段；走 VersionResolver
        //    单链 4 层 fallback（agent → game → channel → market）的同一套实现。
        //
        //    需要做两次 resolve：
        //      - lobbyResolved（gameId = bc.gameId）→ 给 AppDataWriter 算 app_appversion 审核标志
        //        与 msext NewRootVC 算 result 后 initinfo 传给 gameController 等价
        //      - subGameResolved（gameId = H5 传的 gameid）→ 拿 game_version 和 game_zip URL
        //        与 msext gameController.uplevel: 等价
        let (lobbyResolved, subGameResolved) = try await resolveBoot()
        let subGameZipURL = subGameResolved.gameZip ?? ""

        // 1. 准备子游戏 zip（msext gameController.m:1239-1255 等价）：
        //    - 目录不存在（首装）：下载到 currentDir
        //    - 目录存在 + 远端 > 本地：advance + 下载到新目录
        //    - 远端 ≤ 本地：复用 currentDir
        splash.update(text: "准备子游戏...", progress: nil)
        let outcome = try await SubGameDownloader.shared.ensureReady(
            directoryKey: request.gameDir,
            gameStart:    request.gameStart,
            remoteVersion: subGameResolved.gameVersion,
            remoteZipURL:  subGameZipURL,
            onProgress: { [weak self] p in
                Task { @MainActor in
                    self?.splash.update(
                        text: String(format: "下载子游戏 %d%%", Int(p * 100)),
                        progress: p
                    )
                }
            }
        )

        // ensureReady 内可能 advance 了目录名，同步到本地状态。
        // 此后所有资源路径（loadFileURL / AppDataWriter / srcIsloop closure）都走它。
        effectiveGameDir = outcome.actualDirectory

        // 2. 写 4 个 app_*.js（containerRole = .subGame）。BatteryMonitor / NetworkMonitor
        //    单例已由大厅启动，此处仅复用其 currentXxx。
        //    resolvedVersion 必须传大厅视角的版本判定结果，让 app_appversion 审核标志
        //    与大厅一致（msext result_state 传递路径等价）。
        //    containerRole.dir 传 effectiveGameDir，让 app_gamedir = msext gamefilepath
        //    （升级后的"xxx1"形式），与 msext app_data.js:2160 等价。
        let writer = AppDataWriter(
            bundleConfig: .shared,
            resolvedVersion: lobbyResolved,
            containerRole: .subGame(name: request.gameStart, dir: effectiveGameDir)
        )
        try writer.writeInitial()
        try writer.writeBattery(BatteryMonitor.shared.currentLevel)
        try writer.writeNetwork(NetworkMonitor.shared.currentCode)

        // 3. 加载子游戏 H5（Phase 1.F：从 loadFileURL 迁到 AppSchemeHandler）
        //    URL：ylgame://h5/subgame/<dir>/<start>/index.html
        //    与大厅同 host（"h5"）→ same-origin → 共享 localStorage。
        //    跨目录引用自动落回 subGameRoot(<dir>)，等价原 allowingReadAccessTo 语义。
        splash.update(text: "进入子游戏...", progress: nil)
        let indexURL = SandboxPaths.subGameIndexAppURL(effectiveGameDir, request.gameStart)
        bridgedWebView.webView.load(URLRequest(url: indexURL))
    }

    /// 子游戏启动期两次 resolve：
    /// - lobby 视角（gameId = bc.gameId）给 app_appversion 审核标志（msext result_state 等价）
    /// - sub-game 视角（gameId = H5 传的 gameid）给 game_version + game_zip URL（msext gameController.uplevel: 等价）
    private func resolveBoot() async throws -> (lobbyResolved: ResolvedVersion, subGameResolved: ResolvedVersion) {
        guard let cfg = await RemoteConfigClient.shared.current() else {
            throw SubGameBootError.remoteConfigUnavailable
        }
        let bc = BundleConfig.shared
        let lobbyResolved = VersionResolver.resolve(
            config: cfg,
            agentId: bc.agent,
            channelId: bc.channel,
            marketId: bc.market,
            gameId: bc.gameId
        )
        let subGameResolved = VersionResolver.resolve(
            config: cfg,
            agentId: bc.agent,
            channelId: bc.channel,
            marketId: bc.market,
            gameId: request.downloadURL    // ← H5 字段名 url，实为 gameid
        )
        // 远端必须给出 game_zip URL，否则 SubGameDownloader 首装路径无法落地。
        // 已有本地缓存且远端不需要升级时（≤本地版本），URL 空也允许 → 让 ensureReady 走复用分支。
        let zip = subGameResolved.gameZip ?? ""
        let localVer = LocalVersionReader.subGameVersion(
            dir: SubGameDirectoryStore.current(forKey: request.gameDir),
            start: request.gameStart
        )
        if zip.isEmpty && subGameResolved.gameVersion > localVer {
            throw SubGameBootError.zipURLNotFound(gameId: request.downloadURL)
        }
        return (lobbyResolved, subGameResolved)
    }

    enum SubGameBootError: Error, CustomStringConvertible {
        case remoteConfigUnavailable
        case zipURLNotFound(gameId: String)

        var description: String {
            switch self {
            case .remoteConfigUnavailable:
                return "远端配置未就绪，请返回大厅重试"
            case .zipURLNotFound(let id):
                return "未找到子游戏 \(id) 的下载地址"
            }
        }
    }

    // MARK: - 外部订阅生命周期（参 §2.4.2）
    //
    // 子游戏栈顶时挂钩 battery / network / appservice。大厅 push 子游戏后大厅
    // viewWillDisappear 已 teardown，避免双发。子游戏 pop 时 teardown，大厅
    // viewWillAppear 重新 setup。
    //
    // 与大厅一致：业务期变化不重写文件，evaluateJavaScript 重新赋值 window.app_*。

    private func setupExternalSubscriptions() {
        NetworkMonitor.shared.start()
        BatteryMonitor.shared.start()
        AppLifecycleObserver.shared.start()
        // 仅子游戏挂电话状态监听（大厅不订阅，与 msext gameController 同款边界）
        PhoneStateMonitor.shared.start()

        let bridge = bridgedWebView.bridge
        let webView = bridgedWebView.webView

        BatteryMonitor.shared.onChange = { level in
            let value = String(format: "%.2f", level)
            Task { @MainActor in
                _ = try? await webView.evaluateJavaScript("window.app_getbattery=\(value);")
            }
            bridge.call("getBattery", data: .string(value), callback: nil)
        }

        NetworkMonitor.shared.onChange = { code in
            Task { @MainActor in
                _ = try? await webView.evaluateJavaScript("window.app_getnetwork=\(code);")
            }
            bridge.call("getnetwork", data: .string("\(code)"), callback: nil)
        }

        // §3.2 ［4］ appservice — 值错位沿用历史 msext WKWebView 路径：
        // 真·进入后台 → "2"，真·回到前台 → "1"。参 WebContainerViewController 同位置注释。
        AppLifecycleObserver.shared.onBackground = {
            bridge.call("appservice", data: .string("2"), callback: nil)
        }
        AppLifecycleObserver.shared.onForeground = {
            bridge.call("appservice", data: .string("1"), callback: nil)
        }

        // 电话状态 — 契约 §3.1 ［14］：来电 "2" / 挂断 "0"
        PhoneStateMonitor.shared.onChange = { state in
            bridge.call("phonestate", data: .string(state), callback: nil)
        }

        // overlay（Phase 7）pop 回子游戏时反向 callback getWebdata。**lifecycle-tied**：
        // 子游戏栈顶时挂钩，overlay push / 子游戏自身 pop 时由 viewWillDisappear 解绑。
        // 这一行为与大厅 observer 的"永久挂钩"配合，复现 msext 双观察者行为：
        //   - 子游戏自身 backgameData pop 回大厅：sub-game observer 在 viewWillDisappear
        //     已解绑 → 仅大厅 observer 触发 → 大厅 H5 收到 getWebdata（msext 单发等价）
        //   - 子游戏内 overlay backgameData pop：sub-game.viewWillAppear 时 observer 重挂
        //     → 大厅 + sub-game 双触发 → 大厅 H5 + sub-game H5 双发收到（msext 双观察者等价）
        subGameReturnObserver = NotificationCenter.default.addObserver(
            forName: .subGameDidReturn,
            object: nil,
            queue: .main
        ) { note in
            let data = (note.userInfo?["data"] as? String) ?? ""
            MainActor.assumeIsolated {
                bridge.call("getWebdata", data: .string(data), callback: nil)
            }
        }
    }

    private func teardownExternalSubscriptions() {
        BatteryMonitor.shared.onChange = nil
        NetworkMonitor.shared.onChange = nil
        AppLifecycleObserver.shared.onBackground = nil
        AppLifecycleObserver.shared.onForeground = nil
        PhoneStateMonitor.shared.onChange = nil
        PhoneStateMonitor.shared.stop()

        if let token = subGameReturnObserver {
            NotificationCenter.default.removeObserver(token)
            subGameReturnObserver = nil
        }
    }

    // MARK: - Error alert

    private func showFatalAlert(error: Error) {
        let alert = UIAlertController(
            title: "加载失败",
            message: "\(error)",
            preferredStyle: .alert
        )
        alert.addAction(UIAlertAction(title: "返回大厅", style: .default) { [weak self] _ in
            self?.navigationController?.popViewController(animated: true)
        })
        present(alert, animated: true)
    }
}

// MARK: - WKNavigationDelegate

extension SubGameViewController: WKNavigationDelegate {
    public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        UIView.animate(withDuration: 0.3, animations: { [weak self] in
            self?.splash.alpha = 0
        }, completion: { [weak self] _ in
            self?.splash.removeFromSuperview()
        })
    }
}

