package com.github.libretube.api

import com.github.libretube.LibreTubeApp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import java.io.File
import java.security.MessageDigest
import java.util.concurrent.ConcurrentHashMap

/**
 * Small persistent JSON cache for API responses that are expensive to request repeatedly.
 *
 * This cache is deliberately separate from downloads: it accelerates navigation/replays and lets
 * the UI reuse recent metadata, while the Downloads database/files remain the guaranteed offline
 * source of truth.
 */
object ApiResponseCache {
    @PublishedApi
    internal const val MAX_CACHE_BYTES = 32L * 1024L * 1024L
    @PublishedApi
    internal const val MAX_MEMORY_ENTRIES = 80

    @PublishedApi
    internal data class MemoryEntry(val timestamp: Long, val json: String)

    @PublishedApi
    internal val memory = ConcurrentHashMap<String, MemoryEntry>()
    @PublishedApi
    internal val cacheDir: File
        get() = File(LibreTubeApp.instance.cacheDir, "api_response_cache").apply { mkdirs() }

    suspend inline fun <reified T> get(
        key: String,
        maxAgeMs: Long
    ): T? = withContext(Dispatchers.IO) {
        val now = System.currentTimeMillis()
        val hashed = hashKey(key)

        memory[hashed]?.let { entry ->
            val age = now - entry.timestamp
            if (age <= maxAgeMs) {
                return@withContext runCatching {
                    JsonHelper.json.decodeFromString<T>(entry.json)
                }.getOrNull()
            }
        }

        val file = File(cacheDir, "$hashed.json")
        if (!file.exists()) return@withContext null

        val text = runCatching { file.readText() }.getOrNull() ?: return@withContext null
        val newline = text.indexOf('\n')
        if (newline <= 0) return@withContext null

        val timestamp = text.substring(0, newline).toLongOrNull() ?: return@withContext null
        val age = now - timestamp
        if (age > maxAgeMs) {
            // Keep the entry on disk. A caller may intentionally retry with a larger stale TTL
            // after a network failure, and the size-based LRU cleanup will evict old files later.
            return@withContext null
        }

        val json = text.substring(newline + 1)
        memory[hashed] = MemoryEntry(timestamp, json)
        trimMemory()

        runCatching { JsonHelper.json.decodeFromString<T>(json) }.getOrNull()
    }

    suspend inline fun <reified T> put(key: String, value: T) = withContext(Dispatchers.IO) {
        val hashed = hashKey(key)
        val timestamp = System.currentTimeMillis()
        val json = JsonHelper.json.encodeToString(value)

        memory[hashed] = MemoryEntry(timestamp, json)
        trimMemory()

        val file = File(cacheDir, "$hashed.json")
        runCatching {
            file.writeText("$timestamp\n$json")
            file.setLastModified(timestamp)
        }
        trimDisk()
    }

    suspend fun remove(key: String) = withContext(Dispatchers.IO) {
        val hashed = hashKey(key)
        memory.remove(hashed)
        runCatching { File(cacheDir, "$hashed.json").delete() }
        Unit
    }

    @PublishedApi
    internal fun hashKey(value: String): String {
        val bytes = MessageDigest.getInstance("SHA-256").digest(value.toByteArray())
        return bytes.joinToString("") { "%02x".format(it) }
    }

    @PublishedApi
    internal fun trimMemory() {
        if (memory.size <= MAX_MEMORY_ENTRIES) return
        memory.entries
            .sortedBy { it.value.timestamp }
            .take(memory.size - MAX_MEMORY_ENTRIES)
            .forEach { memory.remove(it.key) }
    }

    @PublishedApi
    internal fun trimDisk() {
        val files = cacheDir.listFiles()?.filter(File::isFile).orEmpty()
        var total = files.sumOf(File::length)
        if (total <= MAX_CACHE_BYTES) return

        files.sortedBy(File::lastModified).forEach { file ->
            if (total <= MAX_CACHE_BYTES) return
            total -= file.length()
            memory.remove(file.nameWithoutExtension)
            runCatching { file.delete() }
        }
    }
}
