// REST 客户端（14B/14E）：URLSession async/await。
// DTO 与 server/pkg/protocol/dto.go 逐字段对齐（snake_case 显式 CodingKeys，不用全局策略，
// 避免 map 键被转换的隐患）；错误体对齐 errors.go APIError {code, message}。
// 路由对齐 server/internal/httpapi/router.go：
//   POST /v1/auth/wechat（移动端 code 换 token）· POST /v1/auth/logout · GET /v1/me
//   GET /v1/packs · POST /v1/orders · GET /v1/orders/{id}
//   POST /v1/feedback（multipart）· POST /v1/metrics/batch
import Foundation

// MARK: - 环境配置（base URL 切换点）

public enum ApiConfig {
    /// 开发期默认本机后端（server 默认监听 :8080）。
    /// TODO(prod): 上线时切到生产域名（https 必须），并移除 App Info.plist 的 NSAllowsLocalNetworking。
    public static var baseURL = URL(string: "http://127.0.0.1:8080")!
}

/// 客户端选择器：DEBUG 构建可切离线 mock（无后端时演示全流程），Release 恒为真实网络层。
public enum Api {
    private static let mockKey = "debug.offline_mock"

    #if DEBUG
    /// 离线 mock 开关（主 App 调试卡里切，存 App Group 便于排查）
    public static var offlineMock: Bool {
        get { UserDefaults(suiteName: AccountStore.appGroupID)?.bool(forKey: mockKey) ?? false }
        set { UserDefaults(suiteName: AccountStore.appGroupID)?.set(newValue, forKey: mockKey) }
    }
    #endif

    public static var client: DuduApi {
        #if DEBUG
        if offlineMock { return MockApi.shared }
        #endif
        return ApiClient.shared
    }
}

// MARK: - DTO（对齐 dto.go）

public struct UserInfoDTO: Codable {
    public let userID: String
    public let nicknameMasked: String

    enum CodingKeys: String, CodingKey {
        case userID = "user_id"
        case nicknameMasked = "nickname_masked"
    }

    public init(userID: String, nicknameMasked: String) {
        self.userID = userID
        self.nicknameMasked = nicknameMasked
    }
}

public struct AuthTokenResponse: Codable {
    public let token: String
    public let user: UserInfoDTO

    public init(token: String, user: UserInfoDTO) {
        self.token = token
        self.user = user
    }
}

public struct MeResponse: Codable {
    public let userID: String
    public let nicknameMasked: String
    public let balanceSeconds: Int64
    public let trialDailyLimit: Int
    public let trialUsedToday: Int
    public let accountState: String // ok | trial | quota

    enum CodingKeys: String, CodingKey {
        case userID = "user_id"
        case nicknameMasked = "nickname_masked"
        case balanceSeconds = "balance_seconds"
        case trialDailyLimit = "trial_daily_limit"
        case trialUsedToday = "trial_used_today"
        case accountState = "account_state"
    }

    public init(userID: String, nicknameMasked: String, balanceSeconds: Int64,
                trialDailyLimit: Int, trialUsedToday: Int, accountState: String) {
        self.userID = userID
        self.nicknameMasked = nicknameMasked
        self.balanceSeconds = balanceSeconds
        self.trialDailyLimit = trialDailyLimit
        self.trialUsedToday = trialUsedToday
        self.accountState = accountState
    }
}

public struct Pack: Codable, Identifiable, Hashable {
    public let id: String
    public let minutes: Int
    public let priceCents: Int
    public let unitDesc: String
    public let tag: String?

    enum CodingKeys: String, CodingKey {
        case id, minutes, tag
        case priceCents = "price_cents"
        case unitDesc = "unit_desc"
    }

    public init(id: String, minutes: Int, priceCents: Int, unitDesc: String, tag: String?) {
        self.id = id
        self.minutes = minutes
        self.priceCents = priceCents
        self.unitDesc = unitDesc
        self.tag = tag
    }
}

public struct PacksResponse: Codable {
    public let packs: [Pack]
    public init(packs: [Pack]) { self.packs = packs }
}

public enum PayChannel {
    public static let native = "native" // 桌面扫码
    public static let app = "app"       // 移动收银台
}

public struct CreateOrderResponse: Codable {
    public let orderID: String
    public let codeURL: String?
    public let payParams: [String: JSONValue]?

    enum CodingKeys: String, CodingKey {
        case orderID = "order_id"
        case codeURL = "code_url"
        case payParams = "pay_params"
    }

    public init(orderID: String, codeURL: String?, payParams: [String: JSONValue]?) {
        self.orderID = orderID
        self.codeURL = codeURL
        self.payParams = payParams
    }
}

public enum OrderStatus {
    public static let pending = "pending"
    public static let paid = "paid"
    public static let closed = "closed"
}

public struct OrderStatusResponse: Codable {
    public let status: String // pending | paid | closed
    public let balanceSeconds: Int64?

    enum CodingKeys: String, CodingKey {
        case status
        case balanceSeconds = "balance_seconds"
    }

    public init(status: String, balanceSeconds: Int64?) {
        self.status = status
        self.balanceSeconds = balanceSeconds
    }
}

public struct FeedbackResponse: Codable {
    public let feedbackID: String

    enum CodingKeys: String, CodingKey {
        case feedbackID = "feedback_id"
    }

    public init(feedbackID: String) { self.feedbackID = feedbackID }
}

public struct MetricEvent: Codable {
    public let event: String
    public let props: [String: JSONValue]?
    public let clientTs: Int64

    enum CodingKeys: String, CodingKey {
        case event, props
        case clientTs = "client_ts"
    }

    public init(event: String, props: [String: JSONValue]?, clientTs: Int64) {
        self.event = event
        self.props = props
        self.clientTs = clientTs
    }
}

public struct MetricsBatchRequest: Codable {
    public let deviceID: String
    public let platform: String // ios
    public let appVersion: String
    public let osVersion: String
    public let events: [MetricEvent]

    enum CodingKeys: String, CodingKey {
        case deviceID = "device_id"
        case platform, events
        case appVersion = "app_version"
        case osVersion = "os_version"
    }

    public init(deviceID: String, platform: String, appVersion: String,
                osVersion: String, events: [MetricEvent]) {
        self.deviceID = deviceID
        self.platform = platform
        self.appVersion = appVersion
        self.osVersion = osVersion
        self.events = events
    }
}

/// REST 统一错误体（errors.go APIError），同时充当客户端侧错误类型。
public struct ApiError: Error, Codable {
    public let code: String
    public let message: String

    public init(code: String, message: String) {
        self.code = code
        self.message = message
    }

    // 对齐 errors.go 错误码常量（客户端需要分支处理的子集）
    public static let unauthorized = "UNAUTHORIZED"
    public static let quotaExceeded = "QUOTA_EXCEEDED"
    public static let feedbackRateLimited = "FEEDBACK_RATE_LIMITED"
}

/// 任意 JSON 值（pay_params map[string]any、打点 props 的 Swift 表达）。
public enum JSONValue: Codable, Hashable {
    case string(String)
    case number(Double)
    case bool(Bool)
    case null
    case array([JSONValue])
    case object([String: JSONValue])

    public init(from decoder: Decoder) throws {
        let c = try decoder.singleValueContainer()
        if c.decodeNil() {
            self = .null
        } else if let b = try? c.decode(Bool.self) {
            self = .bool(b)
        } else if let n = try? c.decode(Double.self) {
            self = .number(n)
        } else if let s = try? c.decode(String.self) {
            self = .string(s)
        } else if let a = try? c.decode([JSONValue].self) {
            self = .array(a)
        } else if let o = try? c.decode([String: JSONValue].self) {
            self = .object(o)
        } else {
            throw DecodingError.dataCorruptedError(in: c, debugDescription: "unsupported JSON value")
        }
    }

    public func encode(to encoder: Encoder) throws {
        var c = encoder.singleValueContainer()
        switch self {
        case .string(let s): try c.encode(s)
        case .number(let n): try c.encode(n)
        case .bool(let b): try c.encode(b)
        case .null: try c.encodeNil()
        case .array(let a): try c.encode(a)
        case .object(let o): try c.encode(o)
        }
    }
}

// MARK: - 设备信息（metrics 公共维度 + 反馈诊断信息）

public enum DeviceInfo {
    public static let platform = "ios"

    public static var appVersion: String {
        (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "0.0"
    }

    public static var osVersion: String {
        let v = ProcessInfo.processInfo.operatingSystemVersion
        return "iOS \(v.majorVersion).\(v.minorVersion).\(v.patchVersion)"
    }

    /// 机型标识（如 iPhone16,1），uname 取硬件名
    public static var model: String {
        var sys = utsname()
        uname(&sys)
        return Mirror(reflecting: sys.machine).children.reduce(into: "") { acc, child in
            if let v = child.value as? Int8, v != 0 {
                acc.append(Character(UnicodeScalar(UInt8(v))))
            }
        }
    }
}

// MARK: - 接口协议（真实实现 ApiClient / 离线演示 MockApi）

public protocol DuduApi {
    func wechatLogin(code: String) async throws -> AuthTokenResponse
    func logout(token: String) async throws
    func me(token: String) async throws -> MeResponse
    func packs() async throws -> PacksResponse
    func createOrder(token: String, packID: String, channel: String) async throws -> CreateOrderResponse
    func orderStatus(token: String, orderID: String) async throws -> OrderStatusResponse
    func submitFeedback(token: String, content: String, jpegImages: [Data],
                        diagnosticsJSON: String?) async throws -> FeedbackResponse
    func postMetrics(_ batch: MetricsBatchRequest) async throws
    /// 开发期专用：模拟微信支付回调（POST /v1/pay/notify 明文 JSON，对齐 billing.MockPay）。
    /// TODO(wechat-pay): 真实支付接入后删除，回调由微信服务端发起。
    func mockPayNotify(orderID: String, amountCents: Int) async throws
}

// MARK: - 真实实现

public final class ApiClient: DuduApi {
    public static let shared = ApiClient()

    private let session: URLSession
    private let encoder = JSONEncoder()
    private let decoder = JSONDecoder()

    public init() {
        let cfg = URLSessionConfiguration.ephemeral
        cfg.timeoutIntervalForRequest = 15
        session = URLSession(configuration: cfg)
    }

    public func wechatLogin(code: String) async throws -> AuthTokenResponse {
        try await send("POST", "/v1/auth/wechat", body: try encoder.encode(["code": code]))
    }

    public func logout(token: String) async throws {
        let _: EmptyResponse = try await send("POST", "/v1/auth/logout", token: token)
    }

    public func me(token: String) async throws -> MeResponse {
        try await send("GET", "/v1/me", token: token)
    }

    public func packs() async throws -> PacksResponse {
        try await send("GET", "/v1/packs")
    }

    public func createOrder(token: String, packID: String, channel: String) async throws -> CreateOrderResponse {
        let body = try encoder.encode(["pack_id": packID, "channel": channel])
        return try await send("POST", "/v1/orders", token: token, body: body)
    }

    public func orderStatus(token: String, orderID: String) async throws -> OrderStatusResponse {
        try await send("GET", "/v1/orders/\(orderID)", token: token)
    }

    public func submitFeedback(token: String, content: String, jpegImages: [Data],
                               diagnosticsJSON: String?) async throws -> FeedbackResponse {
        let boundary = "dudu-" + UUID().uuidString
        var body = Data()
        func appendField(_ name: String, _ value: String) {
            body.append(Data("--\(boundary)\r\nContent-Disposition: form-data; name=\"\(name)\"\r\n\r\n\(value)\r\n".utf8))
        }
        appendField("content", content)
        if let diagnosticsJSON {
            appendField("diagnostics", diagnosticsJSON)
        }
        for (i, data) in jpegImages.enumerated() {
            body.append(Data("--\(boundary)\r\nContent-Disposition: form-data; name=\"images[]\"; filename=\"\(i + 1).jpg\"\r\nContent-Type: image/jpeg\r\n\r\n".utf8))
            body.append(data)
            body.append(Data("\r\n".utf8))
        }
        body.append(Data("--\(boundary)--\r\n".utf8))
        return try await send("POST", "/v1/feedback", token: token, body: body,
                              contentType: "multipart/form-data; boundary=\(boundary)")
    }

    public func postMetrics(_ batch: MetricsBatchRequest) async throws {
        let _: EmptyResponse = try await send("POST", "/v1/metrics/batch", body: try encoder.encode(batch))
    }

    public func mockPayNotify(orderID: String, amountCents: Int) async throws {
        // billing.MockPay.ParseNotify 要求 {order_id, transaction_id, amount_cents}
        let payload: [String: JSONValue] = [
            "order_id": .string(orderID),
            "transaction_id": .string("mock-tx-" + UUID().uuidString),
            "amount_cents": .number(Double(amountCents)),
        ]
        let _: EmptyResponse = try await send("POST", "/v1/pay/notify", body: try encoder.encode(payload))
    }

    // MARK: 通用请求

    private struct EmptyResponse: Decodable {}

    private func send<T: Decodable>(_ method: String, _ path: String, token: String? = nil,
                                    body: Data? = nil,
                                    contentType: String = "application/json") async throws -> T {
        guard let url = URL(string: ApiConfig.baseURL.absoluteString + path) else {
            throw ApiError(code: "BAD_REQUEST", message: "无效地址")
        }
        var req = URLRequest(url: url)
        req.httpMethod = method
        // 反馈表落库会读这两个头（feedback.go X-Platform / X-App-Version），统一带上
        req.setValue(DeviceInfo.platform, forHTTPHeaderField: "X-Platform")
        req.setValue(DeviceInfo.appVersion, forHTTPHeaderField: "X-App-Version")
        if let token {
            req.setValue("Bearer " + token, forHTTPHeaderField: "Authorization")
        }
        if let body {
            req.httpBody = body
            req.setValue(contentType, forHTTPHeaderField: "Content-Type")
        }
        let (data, resp): (Data, URLResponse)
        do {
            (data, resp) = try await session.data(for: req)
        } catch {
            throw ApiError(code: "NETWORK", message: "网络不可用，稍后再试")
        }
        guard let http = resp as? HTTPURLResponse else {
            throw ApiError(code: "INTERNAL", message: "无效响应")
        }
        guard (200..<300).contains(http.statusCode) else {
            if let apiErr = try? decoder.decode(ApiError.self, from: data) {
                throw apiErr
            }
            throw ApiError(code: "HTTP_\(http.statusCode)", message: "请求失败")
        }
        if T.self == EmptyResponse.self, let empty = EmptyResponse() as? T {
            return empty // 204 / 空体（metrics、logout、pay/notify）
        }
        do {
            return try decoder.decode(T.self, from: data)
        } catch {
            // 兼容空体 200
            if let empty = try? decoder.decode(T.self, from: Data("{}".utf8)) {
                return empty
            }
            throw ApiError(code: "DECODE", message: "响应解析失败")
        }
    }
}

#if DEBUG
// MARK: - 离线 mock（无后端时演示登录 / 购买 / 反馈全流程，仅 DEBUG 构建）

public final class MockApi: DuduApi {
    public static let shared = MockApi()

    private var balanceSeconds: Int64
    private var orders: [String: (pack: Pack, created: Date)] = [:]

    /// 与 server seed 同款三档
    private let mockPacks = [
        Pack(id: "pack_100", minutes: 100, priceCents: 900, unitDesc: "约 ¥0.09 / 分钟", tag: nil),
        Pack(id: "pack_500", minutes: 500, priceCents: 3900, unitDesc: "约 ¥0.078 / 分钟", tag: "省 13%"),
        Pack(id: "pack_2000", minutes: 2000, priceCents: 12900, unitDesc: "约 ¥0.065 / 分钟", tag: "省 28%"),
    ]

    private init() {
        // 余额沿用 App Group 已存值，重启 App 后离线演示状态不丢
        let d = UserDefaults(suiteName: AccountStore.appGroupID)
        balanceSeconds = Int64(d?.integer(forKey: "account.balance_seconds") ?? 0)
    }

    public func wechatLogin(code: String) async throws -> AuthTokenResponse {
        try await Task.sleep(nanoseconds: 300_000_000)
        return AuthTokenResponse(token: "mock-jwt-offline",
                                 user: UserInfoDTO(userID: "u_mock", nicknameMasked: "wang***"))
    }

    public func logout(token: String) async throws {}

    public func me(token: String) async throws -> MeResponse {
        MeResponse(userID: "u_mock", nicknameMasked: "wang***",
                   balanceSeconds: balanceSeconds,
                   trialDailyLimit: DuduProtocol.trialDailySeconds, trialUsedToday: 60,
                   accountState: balanceSeconds > 0 ? "ok" : "trial")
    }

    public func packs() async throws -> PacksResponse {
        try await Task.sleep(nanoseconds: 200_000_000)
        return PacksResponse(packs: mockPacks)
    }

    public func createOrder(token: String, packID: String, channel: String) async throws -> CreateOrderResponse {
        guard let pack = mockPacks.first(where: { $0.id == packID }) else {
            throw ApiError(code: "BAD_REQUEST", message: "无效时长包")
        }
        let id = "o-mock-" + UUID().uuidString.prefix(8)
        orders[id] = (pack, Date())
        return CreateOrderResponse(orderID: id, codeURL: nil, payParams: ["mock": .bool(true)])
    }

    public func orderStatus(token: String, orderID: String) async throws -> OrderStatusResponse {
        guard let order = orders[orderID] else {
            throw ApiError(code: "BAD_REQUEST", message: "订单不存在")
        }
        // 下单 1.5s 后视为已支付并入账
        if Date().timeIntervalSince(order.created) > 1.5 {
            if orders.removeValue(forKey: orderID) != nil {
                balanceSeconds += Int64(order.pack.minutes * 60)
            }
            return OrderStatusResponse(status: OrderStatus.paid, balanceSeconds: balanceSeconds)
        }
        return OrderStatusResponse(status: OrderStatus.pending, balanceSeconds: nil)
    }

    public func submitFeedback(token: String, content: String, jpegImages: [Data],
                               diagnosticsJSON: String?) async throws -> FeedbackResponse {
        try await Task.sleep(nanoseconds: 500_000_000)
        return FeedbackResponse(feedbackID: "fb-mock")
    }

    public func postMetrics(_ batch: MetricsBatchRequest) async throws {}

    public func mockPayNotify(orderID: String, amountCents: Int) async throws {}
}
#endif
