package com.doctate.watch.net

import com.doctate.watch.settings.Settings
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.asRequestBody
import org.json.JSONObject
import java.io.File
import java.io.IOException

/**
 * POST a recorded audio file to the Doctate server. Wire format mirrors
 * common/src/constants.rs and clients/desktop/src/server_sync.rs:344-399.
 * If the server contract changes, both this client and server/tests/upload_test.rs
 * fail independently — that coupling is intentional.
 */
class UploadClient(
    private val http: OkHttpClient,
    private val settings: Settings,
) {
    suspend fun upload(caseId: String, recordedAt: String, audio: File): UploadResult =
        withContext(Dispatchers.IO) {
            val body = MultipartBody.Builder()
                .setType(MultipartBody.FORM)
                .addFormDataPart(FIELD_CASE_ID, caseId)
                .addFormDataPart(FIELD_RECORDED_AT, recordedAt)
                .addFormDataPart(
                    FIELD_AUDIO,
                    audio.name,
                    audio.asRequestBody(CONTENT_TYPE_AUDIO_MP4.toMediaType()),
                )
                .build()
            val request = Request.Builder()
                .url(settings.serverUrl.trimEnd('/') + UPLOAD_PATH)
                .header(API_KEY_HEADER, settings.apiKey)
                .post(body)
                .build()
            try {
                http.newCall(request).execute().use { response ->
                    classify(response.code, response.body?.string().orEmpty())
                }
            } catch (e: IOException) {
                UploadResult.Transient("network: ${e.message}")
            }
        }

    private fun classify(code: Int, body: String): UploadResult = when {
        code in 200..299 -> parseAck(body)
        code == 408 || code == 429 || code in 500..599 -> UploadResult.Transient("HTTP $code: $body")
        else -> UploadResult.Terminal("HTTP $code: $body")
    }

    private fun parseAck(body: String): UploadResult = try {
        val json = JSONObject(body)
        UploadResult.Success(
            caseId = json.getString("case_id"),
            recordedAt = json.getString("recorded_at"),
            status = json.getString("status"),
        )
    } catch (e: Exception) {
        UploadResult.Terminal("parse ack: ${e.message}")
    }

    companion object {
        // Wire-protocol constants. Source of truth: common/src/constants.rs:1-14.
        const val API_KEY_HEADER = "X-API-Key"
        const val UPLOAD_PATH = "/api/upload"
        const val FIELD_CASE_ID = "case_id"
        const val FIELD_RECORDED_AT = "recorded_at"
        const val FIELD_AUDIO = "audio"
        const val CONTENT_TYPE_AUDIO_MP4 = "audio/mp4"
    }
}
