package app.dudu.net

import app.dudu.BuildConfig
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONArray
import org.json.JSONObject
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.util.concurrent.TimeUnit
import java.util.zip.GZIPOutputStream

/** REST 错误：服务端统一错误体 {"code","message"}（见 server/pkg/protocol/errors.go）。 */
class ApiException(val code: String, message: String) : Exception(message) {
    companion object {
        const val NETWORK = "NETWORK" // 客户端本地网络错误（非服务端码）
    }
}

/**
 * REST 客户端，契约逐一对齐 server/pkg/protocol/dto.go 与 internal/httpapi/router.go：
 *  - POST /v1/auth/wechat        微信 code 换 JWT（AuthWechatRequest → AuthTokenResponse）
 *  - GET  /v1/me                 MeResponse
 *  - GET  /v1/packs              PacksResponse
 *  - POST /v1/orders             CreateOrderRequest(channel=app) → CreateOrderResponse
 *  - GET  /v1/orders/{id}        OrderStatusResponse
 *  - POST /v1/feedback           multipart（content / images[] / diagnostics）
 *  - POST /v1/metrics/batch      MetricsBatchRequest（gzip，可匿名）
 *
 * 选型：OkHttp + org.json（与 AsrSocket 一致），不引 Retrofit/Moshi——接口仅 8 个，
 * 手写解析依赖最小且字段对齐一目了然。
 */
object DuduApi {

    // TODO(release): 生产切 https://api.dudu.app —— 在 app/build.gradle.kts 按
    // buildType 注入不同 BuildConfig.API_BASE，此处无需改动。
    private val base = BuildConfig.API_BASE.trimEnd('/')

    // 设备 ID：与 WS 握手 X-Device-ID 同源（AccountRepository.deviceId），DuduApp.onCreate 注入一次。
    // 服务端 touchDevice 依赖该头维护 devices 表，缺失则 did=="" 直接 return。
    @Volatile
    private var deviceId: String = ""

    /** 进程启动时由 DuduApp 注入设备 ID，供所有 REST 请求带 X-Device-ID 头。 */
    fun init(deviceId: String) {
        this.deviceId = deviceId
    }

    private val client: OkHttpClient by lazy {
        OkHttpClient.Builder()
            .connectTimeout(5, TimeUnit.SECONDS)
            .callTimeout(60, TimeUnit.SECONDS)
            .build()
    }

    // ─── DTO（字段对齐 dto.go） ──────────────────────────────────────────────

    data class UserInfo(val userId: String, val nicknameMasked: String)

    data class LoginResult(val token: String, val user: UserInfo)

    data class Me(
        val userId: String,
        val nicknameMasked: String,
        val balanceSeconds: Long,
        val trialDailyLimit: Int, // 秒（服务端 TrialDailySeconds）
        val trialUsedToday: Int,  // 秒
        val accountState: String, // ok | trial | quota
    )

    data class Pack(
        val id: String,
        val minutes: Int,
        val priceCents: Int,
        val unitDesc: String,
        val tag: String?, // "省 13%" 等，可空
    )

    data class OrderCreated(val orderId: String, val payParams: Map<String, Any?>)

    data class OrderStatus(val status: String, val balanceSeconds: Long) // pending | paid | closed

    // ─── 接口 ────────────────────────────────────────────────────────────────

    /** POST /v1/auth/wechat：移动端 OpenSDK code 换 JWT。 */
    suspend fun loginWechat(code: String): LoginResult {
        val req = jsonRequest("/v1/auth/wechat", null, JSONObject().put("code", code))
        return exec(req) { body ->
            val j = JSONObject(body)
            val u = j.getJSONObject("user")
            LoginResult(
                token = j.getString("token"),
                user = UserInfo(u.getString("user_id"), u.optString("nickname_masked")),
            )
        }
    }

    /** POST /v1/auth/logout：JWT 进黑名单（失败静默，本地照常清态）。 */
    suspend fun logout(token: String) {
        val req = jsonRequest("/v1/auth/logout", token, JSONObject())
        runCatching { exec(req) { } }
    }

    /** GET /v1/me。 */
    suspend fun me(token: String): Me {
        val req = getRequest("/v1/me", token)
        return exec(req) { body ->
            val j = JSONObject(body)
            Me(
                userId = j.getString("user_id"),
                nicknameMasked = j.optString("nickname_masked"),
                balanceSeconds = j.optLong("balance_seconds"),
                trialDailyLimit = j.optInt("trial_daily_limit"),
                trialUsedToday = j.optInt("trial_used_today"),
                accountState = j.optString("account_state"),
            )
        }
    }

    /** GET /v1/packs（无需登录，服务端可配）。 */
    suspend fun packs(): List<Pack> {
        val req = getRequest("/v1/packs", null)
        return exec(req) { body ->
            val arr = JSONObject(body).getJSONArray("packs")
            (0 until arr.length()).map { i ->
                val p = arr.getJSONObject(i)
                Pack(
                    id = p.getString("id"),
                    minutes = p.getInt("minutes"),
                    priceCents = p.getInt("price_cents"),
                    unitDesc = p.optString("unit_desc"),
                    tag = p.optString("tag").ifEmpty { null },
                )
            }
        }
    }

    /** POST /v1/orders：channel 固定 app（移动收银台）→ pay_params。 */
    suspend fun createOrder(token: String, packId: String): OrderCreated {
        val req = jsonRequest(
            "/v1/orders", token,
            JSONObject().put("pack_id", packId).put("channel", "app"),
        )
        return exec(req) { body ->
            val j = JSONObject(body)
            val params = mutableMapOf<String, Any?>()
            j.optJSONObject("pay_params")?.let { pp ->
                pp.keys().forEach { k -> params[k] = pp.get(k) }
            }
            OrderCreated(j.getString("order_id"), params)
        }
    }

    /** GET /v1/orders/{id}：轮询至 paid。 */
    suspend fun orderStatus(token: String, orderId: String): OrderStatus {
        val req = getRequest("/v1/orders/$orderId", token)
        return exec(req) { body ->
            val j = JSONObject(body)
            OrderStatus(j.getString("status"), j.optLong("balance_seconds"))
        }
    }

    /**
     * POST /v1/feedback（multipart，需登录）。
     * images：原始字节（≤3 张、单张 ≤5MB，调用方已校验），服务端按 magic bytes 验格式。
     */
    suspend fun submitFeedback(
        token: String,
        content: String,
        images: List<ByteArray>,
        diagnostics: String?,
    ): String {
        val mb = MultipartBody.Builder().setType(MultipartBody.FORM)
            .addFormDataPart("content", content)
        if (diagnostics != null) mb.addFormDataPart("diagnostics", diagnostics)
        images.forEachIndexed { i, data ->
            val (ext, mime) = sniffImage(data) ?: ("jpg" to "image/jpeg")
            mb.addFormDataPart("images[]", "${i + 1}.$ext", data.toRequestBody(mime.toMediaType()))
        }
        val req = Request.Builder()
            .url(base + "/v1/feedback")
            .commonHeaders(token)
            .post(mb.build())
            .build()
        return exec(req) { body -> JSONObject(body).getString("feedback_id") }
    }

    /** POST /v1/metrics/batch（gzip JSON，可匿名）：返回是否成功，永不抛错。 */
    suspend fun metricsBatch(bodyJson: String): Boolean = withContext(Dispatchers.IO) {
        val gz = ByteArrayOutputStream()
        GZIPOutputStream(gz).use { it.write(bodyJson.toByteArray(Charsets.UTF_8)) }
        val req = Request.Builder()
            .url(base + "/v1/metrics/batch")
            .header("Content-Encoding", "gzip")
            .post(gz.toByteArray().toRequestBody(JSON))
            .build()
        runCatching { client.newCall(req).execute().use { it.isSuccessful } }.getOrDefault(false)
    }

    /**
     * POST /v1/pay/notify —— **仅 Mock 支付链路使用**：模拟微信支付回调，让 dev server
     * 把订单置为 paid（回调体对齐 server billing.MockPay.ParseNotify 的明文 JSON）。
     * TODO(wechat-pay): 真实微信支付接入后删除，回调由微信服务器直达后端。
     */
    suspend fun mockPayNotify(orderId: String, amountCents: Int): Boolean = withContext(Dispatchers.IO) {
        val body = JSONObject()
            .put("order_id", orderId)
            .put("transaction_id", "mock-tx-" + System.currentTimeMillis())
            .put("amount_cents", amountCents)
            .toString()
        val req = Request.Builder()
            .url(base + "/v1/pay/notify")
            .post(body.toRequestBody(JSON))
            .build()
        runCatching { client.newCall(req).execute().use { it.isSuccessful } }.getOrDefault(false)
    }

    // ─── 内部 ────────────────────────────────────────────────────────────────

    private val JSON = "application/json; charset=utf-8".toMediaType()

    /** 统一附加设备/平台头：X-Device-ID（服务端 touchDevice 依赖）、X-Platform、X-App-Version。 */
    private fun Request.Builder.commonHeaders(token: String?): Request.Builder = apply {
        if (token != null) header("Authorization", "Bearer $token")
        if (deviceId.isNotEmpty()) header("X-Device-ID", deviceId)
        header("X-Platform", "android")
        header("X-App-Version", BuildConfig.VERSION_NAME)
    }

    private fun getRequest(path: String, token: String?): Request =
        Request.Builder().url(base + path).commonHeaders(token).build()

    private fun jsonRequest(path: String, token: String?, body: JSONObject): Request =
        Request.Builder().url(base + path).commonHeaders(token)
            .post(body.toString().toRequestBody(JSON)).build()

    private suspend fun <T> exec(req: Request, parse: (String) -> T): T = withContext(Dispatchers.IO) {
        val resp = try {
            client.newCall(req).execute()
        } catch (e: IOException) {
            throw ApiException(ApiException.NETWORK, "网络连接失败，检查网络后再试")
        }
        resp.use {
            val body = it.body?.string().orEmpty()
            if (!it.isSuccessful) {
                val err = runCatching { JSONObject(body) }.getOrNull()
                val code = err?.optString("code").orEmpty().ifEmpty { "HTTP_${it.code}" }
                val msg = err?.optString("message").orEmpty().ifEmpty { "请求失败，稍后再试" }
                throw ApiException(code, msg)
            }
            try {
                parse(body)
            } catch (e: org.json.JSONException) {
                throw ApiException(ApiException.NETWORK, "响应解析失败，稍后再试")
            }
        }
    }

    /** magic bytes 判断图片格式（与服务端 feedback.sniffImage 一致）。 */
    fun sniffImage(data: ByteArray): Pair<String, String>? = when {
        data.size > 3 && data[0] == 0xFF.toByte() && data[1] == 0xD8.toByte() && data[2] == 0xFF.toByte() ->
            "jpg" to "image/jpeg"
        data.size > 4 && data[0] == 0x89.toByte() && data[1] == 'P'.code.toByte() &&
            data[2] == 'N'.code.toByte() && data[3] == 'G'.code.toByte() ->
            "png" to "image/png"
        data.size > 12 && String(data, 0, 4) == "RIFF" && String(data, 8, 4) == "WEBP" ->
            "webp" to "image/webp"
        else -> null
    }
}
