package dev.castarr.tv.data

import android.content.Context
import android.util.Log
import dev.castarr.tv.auth.DeviceAuth
import dev.castarr.tv.playlist.Channel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONArray
import org.json.JSONObject
import java.net.HttpURLConnection
import java.net.URL
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone

/**
 * Channel source backed by the Dispatcharr fork's Bearer API: channels,
 * groups, EPG grid and per-user favorites. Stream URLs go through the
 * Dispatcharr proxy, optionally with a selected output profile.
 */
class DispatcharrRepository(context: Context, private val auth: DeviceAuth) {

    private val prefs = context.getSharedPreferences("dispatcharr", Context.MODE_PRIVATE)
    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

    val channels = MutableStateFlow<List<Channel>>(emptyList())
    val favorites = MutableStateFlow<Set<Int>>(emptySet())
    val profiles = MutableStateFlow<List<String>>(emptyList())
    val epgUpdatedAt = MutableStateFlow(0L)
    val status = MutableStateFlow("")

    private var programmesByTvgId: Map<String, List<Programme>> = emptyMap()
    var outputProfile: String = ""

    init {
        runCatching {
            prefs.getString("channels_cache", null)?.let {
                channels.value = Channel.listFromJson(JSONArray(it))
            }
        }
    }

    fun refresh(onDone: (Result<Int>) -> Unit = {}) {
        scope.launch {
            val result = runCatching {
                status.value = "loading_channels"
                val token = auth.accessToken() ?: error("not logged in")
                val groups = fetchGroups(token)
                val list = fetchChannels(token, groups)
                channels.value = list
                prefs.edit().putString("channels_cache", Channel.listToJson(list).toString()).apply()
                launch { runCatching { refreshFavorites(token) } }
                launch { runCatching { refreshProfiles(token) } }
                launch {
                    runCatching { refreshEpg(token) }
                        .onFailure { Log.w(TAG, "epg failed: ${it.javaClass.simpleName}: ${it.message?.take(160)}") }
                }
                list.size
            }
            result.onFailure { Log.w(TAG, "refresh failed: ${it.javaClass.simpleName}: ${it.message?.take(160)}") }
            status.value = if (result.isSuccess) "" else "channels_error"
            onDone(result)
        }
    }

    fun nowNext(channel: Channel): NowNext =
        XmltvParser.nowNext(programmesByTvgId[channel.tvgId])

    /** Programmes of this channel between now and now + [windowMs]. */
    fun upcoming(channel: Channel, windowMs: Long): List<Programme> {
        val now = System.currentTimeMillis()
        return XmltvParser.programmesIn(programmesByTvgId[channel.tvgId], now, now + windowMs)
    }

    fun toggleFavorite(channel: Channel) {
        if (channel.backendId == 0) return
        scope.launch {
            runCatching {
                val token = auth.accessToken() ?: return@launch
                val isFavorite = channel.backendId in favorites.value
                request(
                    "${auth.serverUrl}/api/channels/favorites/${channel.backendId}/",
                    if (isFavorite) "DELETE" else "POST",
                    token,
                )
                favorites.value =
                    if (isFavorite) favorites.value - channel.backendId
                    else favorites.value + channel.backendId
            }.onFailure { Log.w(TAG, "favorite toggle failed", it) }
        }
    }

    // The live proxy resolves channels by UUID (integer ids 404).
    private fun streamUrl(uuid: String): String {
        val base = "${auth.serverUrl}/proxy/ts/stream/$uuid"
        return if (outputProfile.isNotEmpty()) "$base?output_profile=$outputProfile" else base
    }

    /** Re-derive stream URLs (e.g. after the output profile changed). */
    fun rebuildStreamUrls() {
        channels.value = channels.value.map { it.copy(url = streamUrl(it.streamKey)) }
    }

    private suspend fun fetchGroups(token: String): Map<Int, String> {
        val body = request("${auth.serverUrl}/api/channels/groups/?page_size=1000", "GET", token)
        val results = paginatedResults(body)
        return (0 until results.length()).associate {
            val obj = results.getJSONObject(it)
            obj.getInt("id") to obj.optString("name")
        }
    }

    private suspend fun fetchChannels(token: String, groups: Map<Int, String>): List<Channel> {
        val list = mutableListOf<Channel>()
        var url: String? = "${auth.serverUrl}/api/channels/channels/?page_size=500"
        while (url != null && list.size < 10_000) {
            val body = request(url, "GET", token)
            val json = runCatching { JSONObject(body) }.getOrNull()
            val results = json?.optJSONArray("results") ?: JSONArray(body)
            for (i in 0 until results.length()) {
                val obj = results.getJSONObject(i)
                val id = obj.getInt("id")
                val uuid = obj.optString("uuid")
                if (uuid.isEmpty() || obj.optBoolean("hidden_from_output")) continue
                list.add(
                    Channel(
                        name = obj.optString("effective_name").ifEmpty { obj.optString("name") },
                        url = streamUrl(uuid),
                        group = groups[obj.optInt("channel_group_id")].orEmpty(),
                        // The list serializer only carries logo ids; the
                        // cache endpoint serves the image without auth.
                        logo = (obj.optInt("effective_logo_id").takeIf { it > 0 }
                            ?: obj.optInt("logo_id").takeIf { it > 0 })
                            ?.let { "${auth.serverUrl.trimEnd('/')}/api/channels/logos/$it/cache/" }
                            .orEmpty(),
                        tvgId = obj.optString("effective_tvg_id").ifEmpty { obj.optString("tvg_id") },
                        backendId = id,
                        streamKey = uuid,
                    )
                )
            }
            url = json?.optString("next")?.takeIf { it.isNotEmpty() && it != "null" }
        }
        list.sortBy { it.name.lowercase() }
        return list
    }

    private suspend fun refreshFavorites(token: String) {
        val body = request("${auth.serverUrl}/api/channels/favorites/", "GET", token)
        val ids = JSONObject(body).optJSONArray("channels") ?: JSONArray()
        favorites.value = (0 until ids.length()).map { ids.getInt(it) }.toSet()
    }

    private suspend fun refreshProfiles(token: String) {
        val body = request("${auth.serverUrl}/api/core/outputprofiles/", "GET", token)
        val results = paginatedResults(body)
        profiles.value = (0 until results.length()).mapNotNull {
            val obj = results.getJSONObject(it)
            if (obj.optBoolean("is_active", true)) obj.optString("name") else null
        }
    }

    private suspend fun refreshEpg(token: String) {
        status.value = "loading_epg"
        val body = request("${auth.serverUrl}/api/epg/grid/", "GET", token)
        // Envelope is {"data": [...]}; tolerate bare arrays and "results" too.
        val root = runCatching { JSONObject(body) }.getOrNull()
        val results = root?.optJSONArray("data")
            ?: root?.optJSONArray("results")
            ?: runCatching { JSONArray(body) }.getOrElse { error("unexpected EPG envelope") }
        val format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US).apply {
            timeZone = TimeZone.getTimeZone("UTC")
        }
        val map = HashMap<String, MutableList<Programme>>()
        for (i in 0 until results.length()) {
            val obj = results.getJSONObject(i)
            val tvgId = obj.optString("tvg_id")
            if (tvgId.isEmpty()) continue
            val start = parseIso(obj.optString("start_time"), format)
            val stop = parseIso(obj.optString("end_time"), format)
            if (start == 0L || stop <= start) continue
            map.getOrPut(tvgId) { mutableListOf() }
                .add(Programme(start, stop, obj.optString("title")))
        }
        map.values.forEach { it.sortBy(Programme::start) }
        programmesByTvgId = map
        epgUpdatedAt.value = System.currentTimeMillis()
        status.value = ""
    }

    private fun parseIso(raw: String, format: SimpleDateFormat): Long = runCatching {
        format.parse(raw.substringBefore(".").substringBefore("+").removeSuffix("Z"))?.time ?: 0L
    }.getOrDefault(0L)

    private fun paginatedResults(body: String): JSONArray =
        runCatching { JSONObject(body).optJSONArray("results") }.getOrNull() ?: JSONArray(body)

    private suspend fun request(url: String, method: String, token: String): String =
        withContext(Dispatchers.IO) {
            val connection = URL(url).openConnection() as HttpURLConnection
            try {
                connection.requestMethod = method
                connection.connectTimeout = 10_000
                connection.readTimeout = 30_000
                connection.setRequestProperty("Authorization", "Bearer $token")
                val code = connection.responseCode
                if (code >= 400) error("HTTP $code from $url")
                connection.inputStream.bufferedReader().use { it.readText() }
            } finally {
                connection.disconnect()
            }
        }

    private companion object {
        const val TAG = "DispatcharrRepo"
    }
}
