package cn.learningpad.oraltrainer.sdk

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

data class AuthUser(
    val id: String,
    val username: String,
    val nickname: String,
)

data class AuthSession(
    val token: String,
    val user: AuthUser,
)

data class Course(
    val videoHash: String,
    val title: String,
    val durationMs: Long?,
    val language: String?,
    val sentenceCount: Int,
    val streamUrl: String,
    val enrolled: Boolean,
) {
    fun toVideoSummary(): TrainingVideoSummary {
        return TrainingVideoSummary(
            videoHash = videoHash,
            title = title,
            streamUri = android.net.Uri.parse(streamUrl),
            durationMs = durationMs,
            sizeBytes = 0L,
            language = language,
            sentenceCount = sentenceCount,
            status = "ready",
        )
    }
}

data class UserResult(
    val attemptId: String,
    val videoHash: String,
    val courseTitle: String,
    val sentenceIndex: Int,
    val sentenceText: String,
    val overallScore: Float,
    val passed: Boolean,
    val createdAt: String,
)

data class UserDubShare(
    val shareId: String,
    val videoHash: String,
    val courseTitle: String,
    val title: String,
    val segmentCount: Int,
    val averageScore: Float,
    val createdAt: String,
)

class UserApiException(
    val statusCode: Int,
    responseBody: String,
) : IOException("HTTP $statusCode: ${responseBody.take(300)}")

class UserApi 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 = android.net.Uri.parse(config.sentenceBoundaryApiBaseUrl.trimEnd('/'))

    init {
        require(baseUri.scheme == "https" || baseUri.scheme == "http") {
            "sentenceBoundaryApiBaseUrl must use http or https."
        }
    }

    fun register(
        username: String,
        password: String,
        nickname: String? = null,
        phone: String? = null,
        onComplete: (Result<AuthSession>) -> Unit,
    ): CancellableRequest = submit(onComplete) {
        requestRegisterBlocking(
            "api/v1/auth/register",
            username = username,
            password = password,
            nickname = nickname,
            phone = phone,
        )
    }

    fun login(
        username: String,
        password: String,
        onComplete: (Result<AuthSession>) -> Unit,
    ): CancellableRequest = submit(onComplete) { requestBlocking("api/v1/auth/login", username, password, auth = false) }

    fun courses(
        token: String,
        onComplete: (Result<List<Course>>) -> Unit,
    ): CancellableRequest = submitObject(onComplete, token) { parseCourses(getBlocking(it, "api/v1/courses")) }

    fun enroll(
        token: String,
        videoHash: String,
        onComplete: (Result<Unit>) -> Unit,
    ): CancellableRequest = submitUnit(onComplete, token) {
        request("api/v1/courses/$videoHash/enroll", token, method = "POST")
    }

    fun results(
        token: String,
        onComplete: (Result<List<UserResult>>) -> Unit,
    ): CancellableRequest = submitObject(onComplete, token) { parseResults(getBlocking(it, "api/v1/me/results")) }

    fun dubShares(
        token: String,
        onComplete: (Result<List<UserDubShare>>) -> Unit,
    ): CancellableRequest = submitObject(onComplete, token) { parseDubShares(getBlocking(it, "api/v1/me/dub-shares")) }

    fun release() {
        executor.shutdownNow()
    }

    private fun <T> submit(
        onComplete: (Result<T>) -> Unit,
        operation: () -> T,
    ): CancellableRequest {
        val future: Future<*> = executor.submit {
            post(onComplete, runCatching(operation))
        }
        return CancellableRequest(future)
    }

    private fun <T> submitObject(
        onComplete: (Result<T>) -> Unit,
        token: String,
        operation: (String) -> T,
    ): CancellableRequest = submit(onComplete) { operation(token) }

    private fun submitUnit(
        onComplete: (Result<Unit>) -> Unit,
        token: String,
        operation: (String) -> Unit,
    ): CancellableRequest = submit(onComplete) {
        operation(token)
        Unit
    }

    private fun <T> post(onComplete: (Result<T>) -> Unit, result: Result<T>) {
        if (!Thread.currentThread().isInterrupted) {
            mainHandler.post { onComplete(result) }
        }
    }

    private fun requestBlocking(path: String, username: String, password: String, auth: Boolean): AuthSession {
        val root = JSONObject().put("username", username).put("password", password)
        val response = request(path, body = root.toString(), method = "POST")
        return parseSession(JSONObject(response))
    }

    private fun requestRegisterBlocking(
        path: String,
        username: String,
        password: String,
        nickname: String?,
        phone: String?,
    ): AuthSession {
        val root = JSONObject()
            .put("username", username)
            .put("password", password)
        nickname?.takeIf { it.isNotBlank() }?.let { root.put("nickname", it) }
        phone?.takeIf { it.isNotBlank() }?.let { root.put("phone", it) }
        val response = request(path, body = root.toString(), method = "POST")
        return parseSession(JSONObject(response))
    }

    private fun getBlocking(token: String, path: String): JSONArray {
        val body = request(path, token = token, method = "GET")
        return JSONArray(body)
    }

    private fun parseCourses(items: JSONArray): List<Course> = List(items.length()) { index ->
        val item = items.getJSONObject(index)
        Course(
            videoHash = item.getString("video_hash"),
            title = item.getString("title"),
            durationMs = if (item.isNull("duration_ms")) null else item.getLong("duration_ms"),
            language = if (item.isNull("language")) null else item.getString("language"),
            sentenceCount = item.getInt("sentence_count"),
            streamUrl = item.getString("stream_url"),
            enrolled = item.getBoolean("enrolled"),
        )
    }

    private fun parseResults(items: JSONArray): List<UserResult> = List(items.length()) { index ->
        val item = items.getJSONObject(index)
        UserResult(
            attemptId = item.getString("attempt_id"),
            videoHash = item.getString("video_hash"),
            courseTitle = item.getString("course_title"),
            sentenceIndex = item.getInt("sentence_index"),
            sentenceText = item.getString("sentence_text"),
            overallScore = item.getDouble("overall_score").toFloat(),
            passed = item.getBoolean("passed"),
            createdAt = item.getString("created_at"),
        )
    }

    private fun parseDubShares(items: JSONArray): List<UserDubShare> = List(items.length()) { index ->
        val item = items.getJSONObject(index)
        UserDubShare(
            shareId = item.getString("share_id"),
            videoHash = item.getString("video_hash"),
            courseTitle = item.getString("course_title"),
            title = item.getString("title"),
            segmentCount = item.getInt("segment_count"),
            averageScore = item.getDouble("average_score").toFloat(),
            createdAt = item.getString("created_at"),
        )
    }

    private fun request(
        path: String,
        token: String? = null,
        body: String? = null,
        method: String = body?.let { "POST" } ?: "GET",
    ): String {
        val endpoint = android.net.Uri.parse(config.sentenceBoundaryApiBaseUrl.trimEnd('/'))
            .buildUpon().appendEncodedPath(path).build()
        val connection = URL(endpoint.toString()).openConnection() as HttpURLConnection
        try {
            connection.requestMethod = method
            connection.connectTimeout = config.connectTimeoutMs
            connection.readTimeout = config.readTimeoutMs
            connection.useCaches = false
            connection.setRequestProperty("Accept", "application/json")
            connection.setRequestProperty("User-Agent", config.userAgent)
            token?.takeIf { it.isNotBlank() }?.let {
                connection.setRequestProperty("Authorization", "Bearer $it")
            }
            if (body != null) {
                connection.doOutput = true
                connection.setRequestProperty("Content-Type", "application/json")
                connection.outputStream.use { it.write(body.toByteArray(Charsets.UTF_8)) }
            }
            val statusCode = connection.responseCode
            val stream = if (statusCode in 200..299) connection.inputStream else connection.errorStream
            val text = stream?.bufferedReader(Charsets.UTF_8)?.use { it.readText() }.orEmpty()
            if (statusCode !in 200..299) throw UserApiException(statusCode, text)
            return text
        } finally {
            connection.disconnect()
        }
    }

    private fun parseSession(root: JSONObject): AuthSession {
        val user = root.getJSONObject("user")
        return AuthSession(
            token = root.getString("token"),
            user = AuthUser(
                id = user.getString("id"),
                username = user.getString("username"),
                nickname = user.getString("nickname"),
            ),
        )
    }
}
