//
//  LocationService.swift
//  ylgamehall
//
//  高德 AMapLocationManager 异步包装：一次性 / 持续两种模式。
//  契约 §3.2 ［6］ getlocationinfo 9 字段；msext gameController.m:2491-2557 等价。
//
//  ⚠️ canImport(AMapLocationKit) 守卫：SDK 未链接时 LocationService 永远抛
//  LocationError.sdkNotLinked，handler 兜底退化为 stub 行为。
//

import Foundation
import CoreLocation
import os.log

/// 定位诊断日志（文件级 let，Logger 是 Sendable，便于在 AMap 回调闭包内直接用）。
private let locLog = Logger(subsystem: "ylgamehall", category: "Location")

#if canImport(AMapLocationKit)
import AMapLocationKit
#endif
#if canImport(AMapFoundationKit)
import AMapFoundationKit
#endif

/// 9 字段定位结果。字段名严格 1:1 H5 端契约（contract §3.2 表 [6]）：
///   - latitude / longitude **是 string** (`String(format: "%f", ...)`)
///   - province **小写 p**（与 sharelogin.Province 大写不同）
public struct LocationPayload: Sendable {
    public let address:   String
    public let city:      String
    public let cityCode:  String
    public let country:   String
    public let district:  String
    /// ⚠️ **JSON 数字**（不是字符串），且保留 `%f` 的 6 位小数文本。
    /// 用 `Decimal` 而不是 `Double`，见下方 init 注释与契约 §3.2 表 B。
    public let latitude:  Decimal
    public let longitude: Decimal
    public let province:  String
    public let street:    String
}

#if canImport(AMapLocationKit)
extension LocationPayload {
    /// 严格照搬 msext 的 `@{...}` 字面量语义。
    ///
    /// msext：
    /// ```objc
    /// @try{ [_bridge callHandler:@"getlocationinfo" data:@{
    ///     @"address":regeocode.formattedAddress, @"city":regeocode.city, ... }]; }
    /// @catch (NSException * e) { NSLog(...); }
    /// ```
    /// `@{}` 字面量里**任何一个 value 为 nil 都会抛 NSException**（这正是它包
    /// `@try/@catch` 的原因），净效果是：只要 9 个字段里有一个是 nil，
    /// **整条 getlocationinfo 都不会发给 H5**。
    ///
    /// 早前我们对每个字段做 `?? ""` 兜底，于是会发出一个带空串的包 —— 数据格式
    /// 与原工程不一致（H5 会收到 msext 里根本收不到的消息）。这里改为任一字段
    /// 为 nil 就返回 nil，由调用方跳过发送。
    nonisolated init?(location: CLLocation, reGeocode: AMapLocationReGeocode) {
        guard let address  = reGeocode.formattedAddress,
              let city     = reGeocode.city,
              let cityCode = reGeocode.citycode,
              let country  = reGeocode.country,
              let district = reGeocode.district,
              let province = reGeocode.province,
              let street   = reGeocode.street
        else { return nil }

        self.address   = address
        self.city      = city
        self.cityCode  = cityCode
        self.country   = country
        self.district  = district
        // 经纬度是 **数字**，保留 `%f` 的 6 位小数精度。
        //
        // ⚠️ 契约影响（docs/H5-Native-Contract.md §3.2 表 B）：msext 的 WVJB 路径
        // （`gameController.m:2528` / `NewRootVC.m:2132`）用
        // `[NSString stringWithFormat:@"%f", ...]` 把经纬度发成**字符串**；但老
        // UIWebView / JSContext 路径（`RootVC.m:1998` / `fourviewVC.m:1541`）拼的是
        // `\"latitude\":%f` —— **不带引号，是 JSON 数字**。现网子游戏 H5
        // （jinxianmahjong 05_Func.js:2029-2031）自造定位对象时也写
        // `"latitude":28.623546` 数字形式。与 `errorCode` 是同一类问题：WVJB 路径
        // 当年把它字符串化了。按项目方决定改回数字。
        //
        // 用 `%f` 文本构造 `Decimal`：既保住 6 位小数的舍入行为，又让
        // JSONSerialization 输出 `28.636486` 而不是 Double 的 17 位有效数字
        // `28.636486000000001`（两者 JS parse 后是同一个 IEEE754 double，H5 不可分辨，
        // 但 JSON 文本要与原工程一致）。
        let latText = String(format: "%f", location.coordinate.latitude)
        let lonText = String(format: "%f", location.coordinate.longitude)
        self.latitude  = Decimal(string: latText) ?? Decimal(location.coordinate.latitude)
        self.longitude = Decimal(string: lonText) ?? Decimal(location.coordinate.longitude)
        self.province  = province
        self.street    = street
    }
}
#endif

/// 一次定位请求可能产出的事件。**一次请求可以产出两条**（先 failure 再 success），
/// 这是 msext `completionBlock` 的原始控制流，不是笔误 —— 详见 requestOnce 注释。
public enum LocationOutcome: Sendable {
    /// → H5 `getlocationinfo({errorCode:12, errorMsg:"缺少定位权限"})`
    case failure
    /// → H5 `getlocationinfo(<9 字段>)`
    case success(LocationPayload)
}

@MainActor
public final class LocationService {

    /// 容器标识（`lobby` / `subGame`），仅用于诊断日志。
    private let owner: String

    /// **每个容器必须持有自己的实例**（不要做成单例）。
    /// msext `NewRootVC.m:1568` / `gameController.m:1230` 各自
    /// `[[AMapLocationManager alloc] init]`，两套 manager + completionBlock +
    /// delegate 完全独立。共用一个 manager 会引入三个原生侧故障：
    ///   1. `requestLocationWithReGeocode:completionBlock:` 同一时刻只保留一个
    ///      单次定位请求，两个容器并发请求会互相顶掉，被顶掉的一侧
    ///      completionBlock 永不触发 → continuation 永挂 → H5 什么都收不到；
    ///   2. `stopUpdatingLocation`「会 cancel 掉所有的单次定位请求」，子游戏
    ///      `backgameData` 会连带取消大厅在飞的定位；
    ///   3. `delegate = nil` 作用在共享 manager 上是**进程级永久**副作用。
    public init(owner: String = "?") {
        self.owner = owner
        #if canImport(AMapLocationKit)
        // msext `configLocationManager` 里 `setDelegate:self` 是**无条件**装上的，
        // 不只服务持续定位：`amapLocationManager:doRequireLocationAuth:` 也走 delegate。
        // 所以这里在 init 就把 shim 挂上，而不是等持续定位分支才 lazy 触发。
        _ = delegateShim
        #endif
    }

    #if canImport(AMapLocationKit)
    /// msext `configLocationManager` 逐项等价。
    private let manager: AMapLocationManager = {
        let m = AMapLocationManager()
        m.desiredAccuracy = kCLLocationAccuracyHundredMeters
        m.pausesLocationUpdatesAutomatically = false
        m.locationTimeout = 6
        m.reGeocodeTimeout = 3
        m.locatingWithReGeocode = true
        return m
    }()

    /// 持续定位（`startlocation` type == 1）的 delegate 承接器。
    /// msext `configLocationManager` 里 `setDelegate:self` 由 VC 自己承担，
    /// 这里用独立对象持有，避免 LocationService 被迫继承 NSObject。
    private lazy var delegateShim: LocationDelegateShim = {
        let shim = LocationDelegateShim(owner: owner)
        manager.delegate = shim
        return shim
    }()
    #endif

    /// 诊断用：requestOnce 调用序号，日志里可对上「谁发起、谁回来」。
    private var requestSeq: UInt64 = 0

    /// 一次性定位 + 逆地理。**逐行等价 msext `initCompleteBlock` 的 completionBlock**
    /// （`gameController.m:2493-2545` / `NewRootVC.m:2099-2150`）：
    ///
    /// ```objc
    /// if (error) {
    ///     callHandler getlocationinfo {errorCode:12,...}        // ①
    ///     if (error.code == AMapLocationErrorLocateFailed) return;
    /// }
    /// if (location) { if (regeocode) {
    ///     if (regeocode.formattedAddress != nil)
    ///         callHandler getlocationinfo <9 字段>              // ②
    /// }}
    /// ```
    ///
    /// 注意 ① 之后**没有 return**（除 locateFailed）：高德在「拿到了 CLLocation 但
    /// 逆地理超时/网络出错」时会带着 error **和** location 一起回调，msext 会先发一条
    /// errorCode 12、再把真实定位补发出去，H5 最终拿到的是真实定位。
    ///
    /// 早前的实现用 `async throws -> LocationPayload` 表达，单个返回值天然只能二选一，
    /// 于是在 error 分支直接 return，②永远不会发 —— H5 只收到 errorCode 12，
    /// `C_Player.SetLocationInfo({errorCode:12})` 还会把已有的 addr 覆盖掉。子游戏
    /// 进房瞬间网络/CPU 最忙、逆地理最容易超时，所以这个分叉在子游戏侧暴露得多。
    /// 因此改成回调式：一次请求可以按 msext 的顺序产出两条 outcome。
    ///
    /// - Parameter emit: 在 MainActor 上调用，可能被调用 0 / 1 / 2 次。
    public func requestOnce(caller: String = "?",
                            emit: @escaping @MainActor (LocationOutcome) -> Void) {
        #if canImport(AMapLocationKit)
        requestSeq += 1
        let seq = requestSeq
        diagLog("[\(caller)] requestOnce #\(seq) 发起（owner=\(owner) manager=\(UInt(bitPattern: ObjectIdentifier(self.manager).hashValue))）")
        let t0 = Date()
        // 高德的 completionBlock 在主线程回调；这里保持与 msext 同样的同步语义。
        let accepted = manager.requestLocation(withReGeocode: true) { loc, regeo, err in
            let ms = Int(Date().timeIntervalSince(t0) * 1000)
            diagLog("[\(caller)] requestOnce #\(seq) completionBlock 回来（耗时 \(ms)ms）：loc=\(loc != nil) regeo=\(regeo != nil) err=\(err?.localizedDescription ?? "nil")")

            MainActor.assumeIsolated {
                if let err = err as NSError? {
                    emit(.failure)                                          // ①
                    if err.code == AMapLocationErrorCode.locateFailed.rawValue {
                        diagLog("[\(caller)] requestOnce #\(seq) locateFailed，按 msext 就此结束")
                        return
                    }
                    diagLog("[\(caller)] requestOnce #\(seq) 有 error 但非 locateFailed，按 msext 继续尝试补发真实定位")
                }
                // msext 要求 location / regeocode / formattedAddress 三者都在才发 ②
                guard let loc, let regeo, regeo.formattedAddress != nil else {
                    diagLog("[\(caller)] requestOnce #\(seq) 无可用逆地理（loc=\(loc != nil) regeo=\(regeo != nil)），按 msext 不发 9 字段")
                    return
                }
                guard let payload = LocationPayload(location: loc, reGeocode: regeo) else {
                    diagLog("[\(caller)] requestOnce #\(seq) 逆地理有 nil 字段，按 msext（@{} 抛 NSException）整条不发")
                    return
                }
                emit(.success(payload))                                     // ②
            }
        }
        // AMapLocationManager.h：返回值是「是否成功添加单次定位Request」。msext 忽略它，
        // 效果就是什么都不发给 H5；回调式实现天然等价（不再有 continuation 可挂死），
        // 这里只留一条日志便于排查。
        if !accepted {
            diagLog("[\(caller)] requestOnce #\(seq) requestLocation 返回 NO（未挂上请求），按 msext 静默")
        }
        #else
        diagLog("[\(caller)] requestOnce: AMapLocationKit 未链接，按 msext 无 SDK 场景不回 H5")
        #endif
    }

    /// 持续定位（`startlocation` type == 1）。
    /// msext `NewRootVC.m:414` / `gameController.m:537`：
    /// `if (tempinfo == 1) [self.locationManager startUpdatingLocation];`
    /// 结果通过 delegate `amapLocationManager:didUpdateLocation:reGeocode:` 逐次推
    /// `getlocationinfo`（msext `gameController.m:2551-2560`），只在 `reGeocode` 与
    /// `formattedAddress` 都非空时发；`didFailWithError` msext 只 NSLog、不通知 H5。
    ///
    /// - Parameter emit: 每次定位更新都会调用（不是一次性）。
    public func startContinuous(caller: String = "?",
                                emit: @escaping @MainActor (LocationOutcome) -> Void) {
        #if canImport(AMapLocationKit)
        diagLog("[\(caller)] startContinuous 发起（owner=\(owner)）")
        delegateShim.onUpdate = emit
        delegateShim.caller = caller
        manager.startUpdatingLocation()
        #else
        diagLog("[\(caller)] startContinuous: AMapLocationKit 未链接，按 msext 无 SDK 场景不回 H5")
        #endif
    }

    /// 停止：msext gameController.m:2473-2478 cleanUpAction 等价
    public func stop(caller: String = "?") {
        #if canImport(AMapLocationKit)
        // `stopUpdatingLocation` 会 cancel 掉本 manager 上所有单次定位请求。因为
        // manager 是本容器私有的（每容器一个实例），影响范围仅限本容器 —— 与 msext
        // cleanUpAction 只动自己那个 manager 一致。
        diagLog("[\(caller)] stop()：stopUpdatingLocation + delegate=nil（owner=\(owner)）")
        manager.stopUpdatingLocation()
        manager.delegate = nil
        #endif
    }
}

#if canImport(AMapLocationKit)

/// `AMapLocationManagerDelegate` 承接器（仅持续定位路径用到）。
/// msext 里这些方法直接实现在 NewRootVC / gameController 上；新外壳把它独立出来，
/// 每个 LocationService 一个，生命周期随宿主容器。
@MainActor
private final class LocationDelegateShim: NSObject, AMapLocationManagerDelegate {

    var onUpdate: (@MainActor (LocationOutcome) -> Void)?
    var caller: String = "?"
    private let owner: String

    init(owner: String) {
        self.owner = owner
        super.init()
    }

    /// msext `gameController.m:2551-2560` 等价：仅 reGeocode 与 formattedAddress
    /// 都非空时推 9 字段，其余情况什么都不发。
    nonisolated func amapLocationManager(_ manager: AMapLocationManager!,
                                         didUpdate location: CLLocation!,
                                         reGeocode: AMapLocationReGeocode!) {
        // 先在当前上下文把 AMap 的非 Sendable 对象拆成纯值，只把 Sendable 的
        // LocationPayload 送进 MainActor（否则 Swift 6 判定 reGeocode 跨隔离域有数据竞争）。
        guard let location, let reGeocode, reGeocode.formattedAddress != nil else {
            diagLog("[持续定位] 更新但无可用逆地理，按 msext 不发")
            return
        }
        guard let payload = LocationPayload(location: location, reGeocode: reGeocode) else {
            diagLog("[持续定位] 逆地理有 nil 字段，按 msext（@{} 抛 NSException）整条不发")
            return
        }
        MainActor.assumeIsolated {
            diagLog("[\(caller)] 持续定位 → getlocationinfo city=\(payload.city)")
            onUpdate?(.success(payload))
        }
    }

    /// msext `gameController.m:2547` 等价：**只打日志，不通知 H5**。
    nonisolated func amapLocationManager(_ manager: AMapLocationManager!,
                                         didFailWithError error: (any Error)!) {
        diagLog("[持续定位 owner=\(owner)] didFailWithError: \(error?.localizedDescription ?? "nil")（msext 同款：不通知 H5）")
    }
}

#endif
