package com.aletheia.app.audiobook

import com.aletheia.app.BuildConfig
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.net.HttpURLConnection
import java.net.URL
import java.nio.charset.StandardCharsets
import java.util.UUID
import org.json.JSONObject

class RemoteAudioBookClient(
    private val baseUrl: String = BuildConfig.AUDIOBOOK_API_BASE_URL,
    private val token: String = BuildConfig.AUDIOBOOK_API_TOKEN
) {
    fun create(source: File, title: String, author: String): RemoteJob {
        require(source.isFile) { "Файл исходной книги не найден" }
        val boundary = "aletheia-${UUID.randomUUID()}"
        val connection = open("v1/audiobooks").apply {
            requestMethod = "POST"
            doOutput = true
            setChunkedStreamingMode(256 * 1024)
            setRequestProperty("Content-Type", "multipart/form-data; boundary=$boundary")
        }
        try {
            BufferedOutputStream(connection.outputStream, 256 * 1024).use { output ->
                output.writeTextPart(boundary, "title", title)
                output.writeTextPart(boundary, "author", author)
                val safeName = source.name.replace('"', '_')
                output.writeAscii("--$boundary\r\n")
                output.writeAscii("Content-Disposition: form-data; name=\"book\"; filename=\"$safeName\"\r\n")
                output.writeAscii("Content-Type: ${contentType(source)}\r\n\r\n")
                source.inputStream().buffered(256 * 1024).use { it.copyTo(output, 256 * 1024) }
                output.writeAscii("\r\n--$boundary--\r\n")
            }
            return parseJob(connection.readSuccessfulBody())
        } finally {
            connection.disconnect()
        }
    }

    fun status(jobId: String): RemoteJob {
        val connection = open("v1/audiobooks/$jobId")
        return try {
            parseJob(connection.readSuccessfulBody())
        } finally {
            connection.disconnect()
        }
    }

    fun download(jobId: String, destination: File) {
        val connection = open("v1/audiobooks/$jobId/file").apply {
            readTimeout = DOWNLOAD_TIMEOUT_MS
        }
        try {
            if (connection.responseCode !in 200..299) {
                throw IOException(connection.errorDescription())
            }
            destination.parentFile?.mkdirs()
            connection.inputStream.buffered(256 * 1024).use { input ->
                FileOutputStream(destination).buffered(256 * 1024).use { output ->
                    input.copyTo(output, 256 * 1024)
                }
            }
            check(destination.isFile && destination.length() > 0L) {
                "Сервер вернул пустой M4A-файл"
            }
        } finally {
            connection.disconnect()
        }
    }

    fun cancelOrDelete(jobId: String) {
        val connection = open("v1/audiobooks/$jobId").apply { requestMethod = "DELETE" }
        try {
            val code = connection.responseCode
            if (code !in 200..299 && code != HttpURLConnection.HTTP_NOT_FOUND) {
                throw IOException(connection.errorDescription())
            }
            (if (code in 200..299) connection.inputStream else connection.errorStream)?.close()
        } finally {
            connection.disconnect()
        }
    }

    private fun open(path: String): HttpURLConnection {
        check(token.isNotBlank()) { "Ключ сервера аудиокниг не добавлен в сборку" }
        return (URL(baseUrl.trimEnd('/') + "/" + path).openConnection() as HttpURLConnection).apply {
            connectTimeout = CONNECT_TIMEOUT_MS
            readTimeout = READ_TIMEOUT_MS
            useCaches = false
            setRequestProperty("Authorization", "Bearer $token")
            setRequestProperty("Accept", "application/json")
        }
    }

    private fun HttpURLConnection.readSuccessfulBody(): String {
        val code = responseCode
        if (code !in 200..299) throw IOException(errorDescription())
        return inputStream.bufferedReader(StandardCharsets.UTF_8).use { it.readText() }
    }

    private fun HttpURLConnection.errorDescription(): String {
        val details = errorStream?.bufferedReader(StandardCharsets.UTF_8)?.use { it.readText() }.orEmpty()
        return "Сервер аудиокниг вернул HTTP $responseCode" +
            details.takeIf(String::isNotBlank)?.let { ": $it" }.orEmpty()
    }

    private fun parseJob(payload: String): RemoteJob {
        val json = JSONObject(payload)
        return RemoteJob(
            id = json.getString("id"),
            status = json.getString("status"),
            stage = json.optString("stage"),
            processedCharacters = json.optLong("processedCharacters"),
            totalCharacters = json.optLong("totalCharacters"),
            currentChapter = json.optString("currentChapter").takeIf { it.isNotBlank() && it != "null" },
            durationMs = json.optLong("durationMs"),
            chaptersJson = json.optJSONArray("chapters")?.toString() ?: "[]",
            error = json.optString("error").takeIf { it.isNotBlank() && it != "null" }
        )
    }

    private fun BufferedOutputStream.writeTextPart(boundary: String, name: String, value: String) {
        writeAscii("--$boundary\r\n")
        writeAscii("Content-Disposition: form-data; name=\"$name\"\r\n")
        writeAscii("Content-Type: text/plain; charset=UTF-8\r\n\r\n")
        write(value.toByteArray(StandardCharsets.UTF_8))
        writeAscii("\r\n")
    }

    private fun BufferedOutputStream.writeAscii(value: String) {
        write(value.toByteArray(StandardCharsets.US_ASCII))
    }

    private fun contentType(file: File): String = when (file.extension.lowercase()) {
        "epub" -> "application/epub+zip"
        "fb2" -> "application/x-fictionbook+xml"
        else -> "application/octet-stream"
    }

    data class RemoteJob(
        val id: String,
        val status: String,
        val stage: String,
        val processedCharacters: Long,
        val totalCharacters: Long,
        val currentChapter: String?,
        val durationMs: Long,
        val chaptersJson: String,
        val error: String?
    )

    private companion object {
        const val CONNECT_TIMEOUT_MS = 30_000
        const val READ_TIMEOUT_MS = 60_000
        const val DOWNLOAD_TIMEOUT_MS = 10 * 60_000
    }
}
