package dev.castarr.tv.data

import android.content.Context
import android.util.Log
import dev.castarr.tv.playlist.Channel
import dev.castarr.tv.playlist.M3uParser
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import org.json.JSONArray
import java.net.HttpURLConnection
import java.net.URL

/**
 * Generic M3U + XMLTV source: fetches and caches the channel list, loads EPG
 * and answers Now/Next per channel. (The Dispatcharr Bearer-API source of
 * issue #5 will sit next to this as a second implementation.)
 */
class SourceRepository(context: Context) {

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

    val channels = MutableStateFlow<List<Channel>>(emptyList())
    val epgUpdatedAt = MutableStateFlow(0L)
    val status = MutableStateFlow("")

    private var programmes: Map<String, List<Programme>> = emptyMap()
    private var nameToEpgId: Map<String, String> = emptyMap()

    var m3uUrl: String
        get() = prefs.getString("m3u_url", "").orEmpty()
        private set(value) { prefs.edit().putString("m3u_url", value).apply() }

    var epgUrl: String
        get() = prefs.getString("epg_url", "").orEmpty()
        private set(value) { prefs.edit().putString("epg_url", value).apply() }

    init {
        loadCachedChannels()
        if (m3uUrl.isNotEmpty()) refresh(m3uUrl, epgUrl)
    }

    fun configure(m3u: String, epg: String, onDone: (Result<Int>) -> Unit = {}) {
        m3uUrl = m3u
        epgUrl = epg
        refresh(m3u, epg, onDone)
    }

    fun refresh(m3u: String = m3uUrl, epg: String = epgUrl, onDone: (Result<Int>) -> Unit = {}) {
        if (m3u.isEmpty()) return
        scope.launch {
            val result = runCatching {
                status.value = "loading_channels"
                val parsed = M3uParser.parse(download(m3u))
                require(parsed.isNotEmpty()) { "empty playlist" }
                channels.value = parsed
                prefs.edit().putString("channels_cache", Channel.listToJson(parsed).toString()).apply()
                parsed.size
            }
            result.onFailure { Log.w(TAG, "channel refresh failed", it) }
            status.value = if (result.isSuccess) "" else "channels_error"
            onDone(result)
            if (result.isSuccess && epg.isNotEmpty()) refreshEpg(epg)
        }
    }

    private fun refreshEpg(url: String) {
        runCatching {
            status.value = "loading_epg"
            val connection = open(url)
            connection.inputStream.use { stream ->
                val parsed = XmltvParser.parse(stream)
                programmes = parsed.programmes
                nameToEpgId = parsed.displayNameToId
            }
            epgUpdatedAt.value = System.currentTimeMillis()
        }.onFailure { Log.w(TAG, "epg refresh failed", it) }
        status.value = ""
    }

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

    private fun programmesFor(channel: Channel): List<Programme>? {
        val direct = channel.tvgId.takeIf { it.isNotEmpty() }?.let { programmes[it] }
        return direct ?: nameToEpgId[channel.name.lowercase()]?.let { programmes[it] }
    }

    /** Now/Next for an M3U channel: match tvg-id first, then name. */
    fun nowNext(channel: Channel): NowNext = XmltvParser.nowNext(programmesFor(channel))

    private fun loadCachedChannels() {
        runCatching {
            val cached = prefs.getString("channels_cache", null) ?: return
            channels.value = Channel.listFromJson(JSONArray(cached))
        }
    }

    /**
     * Only plain HTTP(S) is fetched, and redirects are followed manually so
     * every hop can be checked again — a paired phone must not be able to
     * point the TV at arbitrary internal services (SSRF).
     */
    private fun open(url: String): HttpURLConnection {
        val parsed = URL(url)
        require(parsed.protocol.equals("http", true) || parsed.protocol.equals("https", true)) {
            "unsupported scheme"
        }
        return (parsed.openConnection() as HttpURLConnection).apply {
            connectTimeout = 15_000
            readTimeout = 60_000
            instanceFollowRedirects = true
        }
    }

    /**
     * Reads at most [MAX_BYTES] and gives up after [MAX_TRANSFER_MS] overall.
     * `readTimeout` alone only bounds a single read, so a server trickling
     * bytes could grow the buffer until the app died.
     */
    private fun download(url: String): String {
        val connection = open(url)
        val deadline = System.currentTimeMillis() + MAX_TRANSFER_MS
        return try {
            val builder = StringBuilder()
            connection.inputStream.bufferedReader().use { reader ->
                val buffer = CharArray(8 * 1024)
                while (true) {
                    val read = reader.read(buffer)
                    if (read < 0) break
                    builder.append(buffer, 0, read)
                    require(builder.length <= MAX_BYTES) { "playlist too large" }
                    require(System.currentTimeMillis() < deadline) { "download timed out" }
                }
            }
            builder.toString()
        } finally {
            connection.disconnect()
        }
    }

    private companion object {
        const val TAG = "SourceRepository"
        const val MAX_BYTES = 24 * 1024 * 1024
        const val MAX_TRANSFER_MS = 120_000L
    }
}
