package com.ponzischeme89.memby.data

import com.ponzischeme89.memby.data.model.MediaStream
import kotlinx.serialization.Serializable
import java.net.URI
import java.net.URLEncoder

@Serializable
data class PlayableSubtitle(
    val id: String = "",
    val url: String,
    val mimeType: String,
    val language: String? = null,
    val label: String? = null,
    val isDefault: Boolean = false,
    val isForced: Boolean = false,
    val isHearingImpaired: Boolean = false,
    val deliveryMethod: String = "External",
    val codec: String? = null,
)

internal fun subtitleTracks(
    streams: List<MediaStream>,
    serverUrl: String,
    token: String,
    itemId: String,
    mediaSourceId: String,
): List<PlayableSubtitle> = streams.mapNotNull { stream ->
    if (!stream.type.equals("Subtitle", true) || stream.index < 0) {
        return@mapNotNull null
    }
    val sourceId = mediaSourceId.ifBlank { itemId }
    val method = stream.deliveryMethod?.takeIf(String::isNotBlank)
        ?: if (stream.isTextSubtitleStream) "External" else "Encode"
    val mime = subtitleMimeType(stream.codec, stream.deliveryUrl.orEmpty())
    val extension = subtitleExtension(stream.codec, stream.deliveryUrl.orEmpty())
    val canonical = "/Videos/${pathSegment(itemId)}/${pathSegment(sourceId)}" +
        "/Subtitles/${stream.index}/Stream.$extension"
    val delivery = stream.deliveryUrl?.takeIf(String::isNotBlank) ?: canonical
    PlayableSubtitle(
        id = stream.index.toString(),
        url = if (method.equals("External", true) && mime != null) {
            authenticatedDeliveryUrl(serverUrl, delivery, token)
        } else "",
        mimeType = mime.orEmpty(),
        language = stream.language?.trim()?.takeIf(String::isNotEmpty),
        label = (stream.displayTitle ?: stream.title)?.trim()?.takeIf(String::isNotEmpty),
        isDefault = stream.isDefault,
        isForced = stream.isForced,
        isHearingImpaired = stream.isHearingImpaired || listOfNotNull(stream.title, stream.displayTitle).any {
            it.contains("sdh", true) || it.contains("hearing", true)
        },
        deliveryMethod = method,
        codec = stream.codec,
    )
}.distinctBy { it.id }

/**
 * "No preference": keep the flag-driven behaviour somebody who has never opened the
 * subtitle menu should still get.
 */
const val SUBTITLE_LANGUAGE_AUTO = "auto"

/**
 * ISO 639-1 codes are the vocabulary, because that is what media3 reports for a track after
 * its own normalisation. The aliases are the three-letter forms Emby actually writes into a
 * stream's language field — for several languages, two of them for the same thing.
 *
 * The same table exists in the gateway (`internal/api/subtitles.go`), which is what makes
 * the two paths agree on what "Italian" means. Add a language to one and add it to both.
 */
private val subtitleLanguageAliases: Map<String, String> = buildMap {
    fun language(code: String, vararg aliases: String) {
        put(code, code)
        aliases.forEach { put(it, code) }
    }
    language("en", "eng")
    language("it", "ita")
    language("es", "spa", "esp")
    language("fr", "fre", "fra")
    language("de", "ger", "deu")
    language("pt", "por")
    language("nl", "dut", "nld")
    language("sv", "swe")
    language("da", "dan")
    language("no", "nor", "nob", "nno")
    language("fi", "fin")
    language("pl", "pol")
    language("cs", "cze", "ces")
    language("hu", "hun")
    language("ro", "rum", "ron")
    language("el", "gre", "ell")
    language("ru", "rus")
    language("uk", "ukr")
    language("tr", "tur")
    language("ar", "ara")
    language("he", "heb", "iw")
    language("hi", "hin")
    language("ja", "jpn")
    language("ko", "kor")
    language("zh", "chi", "zho", "cmn", "yue")
    language("th", "tha")
    language("vi", "vie")
}

/**
 * Folds a track's language onto that vocabulary. An unrecognised code survives as itself
 * rather than becoming empty, so an exact match on a language the table has no entry for
 * still works.
 */
fun normalizeSubtitleLanguage(raw: String?): String {
    val value = raw?.trim()?.lowercase().orEmpty()
    if (value.isEmpty()) return ""
    val base = value.takeWhile { it != '-' && it != '_' }
    return subtitleLanguageAliases[base] ?: base
}

/** The parts of a subtitle track [selectSubtitleId] needs, whatever produced it. */
data class SubtitleCandidate(
    val id: String,
    val language: String?,
    val isDefault: Boolean = false,
    val isForced: Boolean = false,
    val isHearingImpaired: Boolean = false,
)

/**
 * Which track to turn on, or null for none. The rules mirror `selectSubtitle` in the
 * gateway exactly, because the direct-to-Emby path has no gateway to ask and a viewer must
 * not get different subtitles depending on whether the container is up:
 *
 * - Off means off.
 * - A chosen language wins, and within it a plain full track beats a forced or
 *   hearing-impaired one — somebody who picked Italian wants the dialogue, not the
 *   signs-only track that happens to come first.
 * - A chosen language the title does not have falls back to a *forced* track only. Forced
 *   subtitles translate what is foreign to the film's own audio, so they are wanted either
 *   way; falling through to English instead would put a language nobody asked for on screen.
 * - With no language chosen: forced, then default, then the first track — which is what an
 *   install that has never touched the setting keeps.
 */
fun selectSubtitleId(
    candidates: List<SubtitleCandidate>,
    enabled: Boolean,
    language: String,
): String? {
    if (!enabled || candidates.isEmpty()) return null
    val preferred = normalizeSubtitleLanguage(language)
    if (preferred.isNotEmpty() && preferred != SUBTITLE_LANGUAGE_AUTO) {
        bestSubtitleInLanguage(candidates, preferred)?.let { return it }
        return candidates.firstOrNull { it.isForced }?.id
    }
    return candidates.firstOrNull { it.isForced }?.id
        ?: candidates.firstOrNull { it.isDefault }?.id
        ?: candidates.first().id
}

// Forced sorts last within a language precisely because it is not a substitute for the full
// track somebody asked for.
private fun bestSubtitleInLanguage(
    candidates: List<SubtitleCandidate>,
    language: String,
): String? = candidates
    .filter { normalizeSubtitleLanguage(it.language) == language }
    .maxByOrNull {
        when {
            it.isForced -> 1
            it.isHearingImpaired -> 2
            it.isDefault -> 5
            else -> 4
        }
    }
    ?.id

internal fun PlayableSubtitle.asCandidate(): SubtitleCandidate = SubtitleCandidate(
    id = id,
    language = language,
    isDefault = isDefault,
    isForced = isForced,
    isHearingImpaired = isHearingImpaired,
)

private fun subtitleExtension(codec: String?, url: String): String =
    when (codec?.trim()?.lowercase()) {
        "subrip" -> "srt"
        "webvtt" -> "vtt"
        "tx3g" -> "mov_text"
        else -> codec?.trim()?.lowercase()?.takeIf(String::isNotEmpty)
            ?: url.substringBefore('?').substringAfterLast('.', "vtt").lowercase()
    }

private fun pathSegment(value: String): String =
    URLEncoder.encode(value, "UTF-8").replace("+", "%20")

internal fun subtitleMimeType(codec: String?, url: String = ""): String? =
    when ((codec?.trim()?.lowercase()?.takeIf(String::isNotEmpty)
        ?: url.substringBefore('?').substringAfterLast('.', "").lowercase())) {
        "srt", "subrip" -> "application/x-subrip"
        "vtt", "webvtt" -> "text/vtt"
        "ass", "ssa" -> "text/x-ssa"
        "ttml", "dfxp" -> "application/ttml+xml"
        "tx3g", "mov_text" -> "application/x-quicktime-tx3g"
        else -> null
    }

internal fun authenticatedDeliveryUrl(serverUrl: String, deliveryUrl: String, token: String): String {
    val absolute = if (runCatching { URI(deliveryUrl).isAbsolute }.getOrDefault(false)) {
        deliveryUrl
    } else {
        serverUrl.trimEnd('/') + "/" + deliveryUrl.trimStart('/')
    }
    if (token.isBlank() || Regex("""(?:[?&])api_key=""", RegexOption.IGNORE_CASE).containsMatchIn(absolute)) {
        return absolute
    }
    val separator = if ('?' in absolute) '&' else '?'
    return absolute + separator + "api_key=" + URLEncoder.encode(token, "UTF-8")
}
