package com.rosetta.messenger.network

import android.content.Context
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeout
import kotlinx.coroutines.withContext
import com.rosetta.messenger.utils.RosettaDev1Log
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import java.io.File
import java.io.IOException
import java.net.SocketTimeoutException
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.coroutineContext

/**
 * Состояние загрузки/скачивания файла
 */
data class TransportState(
    val id: String,
    val progress: Int // 0-100
)

/**
 * Менеджер транспорта файлов
 * Загрузка и скачивание файлов с транспортного сервера
 * Совместимо с desktop версией (TransportProvider)
 */
object TransportManager {
    private const val MAX_RETRIES = 3
    private const val INITIAL_BACKOFF_MS = 1000L
    private const val UPLOAD_ATTEMPT_TIMEOUT_MS = 45_000L
    
    private var transportServer: String? = null
    private var appContext: Context? = null
    
    private val _uploading = MutableStateFlow<List<TransportState>>(emptyList())
    val uploading: StateFlow<List<TransportState>> = _uploading.asStateFlow()
    private val activeUploadCalls = ConcurrentHashMap<String, Call>()
    
    private val _downloading = MutableStateFlow<List<TransportState>>(emptyList())
    val downloading: StateFlow<List<TransportState>> = _downloading.asStateFlow()
    private val activeDownloadCalls = ConcurrentHashMap<String, Call>()
    
    private val client = OkHttpClient.Builder()
        .connectTimeout(60, TimeUnit.SECONDS)
        .readTimeout(5, TimeUnit.MINUTES)   // 5 минут для больших файлов
        .writeTimeout(5, TimeUnit.MINUTES)  // 5 минут для больших файлов
        .build()
    
    /**
     * Инициализация с контекстом приложения (вызывать из Application.onCreate)
     */
    fun init(context: Context) {
        appContext = context.applicationContext
    }
    
    /**
     * Установить адрес транспортного сервера
     */
    fun setTransportServer(server: String) {
        val normalized = server.trim().trimEnd('/')
        transportServer = normalized.ifBlank { null }
        RosettaDev1Log.d("net/transport-server set=${transportServer.orEmpty()}")
    }
    
    /**
     * Получить текущий адрес транспортного сервера
     */
    fun getTransportServer(): String? = transportServer
    
    /**
     * Получить активный сервер для скачивания/загрузки.
     * Desktop parity: ждём сервер из PacketRequestTransport (0x0F), а не используем hardcoded CDN.
     */
    private suspend fun getActiveServer(serverOverride: String? = null): String {
        val normalizedOverride = serverOverride?.trim()?.trimEnd('/').orEmpty()
        if (normalizedOverride.isNotEmpty()) {
            return normalizedOverride
        }
        transportServer?.let { return it }
        requestTransportServer()
        repeat(40) { // 10s total
            val server = transportServer
            if (!server.isNullOrBlank()) {
                return server
            }
            delay(250)
        }
        throw IOException("Transport server is not set")
    }
    
    /**
     * Retry с exponential backoff: 1с, 2с, 4с
     */
    private suspend fun <T> withRetry(
        operation: String = "transport",
        id: String = "-",
        block: suspend () -> T
    ): T {
        var lastException: Exception? = null
        repeat(MAX_RETRIES) { attempt ->
            try {
                return block()
            } catch (e: CancellationException) {
                RosettaDev1Log.w(
                    "net/$operation cancelled id=${id.take(12)} attempt=${attempt + 1}/$MAX_RETRIES",
                    e
                )
                throw e
            } catch (e: Exception) {
                lastException = if (e is IOException) e else IOException("Download error: ${e.message}", e)
                val shouldRetry = attempt < MAX_RETRIES - 1
                if (shouldRetry) {
                    val backoffMs = INITIAL_BACKOFF_MS shl attempt
                    RosettaDev1Log.w(
                        "net/$operation retry id=${id.take(12)} attempt=${attempt + 1}/$MAX_RETRIES " +
                            "backoff=${backoffMs}ms reason=${e.javaClass.simpleName}:${e.message ?: "unknown"}"
                    )
                } else {
                    RosettaDev1Log.e(
                        "net/$operation failed id=${id.take(12)} attempt=${attempt + 1}/$MAX_RETRIES " +
                            "reason=${e.javaClass.simpleName}:${e.message ?: "unknown"}",
                        e
                    )
                }
                if (attempt < MAX_RETRIES - 1) {
                    delay(INITIAL_BACKOFF_MS shl attempt) // 1s, 2s, 4s
                }
            }
        }
        throw lastException!!
    }

    /**
     * Запросить адрес транспортного сервера с сервера протокола
     */
    fun requestTransportServer() {
        val packet = PacketRequestTransport()
        RosettaDev1Log.d("net/transport-server request packet=0x0F")
        ProtocolRuntimeAccess.get().sendPacket(packet)
    }

    /**
     * Принудительно отменяет активный HTTP call для скачивания attachment.
     * Нужен для pause/resume в file bubble.
     */
    fun cancelDownload(id: String) {
        activeDownloadCalls.remove(id)?.cancel()
        _downloading.value = _downloading.value.filter { it.id != id }
    }

    /**
     * Принудительно отменяет активный HTTP call для upload attachment.
     */
    fun cancelUpload(id: String) {
        activeUploadCalls.remove(id)?.cancel()
        _uploading.value = _uploading.value.filter { it.id != id }
    }

    private suspend fun awaitDownloadResponse(id: String, request: Request): Response =
        suspendCancellableCoroutine { cont ->
            val call = client.newCall(request)
            activeDownloadCalls[id] = call

            cont.invokeOnCancellation {
                activeDownloadCalls.remove(id, call)
                call.cancel()
            }

            call.enqueue(object : Callback {
                override fun onFailure(call: Call, e: IOException) {
                    activeDownloadCalls.remove(id, call)
                    if (call.isCanceled()) {
                        cont.cancel(CancellationException("Download cancelled"))
                    } else {
                        cont.resumeWithException(e)
                    }
                }

                override fun onResponse(call: Call, response: Response) {
                    activeDownloadCalls.remove(id, call)
                    if (cont.isCancelled) {
                        response.close()
                        return
                    }
                    cont.resume(response)
                }
            })
        }

    private suspend fun awaitUploadResponse(id: String, request: Request): Response =
        suspendCancellableCoroutine { cont ->
            val call = client.newCall(request)
            activeUploadCalls[id] = call

            cont.invokeOnCancellation {
                activeUploadCalls.remove(id, call)
                call.cancel()
            }

            call.enqueue(object : Callback {
                override fun onFailure(call: Call, e: IOException) {
                    activeUploadCalls.remove(id, call)
                    if (call.isCanceled()) {
                        cont.cancel(CancellationException("Upload cancelled"))
                    } else {
                        cont.resumeWithException(e)
                    }
                }

                override fun onResponse(call: Call, response: Response) {
                    activeUploadCalls.remove(id, call)
                    if (cont.isCancelled) {
                        response.close()
                        return
                    }
                    cont.resume(response)
                }
            })
        }

    private fun parseContentRangeTotal(value: String?): Long? {
        if (value.isNullOrBlank()) return null
        // Example: "bytes 100-999/12345"
        val totalPart = value.substringAfter('/').trim()
        if (totalPart.isEmpty() || totalPart == "*") return null
        return totalPart.toLongOrNull()
    }
    
    /**
     * Загрузить файл на транспортный сервер с отслеживанием прогресса
     * @param id Уникальный ID файла
     * @param content Содержимое файла (base64 или бинарные данные)
     * @return Tag для скачивания файла
     */
    suspend fun uploadFile(id: String, content: String): String = withContext(Dispatchers.IO) {
        val server = getActiveServer()
        RosettaDev1Log.i(
            "net/upload start id=${id.take(12)} server=$server bytes=${content.length}"
        )
        ProtocolRuntimeAccess.get().addLog("📤 Upload start: id=${id.take(8)}, server=$server")

        // Добавляем в список загрузок
        _uploading.value = _uploading.value + TransportState(id, 0)

        try {
            withRetry(operation = "upload", id = id) {
                val contentBytes = content.toByteArray(Charsets.UTF_8)
                val totalSize = contentBytes.size.toLong()

                val progressRequestBody = object : RequestBody() {
                    override fun contentType() = "application/octet-stream".toMediaType()
                    override fun contentLength() = totalSize

                    override fun writeTo(sink: okio.BufferedSink) {
                        val source = okio.Buffer().write(contentBytes)
                        var uploaded = 0L
                        val bufferSize = 8 * 1024L
                        var lastProgressUpdateMs = 0L

                        while (true) {
                            val read = source.read(sink.buffer, bufferSize)
                            if (read == -1L) break

                            uploaded += read
                            sink.flush()

                            val now = System.currentTimeMillis()
                            val isLast = uploaded >= totalSize
                            if (isLast || now - lastProgressUpdateMs >= 200) {
                                lastProgressUpdateMs = now
                                val progress = ((uploaded * 100) / totalSize).toInt()
                                _uploading.value = _uploading.value.map {
                                    if (it.id == id) it.copy(progress = progress) else it
                                }
                            }
                        }
                    }
                }

                val requestBody = MultipartBody.Builder()
                    .setType(MultipartBody.FORM)
                    .addFormDataPart("file", id, progressRequestBody)
                    .build()

                val request = Request.Builder()
                    .url("$server/u")
                    .post(requestBody)
                    .build()
                val response =
                    try {
                        withTimeout(UPLOAD_ATTEMPT_TIMEOUT_MS) {
                            awaitUploadResponse(id, request)
                        }
                    } catch (timeout: CancellationException) {
                        if (timeout is kotlinx.coroutines.TimeoutCancellationException) {
                            activeUploadCalls.remove(id)?.cancel()
                            RosettaDev1Log.w(
                                "net/upload attempt-timeout id=${id.take(12)} timeoutMs=$UPLOAD_ATTEMPT_TIMEOUT_MS"
                            )
                            throw SocketTimeoutException(
                                "Upload timeout after ${UPLOAD_ATTEMPT_TIMEOUT_MS}ms"
                            )
                        }
                        throw timeout
                    }

                val tag =
                    response.use { uploadResponse ->
                        if (!uploadResponse.isSuccessful) {
                            val errorBody = uploadResponse.body?.string()?.take(240).orEmpty()
                            RosettaDev1Log.e(
                                "net/upload http-fail id=${id.take(12)} code=${uploadResponse.code} body=$errorBody"
                            )
                            throw IOException("Upload failed: ${uploadResponse.code}")
                        }

                        val responseBody = uploadResponse.body?.string()
                            ?: throw IOException("Empty response")
                        org.json.JSONObject(responseBody).getString("t")
                    }

                // Обновляем прогресс до 100%
                _uploading.value = _uploading.value.map {
                    if (it.id == id) it.copy(progress = 100) else it
                }

                ProtocolRuntimeAccess.get().addLog("✅ Upload success: id=${id.take(8)}, tag=${tag.take(10)}")
                RosettaDev1Log.i("net/upload success id=${id.take(12)} tag=${tag.take(16)}")

                tag
            }
        } catch (e: CancellationException) {
            ProtocolRuntimeAccess.get().addLog("🛑 Upload cancelled: id=${id.take(8)}")
            RosettaDev1Log.w("net/upload cancelled id=${id.take(12)}", e)
            throw e
        } catch (e: Exception) {
            ProtocolRuntimeAccess.get().addLog(
                "❌ Upload failed: id=${id.take(8)}, reason=${e.javaClass.simpleName}: ${e.message ?: "unknown"}"
            )
            RosettaDev1Log.e(
                "net/upload failed id=${id.take(12)} reason=${e.javaClass.simpleName}:${e.message ?: "unknown"}",
                e
            )
            throw e
        } finally {
            activeUploadCalls.remove(id)?.cancel()
            // Удаляем из списка загрузок
            _uploading.value = _uploading.value.filter { it.id != id }
        }
    }
    
    /**
     * Скачать файл с транспортного сервера — СТРИМИНГ на диск.
     * Файл скачивается чанками по 64KB во временный файл, потом читается как String.
     * Это предотвращает OutOfMemoryError для больших файлов (30MB+).
     *
     * @param id Уникальный ID файла (для трекинга)
     * @param tag Tag файла на сервере
     * @return Содержимое файла
     */
    suspend fun downloadFile(
        id: String,
        tag: String,
        transportServer: String? = null
    ): String = withContext(Dispatchers.IO) {
        val server = getActiveServer(transportServer)
        ProtocolRuntimeAccess.get().addLog("📥 Download start: id=${id.take(8)}, tag=${tag.take(10)}, server=$server")

        // Добавляем в список скачиваний
        _downloading.value = _downloading.value + TransportState(id, 0)

        try {
            withRetry(operation = "download", id = id) {
                val request = Request.Builder()
                    .url("$server/d/$tag")
                    .get()
                    .build()

                val response = awaitDownloadResponse(id, request)

                if (!response.isSuccessful) {
                    throw IOException("Download failed: ${response.code}")
                }

                val body = response.body ?: throw IOException("Empty response body")
                val contentLength = body.contentLength()

                // Для маленьких файлов (<2MB) — читаем в память напрямую (быстрее)
                if (contentLength in 1..2_000_000) {
                    val content = body.string()
                    _downloading.value = _downloading.value.map {
                        if (it.id == id) it.copy(progress = 100) else it
                    }
                    ProtocolRuntimeAccess.get().addLog("✅ Download OK (mem): id=${id.take(8)}, size=${content.length}")
                    return@withRetry content
                }

                // Для больших файлов — стримим на диск чанками
                val cacheDir = appContext?.cacheDir ?: throw IOException("No app context for streaming download")
                val tempFile = File(cacheDir, "dl_${id.take(16)}_${System.currentTimeMillis()}.tmp")

                try {
                    var totalRead = 0L
                    val buffer = ByteArray(64 * 1024) // 64KB chunks

                    body.byteStream().use { inputStream ->
                        tempFile.outputStream().use { outputStream ->
                            while (true) {
                                val bytesRead = inputStream.read(buffer)
                                if (bytesRead == -1) break

                                outputStream.write(buffer, 0, bytesRead)
                                totalRead += bytesRead

                                // Обновляем прогресс
                                if (contentLength > 0) {
                                    val progress = ((totalRead * 100) / contentLength).toInt().coerceIn(0, 99)
                                    _downloading.value = _downloading.value.map {
                                        if (it.id == id) it.copy(progress = progress) else it
                                    }
                                }
                            }
                        }
                    }

                    // Проверяем целостность скачанного контента
                    if (contentLength > 0 && totalRead != contentLength) {
                        throw IOException("Incomplete download: expected=$contentLength, got=$totalRead")
                    }
                    if (totalRead == 0L) {
                        throw IOException("Empty download: 0 bytes received")
                    }

                    // Читаем результат из файла
                    val content = tempFile.readText(Charsets.UTF_8)

                    _downloading.value = _downloading.value.map {
                        if (it.id == id) it.copy(progress = 100) else it
                    }

                    ProtocolRuntimeAccess.get().addLog("✅ Download OK (stream): id=${id.take(8)}, size=$totalRead")
                    content
                } finally {
                    tempFile.delete()
                }
            }
        } catch (e: Exception) {
            ProtocolRuntimeAccess.get().addLog(
                "❌ Download failed: id=${id.take(8)}, reason=${e.javaClass.simpleName}: ${e.message ?: "unknown"}"
            )
            throw e
        } finally {
            activeDownloadCalls.remove(id)?.cancel()
            // Удаляем из списка скачиваний
            _downloading.value = _downloading.value.filter { it.id != id }
        }
    }
    
    /**
     * Получить прогресс скачивания файла
     */
    fun getDownloadProgress(id: String): Int {
        return _downloading.value.find { it.id == id }?.progress ?: -1
    }
    
    /**
     * Получить прогресс загрузки файла
     */
    fun getUploadProgress(id: String): Int {
        return _uploading.value.find { it.id == id }?.progress ?: -1
    }

    /**
     * Скачать файл с CDN напрямую во временный файл (без загрузки в память).
     * Вызывающий код отвечает за удаление файла.
     *
     * @param id Уникальный ID файла (для трекинга)
     * @param tag Tag файла на сервере
     * @return Временный файл с зашифрованным содержимым
     */
    suspend fun downloadFileRaw(
        id: String,
        tag: String,
        transportServer: String? = null
    ): File = withContext(Dispatchers.IO) {
        val cacheDir = appContext?.cacheDir ?: throw IOException("No app context")
        val tempFile = File(cacheDir, "dlr_${id.take(16)}_${System.currentTimeMillis()}.tmp")
        try {
            downloadFileRawResumable(
                id = id,
                tag = tag,
                targetFile = tempFile,
                resumeFromBytes = 0L,
                transportServer = transportServer
            )
        } catch (e: Exception) {
            tempFile.delete()
            throw e
        }
    }

    /**
     * Resumable download with HTTP Range support.
     * If server supports range (206), continues from `targetFile.length()`.
     * If not, safely restarts from zero and rewrites target file.
     */
    suspend fun downloadFileRawResumable(
        id: String,
        tag: String,
        targetFile: File,
        resumeFromBytes: Long = 0L,
        transportServer: String? = null
    ): File = withContext(Dispatchers.IO) {
        val server = getActiveServer(transportServer)
        ProtocolRuntimeAccess.get().addLog(
            "📥 Download raw(resume) start: id=${id.take(8)}, tag=${tag.take(10)}, server=$server, resume=$resumeFromBytes"
        )

        _downloading.value = _downloading.value.filter { it.id != id } + TransportState(id, 0)

        try {
            withRetry(operation = "download-raw-resume", id = id) {
                val existingBytes = if (targetFile.exists()) targetFile.length() else 0L
                val startOffset = maxOf(existingBytes, resumeFromBytes.coerceAtLeast(0L))
                    .coerceAtMost(existingBytes)

                val requestBuilder = Request.Builder()
                    .url("$server/d/$tag")
                    .get()
                if (startOffset > 0L) {
                    requestBuilder.addHeader("Range", "bytes=$startOffset-")
                }

                val response = awaitDownloadResponse(id, requestBuilder.build())
                if (!response.isSuccessful) {
                    throw IOException("Download failed: ${response.code}")
                }

                val body = response.body ?: throw IOException("Empty response body")
                val rangeAccepted = response.code == 206
                val writeFromOffset = if (rangeAccepted) startOffset else 0L
                val incomingLength = body.contentLength().coerceAtLeast(0L)
                val totalFromHeader = parseContentRangeTotal(response.header("Content-Range"))
                val totalBytes = when {
                    totalFromHeader != null && totalFromHeader > 0L -> totalFromHeader
                    incomingLength > 0L -> writeFromOffset + incomingLength
                    else -> -1L
                }

                if (writeFromOffset == 0L && targetFile.exists()) {
                    targetFile.delete()
                }
                targetFile.parentFile?.mkdirs()

                val append = writeFromOffset > 0L
                var totalRead = writeFromOffset
                val buffer = ByteArray(64 * 1024)

                body.byteStream().use { inputStream ->
                    java.io.FileOutputStream(targetFile, append).use { outputStream ->
                        while (true) {
                            coroutineContext.ensureActive()
                            val bytesRead = try {
                                inputStream.read(buffer)
                            } catch (e: IOException) {
                                if (!coroutineContext.isActive) {
                                    throw CancellationException("Download cancelled", e)
                                }
                                throw e
                            }
                            if (bytesRead == -1) break

                            outputStream.write(buffer, 0, bytesRead)
                            totalRead += bytesRead

                            if (totalBytes > 0L) {
                                val progress =
                                    ((totalRead * 100L) / totalBytes).toInt().coerceIn(0, 99)
                                _downloading.value = _downloading.value.map {
                                    if (it.id == id) it.copy(progress = progress) else it
                                }
                            }
                        }
                    }
                }

                if (totalBytes > 0L && totalRead < totalBytes) {
                    throw IOException(
                        "Incomplete download: expected=$totalBytes, got=$totalRead"
                    )
                }
                if (totalRead == 0L) {
                    throw IOException("Empty download: 0 bytes received")
                }

                _downloading.value = _downloading.value.map {
                    if (it.id == id) it.copy(progress = 100) else it
                }
                ProtocolRuntimeAccess.get().addLog(
                    "✅ Download raw(resume) OK: id=${id.take(8)}, size=$totalRead"
                )
                targetFile
            }
        } catch (e: Exception) {
            ProtocolRuntimeAccess.get().addLog(
                "❌ Download raw(resume) failed: id=${id.take(8)}, reason=${e.javaClass.simpleName}: ${e.message ?: "unknown"}"
            )
            throw e
        } finally {
            activeDownloadCalls.remove(id)?.cancel()
            _downloading.value = _downloading.value.filter { it.id != id }
        }
    }
}
