package com.yaros.RadioUrl.helpers

import android.content.Context
import android.content.SharedPreferences
import com.google.gson.Gson
import com.google.gson.JsonParseException
import com.google.gson.reflect.TypeToken
import com.yaros.RadioUrl.core.APIInterface.data.Category
import com.yaros.RadioUrl.data.Station
import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap

/**
 * Менеджер кеширования для радиостанций и категорий.
 *
 * Особенности реализации:
 * - Собственный файл SharedPreferences (не дефолтный) — не разрастает
 *   и не тормозит основной PreferenceManager-файл настроек на слабых устройствах.
 * - In-memory слой поверх диска — повторные обращения в рамках одной сессии
 *   не парсят JSON заново.
 * - Версионирование формата кеша — при смене полей модели Station/Category
 *   старый кеш не роняет приложение, а просто сбрасывается.
 * - Защита от повреждённого JSON (частая причина краша на реальных
 *   устройствах при OOM/kill процесса во время записи).
 * - "Stale-if-error" — при сетевой ошибке можно отдать протухший кеш,
 *   чтобы пользователь не видел пустой экран.
 * - Мягкая деградация: если init() ещё не вызван, кеш не крашит приложение,
 *   а просто временно не работает (и логирует это).
 */
object CacheManager {

    private const val TAG = "CacheManager"

    // Отдельный файл, а не дефолтные PreferenceManager-настройки
    private const val PREFS_FILE_NAME = "swr_cache_manager"

    // Версия формата кеша. Увеличивайте при изменении полей Station/Category,
    // чтобы старые несовместимые данные автоматически сбрасывались.
    private const val CACHE_FORMAT_VERSION = 1
    private const val PREF_CACHE_FORMAT_VERSION = "cache_format_version"

    // Ключи для SharedPreferences
    private const val PREF_CACHE_ENABLED = "cache_enabled"
    private const val PREF_CACHE_MAX_SIZE = "cache_max_size_mb"
    private const val PREF_STATIONS_CACHE = "stations_cache"
    private const val PREF_STATIONS_CACHE_TIME = "stations_cache_time"
    private const val PREF_CATEGORIES_CACHE = "categories_cache"
    private const val PREF_CATEGORIES_CACHE_TIME = "categories_cache_time"
    private const val PREF_CATEGORY_STATIONS_PREFIX = "category_stations_"
    private const val PREF_CATEGORY_STATIONS_TIME_PREFIX = "category_stations_time_"

    // Время жизни кеша (по умолчанию 1 час)
    private const val CACHE_EXPIRATION_TIME = 60 * 60 * 1000L
    // Насколько дольше можно отдавать протухший кеш как fallback при ошибке сети
    private const val STALE_FALLBACK_WINDOW = 24 * 60 * 60 * 1000L // 24 часа

    // Размеры кеша в МБ
    const val CACHE_SIZE_SMALL = 10
    const val CACHE_SIZE_MEDIUM = 25
    const val CACHE_SIZE_LARGE = 50
    const val CACHE_SIZE_XLARGE = 100

    @Volatile
    private var sharedPreferences: SharedPreferences? = null
    private val gson = Gson()

    // Простой in-memory слой: избавляет от повторного чтения/парсинга JSON
    // на слабых устройствах в рамках одной "живой" сессии процесса.
    private data class MemoryEntry(val json: String, val timestamp: Long)
    private val memoryCache = ConcurrentHashMap<String, MemoryEntry>()

    fun init(context: Context) {
        if (sharedPreferences == null) {
            synchronized(this) {
                if (sharedPreferences == null) {
                    val prefs = context.applicationContext
                        .getSharedPreferences(PREFS_FILE_NAME, Context.MODE_PRIVATE)
                    sharedPreferences = prefs
                    migrateFormatIfNeeded(prefs)
                }
            }
        }
    }

    /**
     * Если версия формата кеша изменилась (например, обновили модель Station
     * новым полем без дефолта, и старый Gson.fromJson упадёт) — сбрасываем кеш
     * целиком одним махом вместо падения на первом же чтении.
     */
    private fun migrateFormatIfNeeded(prefs: SharedPreferences) {
        val storedVersion = prefs.getInt(PREF_CACHE_FORMAT_VERSION, -1)
        if (storedVersion != CACHE_FORMAT_VERSION) {
            Timber.tag(TAG).i("Cache format changed ($storedVersion -> $CACHE_FORMAT_VERSION), clearing cache")
            prefs.edit()
                .clear()
                .putInt(PREF_CACHE_FORMAT_VERSION, CACHE_FORMAT_VERSION)
                .apply()
        }
    }

    private fun prefs(): SharedPreferences? {
        val p = sharedPreferences
        if (p == null) {
            Timber.tag(TAG).w("CacheManager used before init() — caching disabled for this call")
        }
        return p
    }

    /** Проверка, включено ли кеширование */
    fun isCachingEnabled(): Boolean {
        return prefs()?.getBoolean(PREF_CACHE_ENABLED, true) ?: true
    }

    fun enableCaching() {
        prefs()?.edit()?.putBoolean(PREF_CACHE_ENABLED, true)?.apply()
    }

    fun disableCaching() {
        prefs()?.edit()?.putBoolean(PREF_CACHE_ENABLED, false)?.apply()
        clearCache()
    }

    fun getMaxCacheSize(): Int {
        return prefs()?.getInt(PREF_CACHE_MAX_SIZE, CACHE_SIZE_MEDIUM) ?: CACHE_SIZE_MEDIUM
    }

    fun setMaxCacheSize(sizeMb: Int) {
        prefs()?.edit()?.putInt(PREF_CACHE_MAX_SIZE, sizeMb)?.apply()
        enforceMaxCacheSize()
    }

    // ---------------------------------------------------------------------
    // Station list (все станции)
    // ---------------------------------------------------------------------

    fun cacheStations(stations: List<Station>) {
        writeEntry(PREF_STATIONS_CACHE, PREF_STATIONS_CACHE_TIME, stations, "stations")
    }

    fun getCachedStations(): List<Station>? {
        return readEntry(PREF_STATIONS_CACHE, PREF_STATIONS_CACHE_TIME, allowStale = false)
    }

    /** То же самое, но если кеш протух — вернёт его всё равно (для fallback при ошибке сети). */
    fun getCachedStationsOrStale(): List<Station>? {
        return readEntry(PREF_STATIONS_CACHE, PREF_STATIONS_CACHE_TIME, allowStale = true)
    }

    // ---------------------------------------------------------------------
    // Categories
    // ---------------------------------------------------------------------

    fun cacheCategories(categories: List<Category>) {
        writeEntry(PREF_CATEGORIES_CACHE, PREF_CATEGORIES_CACHE_TIME, categories, "categories")
    }

    fun getCachedCategories(): List<Category>? {
        return readEntry(PREF_CATEGORIES_CACHE, PREF_CATEGORIES_CACHE_TIME, allowStale = false)
    }

    fun getCachedCategoriesOrStale(): List<Category>? {
        return readEntry(PREF_CATEGORIES_CACHE, PREF_CATEGORIES_CACHE_TIME, allowStale = true)
    }

    // ---------------------------------------------------------------------
    // Stations by category
    // ---------------------------------------------------------------------

    fun cacheCategoryStations(categoryId: Int, stations: List<Station>) {
        writeEntry(
            "$PREF_CATEGORY_STATIONS_PREFIX$categoryId",
            "$PREF_CATEGORY_STATIONS_TIME_PREFIX$categoryId",
            stations,
            "category $categoryId stations"
        )
    }

    fun getCachedCategoryStations(categoryId: Int): List<Station>? {
        return readEntry(
            "$PREF_CATEGORY_STATIONS_PREFIX$categoryId",
            "$PREF_CATEGORY_STATIONS_TIME_PREFIX$categoryId",
            allowStale = false
        )
    }

    fun getCachedCategoryStationsOrStale(categoryId: Int): List<Station>? {
        return readEntry(
            "$PREF_CATEGORY_STATIONS_PREFIX$categoryId",
            "$PREF_CATEGORY_STATIONS_TIME_PREFIX$categoryId",
            allowStale = true
        )
    }

    // ---------------------------------------------------------------------
    // Общая реализация чтения/записи
    // ---------------------------------------------------------------------

    private inline fun <reified T> writeEntry(
        dataKey: String,
        timeKey: String,
        data: List<T>,
        logLabel: String
    ) {
        if (!isCachingEnabled()) return
        val p = prefs() ?: return
        try {
            val json = gson.toJson(data)
            val timestamp = System.currentTimeMillis()
            p.edit()
                .putString(dataKey, json)
                .putLong(timeKey, timestamp)
                .apply()
            memoryCache[dataKey] = MemoryEntry(json, timestamp)
            Timber.tag(TAG).d("Cached ${data.size} $logLabel")
            enforceMaxCacheSize()
        } catch (e: Exception) {
            Timber.tag(TAG).e(e, "Error caching $logLabel")
        }
    }

    private inline fun <reified T> readEntry(
        dataKey: String,
        timeKey: String,
        allowStale: Boolean
    ): List<T>? {
        if (!isCachingEnabled()) return null
        val p = prefs() ?: return null

        val now = System.currentTimeMillis()

        // Сначала пробуем in-memory слой — не трогаем диск лишний раз
        memoryCache[dataKey]?.let { mem ->
            val age = now - mem.timestamp
            if (age <= CACHE_EXPIRATION_TIME || (allowStale && age <= STALE_FALLBACK_WINDOW)) {
                return parseOrEvict(dataKey, mem.json)
            }
        }

        return try {
            val cacheTime = p.getLong(timeKey, 0)
            val age = now - cacheTime
            val withinFreshWindow = age <= CACHE_EXPIRATION_TIME
            val withinStaleWindow = allowStale && age <= STALE_FALLBACK_WINDOW

            if (!withinFreshWindow && !withinStaleWindow) {
                Timber.tag(TAG).d("Cache for $dataKey expired (age=${age}ms)")
                return null
            }

            val json = p.getString(dataKey, null) ?: return null
            memoryCache[dataKey] = MemoryEntry(json, cacheTime)
            parseOrEvict(dataKey, json)
        } catch (e: Exception) {
            Timber.tag(TAG).e(e, "Error retrieving cache for $dataKey")
            null
        }
    }

    private inline fun <reified T> parseOrEvict(dataKey: String, json: String): List<T>? {
        return try {
            val type = TypeToken.getParameterized(List::class.java, T::class.java).type
            gson.fromJson<List<T>>(json, type)
        } catch (e: JsonParseException) {
            // Повреждённый или несовместимый JSON — не роняем вызывающий код,
            // а тихо чистим битую запись, чтобы она не мешала при следующем чтении.
            Timber.tag(TAG).e(e, "Corrupted cache entry for $dataKey, evicting")
            evictEntry(dataKey)
            null
        }
    }

    private fun evictEntry(dataKey: String) {
        memoryCache.remove(dataKey)
        prefs()?.edit()?.remove(dataKey)?.apply()
    }

    // ---------------------------------------------------------------------
    // Обслуживание кеша
    // ---------------------------------------------------------------------

    /** Очистить весь кеш (кроме флагов enabled/max size/версии формата) */
    fun clearCache() {
        val p = prefs() ?: return
        try {
            val editor = p.edit()
            editor.remove(PREF_STATIONS_CACHE)
            editor.remove(PREF_STATIONS_CACHE_TIME)
            editor.remove(PREF_CATEGORIES_CACHE)
            editor.remove(PREF_CATEGORIES_CACHE_TIME)

            p.all.keys.forEach { key ->
                if (key.startsWith(PREF_CATEGORY_STATIONS_PREFIX) ||
                    key.startsWith(PREF_CATEGORY_STATIONS_TIME_PREFIX)
                ) {
                    editor.remove(key)
                }
            }
            editor.apply()
            memoryCache.clear()
            Timber.tag(TAG).d("Cache cleared")
        } catch (e: Exception) {
            Timber.tag(TAG).e(e, "Error clearing cache")
        }
    }

    /** Приблизительный размер кеша в байтах */
    fun getCacheSize(): Long {
        val p = prefs() ?: return 0L
        return try {
            var totalSize = 0L
            p.getString(PREF_STATIONS_CACHE, null)?.let { totalSize += it.length * 2L }
            p.getString(PREF_CATEGORIES_CACHE, null)?.let { totalSize += it.length * 2L }
            p.all.keys.forEach { key ->
                if (key.startsWith(PREF_CATEGORY_STATIONS_PREFIX)) {
                    p.getString(key, null)?.let { totalSize += it.length * 2L }
                }
            }
            totalSize
        } catch (e: Exception) {
            Timber.tag(TAG).e(e, "Error calculating cache size")
            0L
        }
    }

    fun getCacheSizeMB(): Double = getCacheSize() / (1024.0 * 1024.0)

    /**
     * Если кеш превысил заданный пользователем лимит (например, на устройствах
     * с малым объёмом памяти/хранилища) — сбрасываем самые "тяжёлые" данные
     * по категориям (они менее критичны, чем общий список станций).
     */
    private fun enforceMaxCacheSize() {
        val maxBytes = getMaxCacheSize() * 1024L * 1024L
        if (getCacheSize() <= maxBytes) return

        val p = prefs() ?: return
        Timber.tag(TAG).w("Cache exceeds max size, trimming per-category entries")
        val categoryKeys = p.all.keys.filter { it.startsWith(PREF_CATEGORY_STATIONS_PREFIX) }
        val editor = p.edit()
        categoryKeys.forEach { key ->
            editor.remove(key)
            editor.remove(key.replace(PREF_CATEGORY_STATIONS_PREFIX, PREF_CATEGORY_STATIONS_TIME_PREFIX))
            memoryCache.remove(key)
        }
        editor.apply()
    }
}