package com.ponzischeme89.memby.data.remote

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.Request
import java.io.ByteArrayOutputStream
import java.util.concurrent.TimeUnit

/**
 * Reads seek preview thumbnails, which are the one thing in the app fetched as raw bytes
 * rather than through Retrofit or Coil.
 *
 * Retrofit is the wrong shape because the direct path wants a *byte range* of a file, and
 * Coil is the wrong home because these are small, transient and asked for in bursts: a
 * viewer holding Right walks through dozens of them, and letting that churn through the
 * artwork cache would evict the backdrops and posters the launcher is about to want again.
 * The player keeps its own small cache instead.
 */
internal object TrickplayClient {

    /**
     * A preview is worth having only while somebody is still pressing. Eight seconds is
     * already far past the point where the thumbnail would have answered the question, and
     * the seek it is describing has committed and moved on.
     */
    private const val TIMEOUT_SECONDS = 8L

    /** A frame is a few kilobytes and an index a few more. Nothing here is megabytes. */
    private const val MAX_BYTES = 4L * 1024L * 1024L

    private const val CHUNK_BYTES = 16 * 1024

    private val http by lazy {
        HttpStack.base.newBuilder()
            .connectTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
            .readTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
            .build()
    }

    /**
     * Fetches [url], optionally only the bytes in [range].
     *
     * Returns null on any failure. Nothing above this can do anything useful with the
     * reason: a preview that does not arrive leaves the seek indicator in the wordless form
     * it has always had, which is a complete answer rather than a degraded one.
     *
     * A server that ignores the range answers 200 with the whole file. Emby does honour it
     * on the BIF route while advertising `Accept-Ranges: none`, so the request is made on
     * the strength of what comes back — but the read is capped either way, because being
     * wrong about that must not turn a seek into a five-megabyte download.
     */
    suspend fun fetch(url: String, range: LongRange? = null): ByteArray? =
        withContext(Dispatchers.IO) {
            val builder = Request.Builder().url(url)
            range?.let { builder.header("Range", "bytes=${it.first}-${it.last}") }
            val limit = (range?.let { it.last - it.first + 1 } ?: MAX_BYTES)
                .coerceIn(0L, MAX_BYTES)
            runCatching {
                http.newCall(builder.build()).execute().use { response ->
                    val stream = response.body?.byteStream()
                    if (!response.isSuccessful || stream == null) return@use null
                    val collected = ByteArrayOutputStream()
                    val chunk = ByteArray(CHUNK_BYTES)
                    while (collected.size() < limit) {
                        val wanted = minOf(chunk.size.toLong(), limit - collected.size()).toInt()
                        val read = stream.read(chunk, 0, wanted)
                        if (read <= 0) break
                        collected.write(chunk, 0, read)
                    }
                    collected.toByteArray().takeIf { it.isNotEmpty() }
                }
            }.getOrNull()
        }
}
