//
//  RemoteConfigClient.swift
//  ylgamehall
//
//  从 BundleConfig.gameConfig 拼出的远端 .txt（实为 JSON）拉取渠道升级配置。
//  详见 docs/H5-Native-Implementation-Design.md §6.3.1 / Plan ADR-008。
//

import Foundation

// MARK: - Codable 模型（单链 4 层嵌套：agent → game → channel → market）

/// 远端配置顶层。所有字段（appVersion / appDownload / gameVersion / gameZip /
/// showmessage / audioDomain / audioBucket）都可在顶层声明 ——作为节点链最浅一层
/// 参与 4 层 fallback 兜底，与 Agent/Game/Channel/Market 同款语义。
nonisolated public struct RemoteConfig: Codable, Sendable {
    public let appVersion: String?
    public let appDownload: String?
    public let gameVersion: String?
    public let gameZip: String?
    public let showmessage: String?
    public let audioDomain: String?
    public let audioBucket: String?
    public let agentlist: [Agent]?

    public init(from decoder: any Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        appVersion  = try c.decodeFlexibleStringIfPresent(forKey: .appVersion)
        appDownload = try c.decodeFlexibleStringIfPresent(forKey: .appDownload)
        gameVersion = try c.decodeFlexibleStringIfPresent(forKey: .gameVersion)
        gameZip     = try c.decodeFlexibleStringIfPresent(forKey: .gameZip)
        showmessage = try c.decodeFlexibleStringIfPresent(forKey: .showmessage)
        audioDomain = try c.decodeFlexibleStringIfPresent(forKey: .audioDomain)
        audioBucket = try c.decodeFlexibleStringIfPresent(forKey: .audioBucket)
        agentlist   = try c.decodeIfPresent([Agent].self, forKey: .agentlist)
    }

    private enum CodingKeys: String, CodingKey {
        // 驼峰 case 名（不带 rawValue），依赖 JSONDecoder.convertFromSnakeCase 自动把
        // JSON 的 app_version / audio_domain 等归一化为 appVersion / audioDomain
        case showmessage, agentlist
        case appVersion, appDownload, gameVersion, gameZip
        case audioDomain, audioBucket
    }
}

nonisolated public struct Agent: Codable, Sendable {
    public let agentid: String?
    public let showmessage: String?
    public let appVersion: String?
    public let appDownload: String?
    public let gameVersion: String?
    public let gameZip: String?
    public let audioDomain: String?
    public let audioBucket: String?
    public let gamelist: [Game]?

    public init(from decoder: any Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        agentid     = try c.decodeFlexibleStringIfPresent(forKey: .agentid)
        showmessage = try c.decodeFlexibleStringIfPresent(forKey: .showmessage)
        appVersion  = try c.decodeFlexibleStringIfPresent(forKey: .appVersion)
        appDownload = try c.decodeFlexibleStringIfPresent(forKey: .appDownload)
        gameVersion = try c.decodeFlexibleStringIfPresent(forKey: .gameVersion)
        gameZip     = try c.decodeFlexibleStringIfPresent(forKey: .gameZip)
        audioDomain = try c.decodeFlexibleStringIfPresent(forKey: .audioDomain)
        audioBucket = try c.decodeFlexibleStringIfPresent(forKey: .audioBucket)
        gamelist    = try c.decodeIfPresent([Game].self, forKey: .gamelist)
    }

    private enum CodingKeys: String, CodingKey {
        case agentid, showmessage, gamelist
        case appVersion, appDownload, gameVersion, gameZip
        case audioDomain, audioBucket
    }
}

nonisolated public struct Channel: Codable, Sendable {
    public let channelid: String?
    public let showmessage: String?
    public let appVersion: String?
    public let appDownload: String?
    public let gameVersion: String?
    public let gameZip: String?
    public let audioDomain: String?
    public let audioBucket: String?
    public let marketlist: [Market]?

    public init(from decoder: any Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        channelid   = try c.decodeFlexibleStringIfPresent(forKey: .channelid)
        showmessage = try c.decodeFlexibleStringIfPresent(forKey: .showmessage)
        appVersion  = try c.decodeFlexibleStringIfPresent(forKey: .appVersion)
        appDownload = try c.decodeFlexibleStringIfPresent(forKey: .appDownload)
        gameVersion = try c.decodeFlexibleStringIfPresent(forKey: .gameVersion)
        gameZip     = try c.decodeFlexibleStringIfPresent(forKey: .gameZip)
        audioDomain = try c.decodeFlexibleStringIfPresent(forKey: .audioDomain)
        audioBucket = try c.decodeFlexibleStringIfPresent(forKey: .audioBucket)
        marketlist  = try c.decodeIfPresent([Market].self, forKey: .marketlist)
    }

    private enum CodingKeys: String, CodingKey {
        case channelid, showmessage, marketlist
        case appVersion, appDownload, gameVersion, gameZip
        case audioDomain, audioBucket
    }
}

nonisolated public struct Market: Codable, Sendable {
    public let marketid: String?
    public let showmessage: String?
    public let appVersion: String?
    public let appDownload: String?
    public let gameVersion: String?
    public let gameZip: String?
    public let audioDomain: String?
    public let audioBucket: String?

    public init(from decoder: any Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        marketid    = try c.decodeFlexibleStringIfPresent(forKey: .marketid)
        showmessage = try c.decodeFlexibleStringIfPresent(forKey: .showmessage)
        appVersion  = try c.decodeFlexibleStringIfPresent(forKey: .appVersion)
        appDownload = try c.decodeFlexibleStringIfPresent(forKey: .appDownload)
        gameVersion = try c.decodeFlexibleStringIfPresent(forKey: .gameVersion)
        gameZip     = try c.decodeFlexibleStringIfPresent(forKey: .gameZip)
        audioDomain = try c.decodeFlexibleStringIfPresent(forKey: .audioDomain)
        audioBucket = try c.decodeFlexibleStringIfPresent(forKey: .audioBucket)
    }

    private enum CodingKeys: String, CodingKey {
        case marketid, showmessage
        case appVersion, appDownload, gameVersion, gameZip
        case audioDomain, audioBucket
    }
}

nonisolated public struct Game: Codable, Sendable {
    public let gameid: String?
    public let showmessage: String?
    public let appVersion: String?
    public let appDownload: String?
    public let gameVersion: String?
    public let gameZip: String?
    public let audioDomain: String?
    public let audioBucket: String?
    public let channellist: [Channel]?

    public init(from decoder: any Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        gameid      = try c.decodeFlexibleStringIfPresent(forKey: .gameid)
        showmessage = try c.decodeFlexibleStringIfPresent(forKey: .showmessage)
        appVersion  = try c.decodeFlexibleStringIfPresent(forKey: .appVersion)
        appDownload = try c.decodeFlexibleStringIfPresent(forKey: .appDownload)
        gameVersion = try c.decodeFlexibleStringIfPresent(forKey: .gameVersion)
        gameZip     = try c.decodeFlexibleStringIfPresent(forKey: .gameZip)
        audioDomain = try c.decodeFlexibleStringIfPresent(forKey: .audioDomain)
        audioBucket = try c.decodeFlexibleStringIfPresent(forKey: .audioBucket)
        channellist = try c.decodeIfPresent([Channel].self, forKey: .channellist)
    }

    private enum CodingKeys: String, CodingKey {
        case gameid, showmessage, channellist
        case appVersion, appDownload, gameVersion, gameZip
        case audioDomain, audioBucket
    }
}

/// `KeyedDecodingContainer` 扩展：宽松解 String —— 服务端历史包袱让 app_version/game_version
/// 等字段可能以 String / Int / Double 任一形式回，msext 用 NSString 自动吞了；我们 Codable
/// 严格校验下需要自己兼容。Bool 兜底是防御性的（极不可能但 0 成本）。
nonisolated extension KeyedDecodingContainer {
    func decodeFlexibleStringIfPresent(forKey key: Key) throws -> String? {
        // key 不存在直接返回 nil（避免 decodeNil(forKey:) 在 key 缺失时 throw keyNotFound）
        guard contains(key) else { return nil }
        if (try? decodeNil(forKey: key)) == true { return nil }
        if let s = try? decode(String.self, forKey: key) { return s }
        if let i = try? decode(Int.self, forKey: key) { return String(i) }
        if let d = try? decode(Double.self, forKey: key) {
            // 整数值的 Double 去掉末尾 .0（如 "43.0" → "43"）
            return d.truncatingRemainder(dividingBy: 1) == 0
                ? String(Int(d))
                : String(d)
        }
        if let b = try? decode(Bool.self, forKey: key) { return String(b) }
        return nil
    }
}

// MARK: - 拉取结果

/// 远端配置拉取的两种成功 outcome（msext `NewRootVC.m:1239-1244` 契约）。
public enum RemoteConfigOutcome: Sendable {
    /// 正常长文本响应，JSON 反序列化成功
    case parsed(RemoteConfig)
    /// 短文本响应（trim 后 utf16.count ≤ 100）—— 服务端运营杀手锏 #1：
    /// 配置异常时返回一段错误文本，客户端当弹窗内容直接显示并停止重试。
    /// 详见 ADR-008-B / msext NewRootVC.m:1239-1243。
    case shortText(String)
}

// MARK: - 错误

public enum RemoteConfigError: Error, Sendable {
    /// `BundleConfig.gameConfig` 为空，无法构造 URL
    case urlConstructionFailed
    /// HTTP 非 2xx
    case httpStatus(Int)
    /// JSON 反序列化失败
    case decodingFailed(any Error)
    /// 全部重试用尽
    case allRetriesFailed(underlying: any Error)
}

// MARK: - 客户端

/// 远端渠道配置拉取器。actor 隔离，URLSession async + 指数退避重试。
public actor RemoteConfigClient {

    public static let shared = RemoteConfigClient()

    private let session: URLSession
    private let urlBuilder: @Sendable () -> URL?
    private let maxRetries: Int

    /// 最近一次 `.parsed(...)` 成功的配置。子游戏跳转链路（SubGameViewController）需要按
    /// H5 传的 gameid 重新跑一次 VersionResolver 拿真实 zip URL —— 没缓存就要再发请求。
    /// `.shortText` 与失败路径不更新此值，保持上一份"已知好"配置可用。
    private var lastParsed: RemoteConfig?

    public init(
        session: URLSession = RemoteConfigClient.defaultSession,
        urlBuilder: @escaping @Sendable () -> URL? = RemoteConfigClient.defaultURLBuilder,
        maxRetries: Int = 3
    ) {
        self.session = session
        self.urlBuilder = urlBuilder
        self.maxRetries = maxRetries
    }

    /// 启动期 fetch 成功后保留的 RemoteConfig；nil 表示从未拉到过。
    public func current() -> RemoteConfig? {
        lastParsed
    }

    /// 默认 URLSession：每次请求 10 s 超时。
    nonisolated public static var defaultSession: URLSession {
        let cfg = URLSessionConfiguration.default
        cfg.timeoutIntervalForRequest = 10
        cfg.timeoutIntervalForResource = 30
        return URLSession(configuration: cfg)
    }

    /// 默认 URL 构造：`https://` + `BundleConfig.gameConfig.replacingOccurrences("-", "/")` + `.txt`。
    /// 当前 demo 渠道注入值 → https://tsgames.daoqi88.cn/config_test/update_jsonv2_test.txt
    ///
    /// 与 msext `NewRootVC.m:250` 用 http 的历史相比，本项目后台已切 HTTPS，
    /// 故无需 NSAllowsArbitraryLoads 等 ATS 例外。不带 SERVERNew 前缀的拼装规则保留。
    nonisolated public static let defaultURLBuilder: @Sendable () -> URL? = {
        let gameConfig = BundleConfig.shared.gameConfig
        guard !gameConfig.isEmpty else { return nil }
        let path = gameConfig.replacingOccurrences(of: "-", with: "/")
        return URL(string: "https://\(path).txt")
    }

    /// 拉远端配置。指数退避 1/2/4 秒最多 maxRetries 次。
    ///
    /// - Returns: `.parsed(RemoteConfig)` 长文本 JSON 成功，或 `.shortText(String)`
    ///   服务端运营截短响应（trim 后 utf16.count ≤ 100，msext 契约）
    public func fetch() async throws -> RemoteConfigOutcome {
        guard let baseURL = urlBuilder() else {
            throw RemoteConfigError.urlConstructionFailed
        }

        var lastError: any Error = RemoteConfigError.urlConstructionFailed
        for attempt in 0..<maxRetries {
            do {
                // 每次重试都生成新 cache-busting query，避免 CDN / Proxy 复用旧响应
                // 格式：?vXXXXXXXXYYYYYYYY（两段 8 位 hex，无 `=`，msext NewRootVC.m:1226 契约）
                let url = appendCacheBuster(to: baseURL)
                return try await fetchOnce(url: url)
            } catch {
                lastError = error
                if attempt < maxRetries - 1 {
                    let delaySeconds = pow(2.0, Double(attempt))
                    try? await Task.sleep(nanoseconds: UInt64(delaySeconds * 1_000_000_000))
                }
            }
        }
        throw RemoteConfigError.allRetriesFailed(underlying: lastError)
    }

    private func fetchOnce(url: URL) async throws -> RemoteConfigOutcome {
        let (data, response) = try await session.data(from: url)
        if let http = response as? HTTPURLResponse,
           !(200..<300).contains(http.statusCode) {
            throw RemoteConfigError.httpStatus(http.statusCode)
        }
        // UTF-8 解码 + trim（msext NewRootVC.m:1237-1238 契约）
        guard let raw = String(data: data, encoding: .utf8) else {
            throw RemoteConfigError.decodingFailed(
                NSError(domain: "RemoteConfigClient", code: -1,
                        userInfo: [NSLocalizedDescriptionKey: "response is not UTF-8"])
            )
        }
        let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)

        // 短文本响应（≤100 utf16 unit）：服务端运营截短响应，当 alert 文本用
        // msext NewRootVC.m:1239-1243 用 NSString length（即 UTF-16 unit count），新外壳用 utf16.count 1:1
        if trimmed.utf16.count <= 100 {
            return .shortText(trimmed)
        }

        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .convertFromSnakeCase
        do {
            let parsed = try decoder.decode(RemoteConfig.self, from: data)
            lastParsed = parsed
            return .parsed(parsed)
        } catch {
            throw RemoteConfigError.decodingFailed(error)
        }
    }

    /// 给 URL 追加 cache-busting query `?vXXXXXXXXYYYYYYYY`（msext NewRootVC.m:1226）。
    /// 注意：不是 `?v=XXXX` 标准 query 而是裸 `?v<16hex>`，照搬契约。
    nonisolated private func appendCacheBuster(to url: URL) -> URL {
        let token = String(format: "%08X%08X",
                           UInt32.random(in: 0...UInt32.max),
                           UInt32.random(in: 0...UInt32.max))
        // URLComponents 会把 query 编码，这里手动拼字符串
        let absolute = url.absoluteString
        let separator = absolute.contains("?") ? "&" : "?"
        return URL(string: "\(absolute)\(separator)v\(token)") ?? url
    }
}
