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.JSONObject
import java.io.BufferedInputStream
import java.io.IOException
import java.io.InterruptedIOException
import java.net.HttpURLConnection
import java.net.URL
import java.security.MessageDigest
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.Future

data class SentenceBoundaryApiResult(
    val videoHash: String,
    val durationMs: Long,
    val algorithmVersion: String,
    val sentences: List<SentenceBoundary>,
)

interface SentenceBoundaryApiCallback {
    fun onSuccess(result: SentenceBoundaryApiResult)

    fun onError(error: Throwable)
}

class CancellableRequest internal constructor(
    private val future: Future<*>,
) {
    fun cancel() {
        future.cancel(true)
    }
}

class SentenceBoundaryApi internal constructor(
    context: Context,
    private val config: OralTrainerSdkConfig,
) {
    private val appContext = context.applicationContext
    private val mainHandler = Handler(Looper.getMainLooper())
    private val executor: ExecutorService = Executors.newCachedThreadPool()
    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."
        }
    }

    fun fetch(
        videoHash: String,
        callback: SentenceBoundaryApiCallback,
    ): CancellableRequest {
        validateHash(videoHash)
        return submit(callback) { fetchBlocking(videoHash.lowercase()) }
    }

    fun fetchForUri(
        uri: Uri,
        contentResolver: ContentResolver = appContext.contentResolver,
        callback: SentenceBoundaryApiCallback,
    ): CancellableRequest {
        return submit(callback) {
            val videoHash = sha256(uri, contentResolver)
            fetchBlocking(videoHash)
        }
    }

    fun release() {
        executor.shutdownNow()
    }

    private fun submit(
        callback: SentenceBoundaryApiCallback,
        operation: () -> SentenceBoundaryApiResult,
    ): CancellableRequest {
        val future = executor.submit {
            try {
                val result = operation()
                if (!Thread.currentThread().isInterrupted) {
                    mainHandler.post { callback.onSuccess(result) }
                }
            } catch (error: Throwable) {
                if (!Thread.currentThread().isInterrupted) {
                    mainHandler.post { callback.onError(error) }
                }
            }
        }
        return CancellableRequest(future)
    }

    private fun fetchBlocking(videoHash: String): SentenceBoundaryApiResult {
        return HttpFallback.forConfig(config).execute(config.sentenceBoundaryApiBaseUrl) { effectiveBaseUrl ->
            val endpoint = Uri.parse(effectiveBaseUrl).buildUpon()
                .appendPath("api")
                .appendPath("v1")
                .appendPath("videos")
                .appendPath(videoHash)
                .appendPath("sentence-boundaries")
                .build()
            val connection = URL(endpoint.toString()).openConnection() as HttpURLConnection
            try {
                connection.requestMethod = "GET"
                connection.connectTimeout = config.connectTimeoutMs
                connection.readTimeout = config.readTimeoutMs
                connection.useCaches = false
                connection.setRequestProperty("Accept", "application/json")
                connection.setRequestProperty("User-Agent", config.userAgent)
                config.userToken?.takeIf { it.isNotBlank() }?.let {
                    connection.setRequestProperty("Authorization", "Bearer $it")
                }
                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 SentenceBoundaryApiException(statusCode, body)
                }
                parseResponse(body, videoHash)
            } finally {
                connection.disconnect()
            }
        }
    }

    private fun parseResponse(
        body: String,
        requestedHash: String,
    ): SentenceBoundaryApiResult {
        val root = JSONObject(body)
        val responseHash = root.getString("video_hash").lowercase()
        if (responseHash != requestedHash.lowercase()) {
            throw IOException("Sentence boundary response hash does not match the request.")
        }
        val jsonSentences = root.getJSONArray("sentences")
        val sentences = buildList(jsonSentences.length()) {
            for (index in 0 until jsonSentences.length()) {
                val sentence = jsonSentences.getJSONObject(index)
                add(
                    SentenceBoundary(
                        index = sentence.getInt("index"),
                        startMs = sentence.getLong("start_ms"),
                        endMs = sentence.getLong("end_ms"),
                        text = if (sentence.isNull("text")) null else sentence.getString("text"),
                        language = if (sentence.isNull("language")) null else sentence.getString("language"),
                        referenceSpeechDurationMs = if (sentence.isNull("reference_speech_duration_ms")) {
                            null
                        } else {
                            sentence.getLong("reference_speech_duration_ms")
                        },
                    )
                )
            }
        }
        return SentenceBoundaryApiResult(
            videoHash = responseHash,
            durationMs = root.getLong("duration_ms"),
            algorithmVersion = root.getString("algorithm_version"),
            sentences = sentences,
        )
    }

    private fun sha256(uri: Uri, contentResolver: ContentResolver): String {
        val digest = MessageDigest.getInstance("SHA-256")
        contentResolver.openInputStream(uri)?.use { input ->
            BufferedInputStream(input).use { buffered ->
                val buffer = ByteArray(1024 * 1024)
                while (true) {
                    if (Thread.currentThread().isInterrupted) {
                        throw InterruptedIOException()
                    }
                    val count = buffered.read(buffer)
                    if (count < 0) {
                        break
                    }
                    digest.update(buffer, 0, count)
                }
            }
        } ?: throw IOException("Cannot open video URI: $uri")
        return digest.digest().joinToString("") { byte -> "%02x".format(byte.toInt() and 0xff) }
    }

    private fun validateHash(videoHash: String) {
        require(SHA256_PATTERN.matches(videoHash)) {
            "videoHash must be a 64-character SHA-256 hex digest."
        }
    }

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

class SentenceBoundaryApiException(
    val statusCode: Int,
    responseBody: String,
) : IOException("Sentence boundary API returned HTTP $statusCode: $responseBody")
