package dev.castarr.tv.playlist

object M3uParser {

    private const val MAX_CHANNELS = 5000
    private const val BOM = '﻿'

    private val attrRegex = Regex("""([\w-]+)="([^"]*)"""")

    /** Thrown when the payload is not a playlist at all (HTML, JSON, …). */
    class NotAPlaylistException : IllegalArgumentException("not an M3U playlist")

    /**
     * Parses an M3U playlist. Rejects payloads that carry no playlist markers
     * at all — pointing the app at a 404 page used to silently replace the
     * channel list with HTML fragments.
     */
    fun parse(content: String): List<Channel> {
        // A UTF-8 BOM is not whitespace, so trim() leaves it in place and the
        // first line stops looking like a comment.
        val text = content.trimStart(BOM)
        if (!looksLikePlaylist(text)) throw NotAPlaylistException()

        val channels = mutableListOf<Channel>()
        var name = ""
        var group = ""
        var logo = ""
        var tvgId = ""
        var pendingInfo = false

        for (rawLine in text.lineSequence()) {
            val line = rawLine.trim().trimStart(BOM)
            when {
                line.startsWith("#EXTINF", ignoreCase = true) -> {
                    val attrs = attrRegex.findAll(line).associate { it.groupValues[1].lowercase() to it.groupValues[2] }
                    group = attrs["group-title"].orEmpty()
                    logo = attrs["tvg-logo"].orEmpty()
                    tvgId = attrs["tvg-id"].orEmpty()
                    name = displayName(line)
                    if (name.isEmpty()) name = attrs["tvg-name"].orEmpty()
                    pendingInfo = true
                }
                line.startsWith("#EXTGRP", ignoreCase = true) -> {
                    group = line.substringAfter(':', "").trim()
                }
                line.isEmpty() || line.startsWith("#") -> Unit
                pendingInfo -> {
                    channels.add(Channel(name.ifEmpty { line }, line, group, logo, tvgId))
                    if (channels.size >= MAX_CHANNELS) return channels
                    name = ""
                    group = ""
                    logo = ""
                    tvgId = ""
                    pendingInfo = false
                }
                else -> {
                    // Bare URL without #EXTINF — still a playable entry.
                    channels.add(Channel(line, line, group, ""))
                    if (channels.size >= MAX_CHANNELS) return channels
                }
            }
        }
        return channels
    }

    private fun looksLikePlaylist(text: String): Boolean {
        val head = text.lineSequence().take(50)
        return head.any {
            val line = it.trim().trimStart(BOM)
            line.startsWith("#EXTM3U", ignoreCase = true) ||
                line.startsWith("#EXTINF", ignoreCase = true)
        }
    }

    /**
     * The display name is everything after the FIRST comma that follows the
     * duration field — `substringAfterLast` swallowed every name containing
     * a comma ("Comedy Central, HD" became "HD").
     */
    private fun displayName(extinf: String): String {
        val payload = extinf.substringAfter(':', "")
        val comma = firstUnquotedComma(payload)
        return if (comma < 0) "" else payload.substring(comma + 1).trim()
    }

    /** Commas inside attribute values (group-title="A, B") do not separate. */
    private fun firstUnquotedComma(payload: String): Int {
        var inQuotes = false
        payload.forEachIndexed { index, c ->
            when {
                c == '"' -> inQuotes = !inQuotes
                c == ',' && !inQuotes -> return index
            }
        }
        return -1
    }
}
