//
//  WebContainerViewController.swift
//  ylgamehall
//
//  H5 大厅承载容器：BridgedWebView 嵌入 + 16:9 letterbox + Splash 覆盖层
//  + 启动流水线（ensureReady → fetch → resolve → upgrade → loadFileURL）。
//  详见 docs/H5-Native-Implementation-Design.md §6.3.5。
//

import UIKit
import WebKit

public final class WebContainerViewController: UIViewController {

    // MARK: - UI

    private let bridgedWebView = BridgedWebView()
    private let splash = SplashOverlay()

    // MARK: - Handlers (有状态的 handler 注册器持有，无状态的走 enum 静态 register)

    private let shakeHandler = ShakeHandler()

    // MARK: - Boot pipeline 内部错误分类
    //
    // 用 enum 区分"终态阻塞"与"系统级错误"：
    // - shortText / ipaUpgrade 是运营杀手锏，弹窗后永停（无重试按钮）
    // - 其它（网络 / 解析 / 解压 / 下载失败）→ showFatalAlert，提供重试

    private enum BootError: Error {
        /// 短文本响应 或 showmessage 非空：契约 §3.2 运营杀手锏 #1/#2
        case operationalMessage(String)
        /// IPA 需要升级，弹窗 + Safari 外链
        case ipaUpgradeRequired(downloadURL: String)
    }

    // MARK: - Lifecycle

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

        setupBridgedWebView()
        setupSplash()

        bridgedWebView.webView.navigationDelegate = self
        registerBridgeHandlers()

        Task { @MainActor in
            await runBootPipeline()
        }
    }

    /// 注册所有 H5 → Native handler 的单一聚合点。
    /// Phase 1.15 仅 vibrator；Phase 2.A 扩展到 8 个 handler 模块。
    private func registerBridgeHandlers() {
        let bridge = bridgedWebView.bridge

        // Phase 1.15
        VibratorHandler.register(on: bridge)                  // §3.1 ［10］［11］［12］扩展

        // Phase 2.A 大厅简单 handler（无外部依赖）
        ClipboardHandler.register(on: bridge)                 // §3.1 ［13］［14］
        shakeHandler.register(on: bridge)                     // §3.1 ［7］［8］［9］ + §3.2 ［5］
        VoicePlayingHandler.register(on: bridge)              // §3.1 ［6］
        DeviceInfoHandler.register(on: bridge)                // §3.1 ［21］ + §3.2 ［1］
        BrowserHandler.register(on: bridge)                   // §3.1 ［16］
        OpenSaomaHandler.register(on: bridge)                 // §3.1 ［22］空 stub
        StartLocationHandler.register(on: bridge)             // §3.1 ［20］Phase 5 完整实现，Phase 2 stub
    }

    // MARK: - 摇一摇支持
    // motionEnded 必须 first responder 才触发；canBecomeFirstResponder = true +
    // viewDidAppear becomeFirstResponder。详见 Design §8.7.1。

    public override var canBecomeFirstResponder: Bool { true }

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

    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: - 16:9 letterbox 布局
    //
    // H5 设计分辨率 1280×720（16:9）。设备比例与 16:9 不匹配时用黑边补齐：
    // - 比 16:9 宽（如现代 iPhone 横屏 2.16:1）→ 左右黑边
    // - 比 16:9 窄（如 iPad 4:3）→ 上下黑边
    //
    // 约束策略：
    //   required：aspect = 16:9，width ≤ view.width，height ≤ view.height
    //   low：    width = view.width，height = view.height（尽量撑满）
    //   居中：   centerX / centerY
    // Auto Layout 会自动选择"短边贴边、长边居中"的解。

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

        let aspect = bridgedWebView.widthAnchor.constraint(
            equalTo: bridgedWebView.heightAnchor,
            multiplier: 16.0 / 9.0
        )
        let widthMax = bridgedWebView.widthAnchor.constraint(lessThanOrEqualTo: view.widthAnchor)
        let heightMax = bridgedWebView.heightAnchor.constraint(lessThanOrEqualTo: view.heightAnchor)
        let widthFill = bridgedWebView.widthAnchor.constraint(equalTo: view.widthAnchor)
        let heightFill = bridgedWebView.heightAnchor.constraint(equalTo: view.heightAnchor)
        widthFill.priority = .defaultLow
        heightFill.priority = .defaultLow

        NSLayoutConstraint.activate([
            bridgedWebView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            bridgedWebView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            aspect, widthMax, heightMax, widthFill, heightFill
        ])
    }

    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 {
        // 重新进入 pipeline 时（重试路径）：重置 splash 并显示
        splash.isHidden = false
        splash.update(text: "拼命启动中...", progress: nil)

        do {
            try await runBootPipelineSteps()
        } catch BootError.operationalMessage(let msg) {
            showBlockingAlert(message: msg)
        } catch BootError.ipaUpgradeRequired(let dl) {
            showIPAUpgradeAlert(downloadURL: dl)
        } catch {
            showFatalAlert(error: error)
        }
    }

    private func runBootPipelineSteps() async throws {
        // 1. 首装解压（仅首次安装走真正的 unzip；之后 fileExists 短路 ≤ 1ms）
        try await ResourceUnzipper.shared.ensureReady()

        // 2. 拉远端渠道配置
        splash.update(text: "拉取配置中...", progress: nil)
        let outcome = try await RemoteConfigClient.shared.fetch()

        let resolved: ResolvedVersion
        switch outcome {
        case .shortText(let msg):
            // 运营杀手锏 #1：运营把整段服务端响应替换为短文本，前端弹窗永停
            throw BootError.operationalMessage(msg)

        case .parsed(let cfg):
            let bc = BundleConfig.shared
            let r = VersionResolver.resolve(
                config: cfg,
                agentId: bc.agent,
                channelId: bc.channel,
                marketId: bc.market,
                gameId: bc.gameId
            )

            // 3. 运营杀手锏 #2：showmessage 非空 → 弹窗永停
            if let msg = r.showmessage, !msg.isEmpty {
                throw BootError.operationalMessage(msg)
            }

            // 4. IPA 升级（远端 appVersion > 本地）：弹窗 + Safari 外链永停
            if r.appVersion > LocalVersionReader.localAppVersion,
               let dl = r.appDownload {
                throw BootError.ipaUpgradeRequired(downloadURL: dl)
            }

            // 5. H5 zip 升级（远端 gameVersion > 本地）：进度条实时更新
            //    LobbyZipUpgrader 的 onProgress 由 URLSession delegate 在后台队列调度，
            //    必须显式 hop 到 MainActor 才能动 splash UI。
            _ = try await LobbyZipUpgrader.shared.upgradeIfNeeded(
                resolved: r,
                onProgress: { [weak self] p in
                    Task { @MainActor in
                        self?.splash.update(
                            text: String(format: "下载更新中 %d%%", Int(p * 100)),
                            progress: p
                        )
                    }
                }
            )
            resolved = r
        }

        // 6. 在 loadFileURL 之前写 4 个 app_*.js（与原 msext NewRootVC.initJSdata 等价时序）
        //    详见 docs/H5-Native-Implementation-Design.md §7.5
        try writeAppDataFiles(resolved: resolved)

        // 7. 加载本地 H5（allowingReadAccessTo 必须给 lobbyRoot 才能跨目录引用资源）
        splash.update(text: "加载大厅...", progress: nil)
        bridgedWebView.webView.loadFileURL(
            SandboxPaths.lobbyIndex,
            allowingReadAccessTo: SandboxPaths.lobbyRoot
        )
    }

    /// 写 4 个 app_*.js 文件，并挂钩 battery / network 变化的重写监听。
    /// 与原 msext NewRootVC.initJSdata + changebattery + changenetstate 等价。
    private func writeAppDataFiles(resolved: ResolvedVersion) throws {
        let writer = AppDataWriter(
            bundleConfig: .shared,
            resolvedVersion: resolved,
            containerRole: .lobby
        )

        // 启动 NetworkMonitor（幂等：内部 started 标记）
        NetworkMonitor.shared.start()
        UIDevice.current.isBatteryMonitoringEnabled = true

        // 首次写入：4 个文件全部落盘
        try writer.writeInitial()
        try writer.writeBattery(UIDevice.current.batteryLevel)
        try writer.writeNetwork(NetworkMonitor.shared.currentCode)

        // 持续监听：battery / network 变化时重写对应文件。
        // 注意：H5 此次加载已读到首次写入的值，下次 reload 才会读到新值；
        // 业务期间的实时变化由 §3.2 ［2］［3］反向 callback 推送（Phase 2 实现）。
        NotificationCenter.default.addObserver(
            forName: UIDevice.batteryLevelDidChangeNotification,
            object: nil,
            queue: .main
        ) { _ in
            try? writer.writeBattery(UIDevice.current.batteryLevel)
        }

        NetworkMonitor.shared.onChange = { code in
            try? writer.writeNetwork(code)
        }
    }

    // MARK: - Alerts (terminal / retry states)

    private var appDisplayName: String {
        (Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String)
            ?? (Bundle.main.infoDictionary?["CFBundleName"] as? String)
            ?? "提示"
    }

    private func showBlockingAlert(message: String) {
        let alert = UIAlertController(
            title: "\(appDisplayName) 提醒",
            message: message,
            preferredStyle: .alert
        )
        alert.addAction(UIAlertAction(title: "确定", style: .default))
        present(alert, animated: true)
    }

    private func showIPAUpgradeAlert(downloadURL: String) {
        let alert = UIAlertController(
            title: "需要升级",
            message: "检测到新版本，请前往下载安装。",
            preferredStyle: .alert
        )
        alert.addAction(UIAlertAction(title: "确定", style: .default) { _ in
            if let url = URL(string: downloadURL) {
                UIApplication.shared.open(url)
            }
        })
        present(alert, animated: true)
    }

    private func showFatalAlert(error: Error) {
        let alert = UIAlertController(
            title: "启动失败",
            message: "\(error)",
            preferredStyle: .alert
        )
        alert.addAction(UIAlertAction(title: "重试", style: .default) { [weak self] _ in
            Task { @MainActor in
                await self?.runBootPipeline()
            }
        })
        present(alert, animated: true)
    }
}

// MARK: - WKNavigationDelegate

extension WebContainerViewController: WKNavigationDelegate {
    public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        // H5 大厅已经渲染完毕，此时把 splash 从 0.3s 内淡出再 removeFromSuperview，
        // 避免黑屏闪烁也避免后续 superview 再持有这层不必要的视图。
        UIView.animate(withDuration: 0.3, animations: { [weak self] in
            self?.splash.alpha = 0
        }, completion: { [weak self] _ in
            self?.splash.removeFromSuperview()
        })
    }
}

// MARK: - SplashOverlay
//
// 与 LaunchScreen 同款启动图 + UIProgressView + 状态文字标签的复合 view。
// 单文件私有：仅 WebContainerViewController 使用；若 Phase 6 子游戏容器复用，再抽出。

private final class SplashOverlay: UIView {

    private let imageView = UIImageView()
    private let progressView = UIProgressView(progressViewStyle: .default)
    private let label = UILabel()

    init() {
        super.init(frame: .zero)
        backgroundColor = .black

        // 与 LaunchScreen.storyboard 同款 contentMode：素材是 1136×640 横屏画面，
        // 在更宽的现代横屏设备上用 scaleAspectFit 左右补黑边（黑底统一），
        // 避免 scaleAspectFill 把 logo 上下裁切；同时确保 LaunchScreen → WebContainer
        // 视觉过渡时不出现裁切方式跳变。
        imageView.contentMode = .scaleAspectFit
        imageView.image = UIImage(named: "SplashImage")
        imageView.clipsToBounds = true
        imageView.translatesAutoresizingMaskIntoConstraints = false
        addSubview(imageView)

        label.textColor = .white
        label.font = .systemFont(ofSize: 14, weight: .medium)
        label.textAlignment = .center
        label.numberOfLines = 1
        label.translatesAutoresizingMaskIntoConstraints = false
        addSubview(label)

        progressView.translatesAutoresizingMaskIntoConstraints = false
        progressView.isHidden = true
        addSubview(progressView)

        NSLayoutConstraint.activate([
            imageView.topAnchor.constraint(equalTo: topAnchor),
            imageView.bottomAnchor.constraint(equalTo: bottomAnchor),
            imageView.leadingAnchor.constraint(equalTo: leadingAnchor),
            imageView.trailingAnchor.constraint(equalTo: trailingAnchor),

            label.centerXAnchor.constraint(equalTo: centerXAnchor),
            label.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -32),
            label.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: 24),
            label.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -24),

            progressView.centerXAnchor.constraint(equalTo: centerXAnchor),
            progressView.bottomAnchor.constraint(equalTo: label.topAnchor, constant: -12),
            progressView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.45)
        ])
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) { fatalError("SplashOverlay does not support init(coder:)") }

    func update(text: String, progress: Double?) {
        label.text = text
        if let p = progress {
            progressView.isHidden = false
            progressView.setProgress(Float(p), animated: true)
        } else {
            progressView.isHidden = true
            progressView.setProgress(0, animated: false)
        }
    }
}
