package xyz.kusoft.argusupdater

import org.json.JSONObject
import java.io.File
import java.net.HttpURLConnection
import java.net.URL
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import java.security.MessageDigest

class ArgusUpdateClient {
    fun fetchManifest(config: ArgusUpdateConfig): ArgusManifest? {
        val url = buildManifestUrl(config)
        val connection = openConnection(url, accept = "application/json")

        return connection.useJsonBody { statusCode, body ->
            when (statusCode) {
                HttpURLConnection.HTTP_OK -> parseManifest(body)
                HttpURLConnection.HTTP_NOT_FOUND -> null
                else -> throw IllegalStateException("Argus вернул HTTP $statusCode для $url.")
            }
        }
    }

    fun downloadRelease(update: ArgusAvailableUpdate, targetFile: File): File {
        val targetDirectory = targetFile.parentFile
            ?: throw IllegalArgumentException("Для временного APK не задан каталог.")
        check(targetDirectory.mkdirs() || targetDirectory.isDirectory) {
            "Не удалось подготовить каталог для загрузки обновления."
        }

        val partialFile = File(targetDirectory, "${targetFile.name}.part")
        partialFile.delete()

        if (targetFile.exists() && sha256(targetFile).equals(update.manifest.release.sha256, ignoreCase = true)) {
            return targetFile
        }
        targetFile.delete()

        try {
            val connection = openConnection(
                update.downloadUrl,
                accept = "application/vnd.android.package-archive, application/octet-stream, */*"
            )
            connection.requestMethod = "GET"
            connection.instanceFollowRedirects = true

            try {
                val statusCode = connection.responseCode
                if (statusCode != HttpURLConnection.HTTP_OK) {
                    throw IllegalStateException("Argus не отдал APK: HTTP $statusCode.")
                }

                connection.inputStream.use { input ->
                    partialFile.outputStream().use { output ->
                        input.copyTo(output)
                    }
                }
            } finally {
                connection.disconnect()
            }

            val expectedBytes = update.manifest.release.packageSizeBytes.takeIf { it > 0 }
            if (expectedBytes != null && partialFile.length() != expectedBytes) {
                val actualBytes = partialFile.length()
                throw IllegalStateException(
                    "Размер APK не совпал. Argus ожидал $expectedBytes байт, получено $actualBytes."
                )
            }

            val actualSha = sha256(partialFile)
            if (!actualSha.equals(update.manifest.release.sha256, ignoreCase = true)) {
                throw IllegalStateException(
                    "Контрольная сумма APK не совпала. Argus ожидал ${update.manifest.release.sha256}, получено $actualSha."
                )
            }

            if (!partialFile.renameTo(targetFile)) {
                partialFile.copyTo(targetFile, overwrite = true)
                partialFile.delete()
            }

            return targetFile
        } catch (error: Exception) {
            partialFile.delete()
            targetFile.delete()
            throw error
        }
    }

    private fun buildManifestUrl(config: ArgusUpdateConfig): String {
        val baseUrl = config.baseUrl.trim().trimEnd('/')
        val slug = encodeUrlPart(config.slug.trim())
        val platform = encodeUrlPart(config.platform.trim())
        val channel = encodeUrlPart(config.channel.trim())
        return "$baseUrl/api/apps/$slug/manifest?platform=$platform&channel=$channel"
    }

    private fun openConnection(url: String, accept: String): HttpURLConnection =
        (URL(url).openConnection() as HttpURLConnection).apply {
            connectTimeout = 15_000
            readTimeout = 60_000
            setRequestProperty("Accept", accept)
        }

    private fun parseManifest(body: String): ArgusManifest {
        val root = JSONObject(body)
        val releaseJson = root.getJSONObject("release")
        return ArgusManifest(
            slug = root.getString("slug"),
            name = root.getString("name"),
            summary = root.getString("summary"),
            description = root.getString("description"),
            androidPackageName = root.optionalString("androidPackageName"),
            repositoryUrl = root.optionalString("repositoryUrl"),
            homepageUrl = root.optionalString("homepageUrl"),
            release = ArgusRelease(
                id = releaseJson.getString("id"),
                version = releaseJson.getString("version"),
                channel = releaseJson.getString("channel"),
                platform = releaseJson.getString("platform"),
                packageKind = releaseJson.getString("packageKind"),
                androidVersionCode = releaseJson.optionalLong("androidVersionCode"),
                downloadPath = releaseJson.getString("downloadPath"),
                originalFileName = releaseJson.getString("originalFileName"),
                contentType = releaseJson.getString("contentType"),
                packageSizeBytes = releaseJson.getLong("packageSizeBytes"),
                sha256 = releaseJson.getString("sha256"),
                publishedAt = releaseJson.getString("publishedAt"),
                notes = releaseJson.optionalString("notes")
            )
        )
    }

    private fun encodeUrlPart(value: String): String =
        URLEncoder.encode(value, StandardCharsets.UTF_8.name()).replace("+", "%20")

    private fun JSONObject.optionalString(name: String): String? {
        if (!has(name) || isNull(name)) {
            return null
        }

        return optString(name)
            .trim()
            .takeIf { it.isNotBlank() && !it.equals("null", ignoreCase = true) }
    }

    private fun JSONObject.optionalLong(name: String): Long? {
        if (!has(name) || isNull(name)) {
            return null
        }

        return when (val value = opt(name)) {
            is Number -> value.toLong()
            is String -> value.trim().toLongOrNull()
            else -> null
        }
    }

    private fun HttpURLConnection.useJsonBody(block: (statusCode: Int, body: String) -> ArgusManifest?): ArgusManifest? =
        try {
            val statusCode = responseCode
            val stream = if (statusCode >= 400) errorStream else inputStream
            val body = stream?.bufferedReader()?.use { it.readText() }.orEmpty()
            block(statusCode, body)
        } finally {
            disconnect()
        }

    private fun sha256(file: File): String {
        val digest = MessageDigest.getInstance("SHA-256")
        file.inputStream().use { input ->
            val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
            while (true) {
                val read = input.read(buffer)
                if (read <= 0) {
                    break
                }
                digest.update(buffer, 0, read)
            }
        }
        return digest.digest().joinToString(separator = "") { byte -> "%02x".format(byte) }
    }
}
