package cn.learningpad.oraltrainer.sdk

import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import android.os.Handler
import android.os.Looper
import org.json.JSONArray
import org.json.JSONObject
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.IOException
import java.net.HttpURLConnection
import java.net.URL
import java.util.UUID
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.Future

/** Uploads one sentence recording to the server-side MOSS assessment endpoint. */
class RemoteImitationQualityAssessor internal constructor(
    context: Context,
    private val config: OralTrainerSdkConfig,
) : ImitationQualityAssessor {
    private val appContext = context.applicationContext
    private val contentResolver: ContentResolver = appContext.contentResolver
    private val executor: ExecutorService = Executors.newCachedThreadPool()
    private val mainHandler = Handler(Looper.getMainLooper())
    private val baseUri = Uri.parse(config.sentenceBoundaryApiBaseUrl.trimEnd('/'))

    init {
        require(baseUri.scheme == "https" || baseUri.scheme == "http") {
            "sentenceBoundaryApiBaseUrl must use http or https."
        }
        require(!baseUri.host.isNullOrBlank()) {
            "sentenceBoundaryApiBaseUrl must include a host."
        }
    }

    override fun assess(
        request: ImitationAssessmentRequest,
        callback: ImitationAssessmentCallback,
    ): CancellableAssessment {
        val future = executor.submit {
            try {
                val result = assessBlocking(request)
                if (!Thread.currentThread().isInterrupted) {
                    mainHandler.post { callback.onResult(result) }
                }
            } catch (error: Throwable) {
                if (!Thread.currentThread().isInterrupted) {
                    mainHandler.post { callback.onError(error) }
                }
            }
        }
        return CancellableAssessment { future.cancel(true) }
    }

    fun release() {
        executor.shutdownNow()
    }

    private fun assessBlocking(request: ImitationAssessmentRequest): ImitationAssessmentResult {
        val videoHash = request.metadata["video_hash"] ?: request.mediaId
        require(SHA256_PATTERN.matches(videoHash)) {
            "ImitationAssessmentRequest.mediaId or metadata[video_hash] must be a SHA-256 hash."
        }
        return HttpFallback.forConfig(config).execute(config.sentenceBoundaryApiBaseUrl) { effectiveBaseUrl ->
            val endpoint = Uri.parse(effectiveBaseUrl).buildUpon()
                .appendPath("api")
                .appendPath("v1")
                .appendPath("videos")
                .appendPath(videoHash.lowercase())
                .appendPath("sentences")
                .appendPath(request.sentence.index.toString())
                .appendPath("assessments")
                .build()
            val connection = URL(endpoint.toString()).openConnection() as HttpURLConnection
            val boundary = "----OralTrainer-${UUID.randomUUID()}"
            try {
                connection.requestMethod = "POST"
                connection.doOutput = true
                connection.connectTimeout = config.connectTimeoutMs
                connection.readTimeout = maxOf(config.readTimeoutMs, 180_000)
                connection.setChunkedStreamingMode(64 * 1024)
                connection.setRequestProperty("Accept", "application/json")
                connection.setRequestProperty("User-Agent", config.userAgent)
                connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=$boundary")
                config.assessmentApiKey?.takeIf { it.isNotBlank() }?.let {
                    connection.setRequestProperty("X-Client-Key", it)
                }
                config.userToken?.takeIf { it.isNotBlank() }?.let {
                    connection.setRequestProperty("Authorization", "Bearer $it")
                }
                BufferedOutputStream(connection.outputStream).use { output ->
                    writeFormField(output, boundary, "language", request.locale ?: request.sentence.language.orEmpty())
                    writeFilePart(output, boundary, request.recordingUri)
                    output.write("--$boundary--\r\n".toByteArray(Charsets.UTF_8))
                }
                val statusCode = connection.responseCode
                val stream = if (statusCode in 200..299) connection.inputStream else connection.errorStream
                val body = stream?.bufferedReader(Charsets.UTF_8)?.use { it.readText() }.orEmpty()
                if (statusCode !in 200..299) {
                    throw RemoteAssessmentException(statusCode, body)
                }
                parseResponse(body)
            } finally {
                connection.disconnect()
            }
        }
    }

    private fun writeFormField(output: BufferedOutputStream, boundary: String, name: String, value: String) {
        output.write("--$boundary\r\n".toByteArray(Charsets.UTF_8))
        output.write("Content-Disposition: form-data; name=\"$name\"\r\n\r\n".toByteArray(Charsets.UTF_8))
        output.write(value.toByteArray(Charsets.UTF_8))
        output.write("\r\n".toByteArray(Charsets.UTF_8))
    }

    private fun writeFilePart(output: BufferedOutputStream, boundary: String, uri: Uri) {
        val fileName = uri.lastPathSegment?.substringAfterLast('/')
            ?.replace("\r", "_")
            ?.replace("\n", "_")
            ?.replace("\"", "_")
            ?.ifBlank { "recording.wav" }
            ?: "recording.wav"
        val mimeType = contentResolver.getType(uri) ?: "audio/wav"
        output.write("--$boundary\r\n".toByteArray(Charsets.UTF_8))
        output.write(
            "Content-Disposition: form-data; name=\"audio\"; filename=\"$fileName\"\r\n".toByteArray(
                Charsets.UTF_8
            )
        )
        output.write("Content-Type: $mimeType\r\n\r\n".toByteArray(Charsets.UTF_8))
        contentResolver.openInputStream(uri)?.use { input ->
            BufferedInputStream(input).use { buffered ->
                val buffer = ByteArray(64 * 1024)
                while (true) {
                    if (Thread.currentThread().isInterrupted) throw IOException("Assessment request was cancelled.")
                    val count = buffered.read(buffer)
                    if (count < 0) break
                    output.write(buffer, 0, count)
                }
            }
        } ?: throw IOException("Cannot open recording URI: $uri")
        output.write("\r\n".toByteArray(Charsets.UTF_8))
    }

    private fun parseResponse(body: String): ImitationAssessmentResult {
        val root = JSONObject(body)
        val substitutions = buildList {
            val array = root.optJSONArray("substitutions") ?: JSONArray()
            for (index in 0 until array.length()) {
                val item = array.optJSONObject(index) ?: continue
                add(item.optString("expected") to item.optString("actual"))
            }
        }
        val details = buildMap {
            val objectValue = root.optJSONObject("details") ?: return@buildMap
            for (key in objectValue.keys()) put(key, objectValue.optString(key))
        }
        return ImitationAssessmentResult(
            overallScore = root.getDouble("overall_score").toFloat(),
            pronunciationScore = root.optNullableDouble("pronunciation_score")?.toFloat(),
            fluencyScore = root.optNullableDouble("fluency_score")?.toFloat(),
            completenessScore = root.optNullableDouble("completeness_score")?.toFloat(),
            feedback = root.optString("feedback").ifBlank { null },
            details = details,
            passed = if (root.has("passed")) root.optBoolean("passed") else null,
            contentScore = root.optNullableDouble("content_score")?.toFloat(),
            durationScore = root.optNullableDouble("duration_score")?.toFloat(),
            pauseScore = root.optNullableDouble("pause_score")?.toFloat(),
            speechRateScore = root.optNullableDouble("speech_rate_score")?.toFloat(),
            durationRatio = root.optNullableDouble("duration_ratio")?.toFloat(),
            referenceText = root.optString("reference_text").ifBlank { null },
            recognizedText = root.optString("recognized_text").ifBlank { null },
            missingTokens = root.optStringArray("missing_tokens"),
            extraTokens = root.optStringArray("extra_tokens"),
            substitutions = substitutions,
        )
    }

    companion object {
        private val SHA256_PATTERN = Regex("^[0-9a-fA-F]{64}$")
    }
}

class RemoteAssessmentException(
    val statusCode: Int,
    responseBody: String,
) : IOException("Remote assessment returned HTTP $statusCode: $responseBody")

private fun JSONObject.optNullableDouble(name: String): Double? {
    if (!has(name) || isNull(name)) return null
    return optDouble(name).takeUnless { it.isNaN() }
}

private fun JSONObject.optStringArray(name: String): List<String> {
    val array = optJSONArray(name) ?: return emptyList()
    return buildList(array.length()) {
        for (index in 0 until array.length()) add(array.optString(index))
    }
}
