package com.fabledsword.minstrel.player.output.upnp

import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserFactory

/**
 * Pull-parsed UPnP device description (the XML returned from a
 * discovered LOCATION URL). Carries the bits we actually need for
 * the picker: friendlyName / manufacturer / modelName for display,
 * AVTransport + RenderingControl control URLs for command dispatch.
 *
 * Filtered to MediaRenderer-capable devices — anything without an
 * AVTransport service control URL is dropped by [parse] returning
 * null (we can't make it play).
 */
data class DeviceDescription(
    val udn: String,
    val friendlyName: String,
    val manufacturer: String,
    val modelName: String,
    val avTransportControlUrl: HttpUrl,
    val renderingControlUrl: HttpUrl?,
    val zoneGroupTopologyControlUrl: HttpUrl? = null,
) {
    companion object {
        private const val AVT_SERVICE_TYPE = "urn:schemas-upnp-org:service:AVTransport:1"
        private const val RC_SERVICE_TYPE = "urn:schemas-upnp-org:service:RenderingControl:1"
        private const val ZGT_SERVICE_TYPE = "urn:schemas-upnp-org:service:ZoneGroupTopology:1"

        private const val TAG_SERVICE = "service"
        private const val TAG_UDN = "UDN"
        private const val TAG_FRIENDLY_NAME = "friendlyName"
        private const val TAG_MANUFACTURER = "manufacturer"
        private const val TAG_MODEL_NAME = "modelName"
        private const val TAG_SERVICE_TYPE = "serviceType"
        private const val TAG_CONTROL_URL = "controlURL"

        /**
         * Parse [xml] (the body fetched from the SSDP LOCATION URL),
         * resolving relative service control URLs against [base].
         * Returns null when AVTransport is missing — we have no way
         * to control the device without it.
         */
        fun parse(xml: String, base: HttpUrl): DeviceDescription? {
            val parser = XmlPullParserFactory.newInstance().newPullParser().apply {
                setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true)
                setInput(xml.reader())
            }
            val acc = ParseState()
            while (parser.eventType != XmlPullParser.END_DOCUMENT) {
                handleEvent(parser, acc, base)
                parser.next()
            }
            val avt = acc.avtControlUrl ?: return null
            return DeviceDescription(
                udn = acc.udn,
                friendlyName = acc.friendlyName,
                manufacturer = acc.manufacturer,
                modelName = acc.modelName,
                avTransportControlUrl = avt,
                renderingControlUrl = acc.rcControlUrl,
                zoneGroupTopologyControlUrl = acc.zgtControlUrl,
            )
        }

        private fun handleEvent(parser: XmlPullParser, acc: ParseState, base: HttpUrl) {
            when (parser.eventType) {
                XmlPullParser.START_TAG -> handleStartTag(parser, acc)
                XmlPullParser.END_TAG -> handleEndTag(parser, acc, base)
                else -> Unit
            }
        }

        private fun handleStartTag(parser: XmlPullParser, acc: ParseState) {
            when (parser.name) {
                TAG_SERVICE -> {
                    acc.inService = true
                    acc.serviceType = ""
                    acc.serviceControlUrl = ""
                }
                TAG_UDN -> acc.udn = parser.nextTextSafe()
                TAG_FRIENDLY_NAME -> acc.friendlyName = parser.nextTextSafe()
                TAG_MANUFACTURER -> acc.manufacturer = parser.nextTextSafe()
                TAG_MODEL_NAME -> acc.modelName = parser.nextTextSafe()
                TAG_SERVICE_TYPE -> if (acc.inService) acc.serviceType = parser.nextTextSafe()
                TAG_CONTROL_URL -> if (acc.inService) acc.serviceControlUrl = parser.nextTextSafe()
            }
        }

        private fun handleEndTag(parser: XmlPullParser, acc: ParseState, base: HttpUrl) {
            if (parser.name != TAG_SERVICE) return
            val resolved = resolveControlUrl(base, acc.serviceControlUrl)
            when (acc.serviceType) {
                AVT_SERVICE_TYPE -> acc.avtControlUrl = resolved
                RC_SERVICE_TYPE -> acc.rcControlUrl = resolved
                ZGT_SERVICE_TYPE -> acc.zgtControlUrl = resolved
            }
            acc.inService = false
        }

        private fun resolveControlUrl(base: HttpUrl, path: String): HttpUrl? {
            if (path.isBlank()) return null
            return path.toHttpUrlOrNull() ?: base.resolve(path)
        }

        private fun XmlPullParser.nextTextSafe(): String =
            runCatching { nextText() }.getOrDefault("")
    }

    /**
     * Mutable accumulator used during pull-parsing. Lives only for the
     * duration of one [parse] call.
     */
    private class ParseState {
        var udn: String = ""
        var friendlyName: String = ""
        var manufacturer: String = ""
        var modelName: String = ""
        var avtControlUrl: HttpUrl? = null
        var rcControlUrl: HttpUrl? = null
        var zgtControlUrl: HttpUrl? = null
        var inService: Boolean = false
        var serviceType: String = ""
        var serviceControlUrl: String = ""
    }
}
