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

import okhttp3.HttpUrl

/**
 * High-level wrapper for the UPnP AVTransport service.
 * Covers SetAVTransportURI / Play / Pause / Stop / Seek /
 * GetPositionInfo / GetTransportInfo / queue management
 * (RemoveAllTracksFromQueue, AddURIToQueue, Next, Previous, SeekToTrack).
 */
// One method per AVTransport SOAP verb; splitting would obscure the 1:1 protocol mapping.
@Suppress("TooManyFunctions")
class AVTransportClient(
    private val soap: SoapClient,
    private val controlUrl: HttpUrl,
) {
    suspend fun setAVTransportURI(uri: String, metadata: String = "") {
        soap.call(
            controlUrl = controlUrl,
            serviceType = SERVICE_TYPE,
            action = "SetAVTransportURI",
            args = mapOf(
                "InstanceID" to "0",
                "CurrentURI" to uri,
                "CurrentURIMetaData" to metadata,
            ),
        )
    }

    /**
     * Convenience overload that builds DIDL-Lite metadata around [uri]
     * + [mime] + [title] and forwards to [setAVTransportURI]. Sonos
     * rejects empty DIDL with vendor error 1023; this constructs the
     * minimal-but-Sonos-acceptable shape:
     *   <DIDL-Lite>
     *     <item id="0" parentID="-1" restricted="1">
     *       <dc:title>...</dc:title>
     *       <upnp:class>object.item.audioItem.musicTrack</upnp:class>
     *       <res protocolInfo="http-get:*:<mime>:*">...</res>
     *     </item>
     *   </DIDL-Lite>
     * Generic UPnP renderers tolerate this shape too — there's no
     * downside to always sending it. Title falls back to "Minstrel"
     * when the caller doesn't supply one.
     */
    suspend fun setAVTransportURIWithMetadata(uri: String, mime: String, title: String) {
        val safeTitle = title.ifBlank { "Minstrel" }
        setAVTransportURI(uri, buildDidlLite(uri, mime, safeTitle))
    }

    /**
     * Remove all tracks from the renderer's queue. UPnP action name is
     * `RemoveAllTracksFromQueue`. Used at activation time to clear out any
     * leftover queue from prior sessions before loading our local queue.
     */
    suspend fun removeAllTracksFromQueue() {
        soap.call(
            controlUrl = controlUrl,
            serviceType = SERVICE_TYPE,
            action = "RemoveAllTracksFromQueue",
            args = mapOf("InstanceID" to "0"),
        )
    }

    /**
     * Remove a contiguous range of tracks from the renderer's native queue.
     * Sonos-specific extension to AVTransport; `UpdateID=0` skips the queue-
     * version check so this works without first calling GetQueue to learn
     * the current update id.
     *
     * [startingIndex] is 1-based per Sonos convention; [numberOfTracks] is
     * the count to remove. Used by OutputPickerController's incremental
     * queue resync path (radio-append: remove tail, then AddURIToQueue
     * the new items).
     */
    suspend fun removeTrackRangeFromQueue(startingIndex: Int, numberOfTracks: Int) {
        soap.call(
            controlUrl = controlUrl,
            serviceType = SERVICE_TYPE,
            action = "RemoveTrackRangeFromQueue",
            args = mapOf(
                "InstanceID" to "0",
                "UpdateID" to "0",
                "StartingIndex" to startingIndex.toString(),
                "NumberOfTracks" to numberOfTracks.toString(),
            ),
        )
    }

    /**
     * Append a track to the renderer's queue. Sonos returns the assigned
     * track number + new total queue length in the response, but we don't
     * read those (we know our intended position). DIDL-Lite metadata is
     * required; reuses the same shape as [setAVTransportURIWithMetadata].
     *
     * [enqueuedURIPosition] is 1-based; 0 means "append to end" per UPnP.
     * Our caller passes 1, 2, 3, ... to ensure stable order.
     */
    suspend fun addURIToQueue(
        uri: String,
        mime: String,
        title: String,
        enqueuedURIPosition: Int = 0,
    ) {
        val safeTitle = title.ifBlank { "Minstrel" }
        val didl = buildDidlLite(uri, mime, safeTitle)
        soap.call(
            controlUrl = controlUrl,
            serviceType = SERVICE_TYPE,
            action = "AddURIToQueue",
            args = mapOf(
                "InstanceID" to "0",
                "EnqueuedURI" to uri,
                "EnqueuedURIMetaData" to didl,
                "DesiredFirstTrackNumberEnqueued" to enqueuedURIPosition.toString(),
                "EnqueueAsNext" to "0",
            ),
        )
    }

    /** Skip to the next track in the renderer's queue. */
    suspend fun next() {
        soap.call(
            controlUrl = controlUrl,
            serviceType = SERVICE_TYPE,
            action = "Next",
            args = mapOf("InstanceID" to "0"),
        )
    }

    /** Skip to the previous track in the renderer's queue. */
    suspend fun previous() {
        soap.call(
            controlUrl = controlUrl,
            serviceType = SERVICE_TYPE,
            action = "Previous",
            args = mapOf("InstanceID" to "0"),
        )
    }

    /**
     * Seek to a specific track in the queue. UPnP Seek unit "TRACK_NR";
     * target is the 1-based track index. Separate from the existing
     * [seek] which uses unit REL_TIME for position-within-track.
     */
    suspend fun seekToTrack(trackNumber: Int) {
        soap.call(
            controlUrl = controlUrl,
            serviceType = SERVICE_TYPE,
            action = "Seek",
            args = mapOf(
                "InstanceID" to "0",
                "Unit" to "TRACK_NR",
                "Target" to trackNumber.toString(),
            ),
        )
    }

    suspend fun play() {
        soap.call(
            controlUrl = controlUrl,
            serviceType = SERVICE_TYPE,
            action = "Play",
            args = mapOf("InstanceID" to "0", "Speed" to "1"),
        )
    }

    suspend fun pause() {
        soap.call(
            controlUrl = controlUrl,
            serviceType = SERVICE_TYPE,
            action = "Pause",
            args = mapOf("InstanceID" to "0"),
        )
    }

    suspend fun stop() {
        soap.call(
            controlUrl = controlUrl,
            serviceType = SERVICE_TYPE,
            action = "Stop",
            args = mapOf("InstanceID" to "0"),
        )
    }

    suspend fun seek(positionMs: Long) {
        soap.call(
            controlUrl = controlUrl,
            serviceType = SERVICE_TYPE,
            action = "Seek",
            args = mapOf(
                "InstanceID" to "0",
                "Unit" to "REL_TIME",
                "Target" to formatHhMmSs(positionMs),
            ),
        )
    }

    suspend fun getPositionInfo(): PositionInfo {
        val result = soap.call(
            controlUrl = controlUrl,
            serviceType = SERVICE_TYPE,
            action = "GetPositionInfo",
            args = mapOf("InstanceID" to "0"),
        )
        return PositionInfo(
            track = result["Track"]?.toIntOrNull() ?: 0,
            trackUri = result["TrackURI"].orEmpty(),
            relTimeMs = parseHhMmSs(result["RelTime"].orEmpty()),
            trackDurationMs = parseHhMmSs(result["TrackDuration"].orEmpty()),
        )
    }

    suspend fun getTransportInfo(): TransportInfo {
        val result = soap.call(
            controlUrl = controlUrl,
            serviceType = SERVICE_TYPE,
            action = "GetTransportInfo",
            args = mapOf("InstanceID" to "0"),
        )
        val state = when (result["CurrentTransportState"]) {
            "PLAYING" -> TransportState.PLAYING
            "PAUSED_PLAYBACK" -> TransportState.PAUSED
            "STOPPED" -> TransportState.STOPPED
            "TRANSITIONING" -> TransportState.TRANSITIONING
            else -> TransportState.UNKNOWN
        }
        return TransportInfo(state)
    }

    private fun buildDidlLite(uri: String, mime: String, title: String): String {
        // Sonos requires (a) the rinconnetworks namespace declared on
        // <DIDL-Lite> even if we don't use Rincon elements directly, and
        // (b) a <desc id="cdudn"> element identifying the URI as an
        // external (non-Sonos-library) source. Without those, Sonos
        // discards our metadata content and regenerates its own with
        // class=object.item and the URL query string as the title
        // (logcat 2026-06-04 confirmed). Match SoCo's pattern.
        val safeTitle = title.ifBlank { "Minstrel" }
        return buildString {
            append("<DIDL-Lite ")
            append("xmlns=\"urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/\" ")
            append("xmlns:dc=\"http://purl.org/dc/elements/1.1/\" ")
            append("xmlns:upnp=\"urn:schemas-upnp-org:metadata-1-0/upnp/\" ")
            append("xmlns:r=\"urn:schemas-rinconnetworks-com:metadata-1-0/\">")
            append("<item id=\"-1\" parentID=\"-1\" restricted=\"true\">")
            append("<dc:title>").append(xmlEscape(safeTitle)).append("</dc:title>")
            append("<upnp:class>object.item.audioItem.musicTrack</upnp:class>")
            append("<res protocolInfo=\"http-get:*:").append(xmlEscape(mime))
            append(":*\">").append(xmlEscape(uri)).append("</res>")
            append("<desc id=\"cdudn\" ")
            append("nameSpace=\"urn:schemas-rinconnetworks-com:metadata-1-0/\">")
            append("RINCON_AssociatedZPUDN")
            append("</desc>")
            append("</item>")
            append("</DIDL-Lite>")
        }
    }

    private fun formatHhMmSs(positionMs: Long): String {
        val totalSec = (positionMs / MILLIS_PER_SECOND).coerceAtLeast(0)
        val h = totalSec / SECONDS_PER_HOUR
        val m = (totalSec % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE
        val s = totalSec % SECONDS_PER_MINUTE
        return "%d:%02d:%02d".format(h, m, s)
    }

    private fun parseHhMmSs(raw: String): Long {
        val parts = raw.split(':').mapNotNull { it.trim().toLongOrNull() }
        return if (parts.size == EXPECTED_HMS_PARTS) {
            val (h, m, s) = parts
            ((h * SECONDS_PER_HOUR) + (m * SECONDS_PER_MINUTE) + s) * MILLIS_PER_SECOND
        } else {
            0L
        }
    }

    private companion object {
        const val SERVICE_TYPE = "urn:schemas-upnp-org:service:AVTransport:1"
        const val MILLIS_PER_SECOND = 1000L
        const val SECONDS_PER_MINUTE = 60L
        const val SECONDS_PER_HOUR = 3600L
        const val EXPECTED_HMS_PARTS = 3
    }
}

data class PositionInfo(
    val track: Int,
    val trackUri: String,
    val relTimeMs: Long,
    val trackDurationMs: Long,
)

enum class TransportState { PLAYING, PAUSED, STOPPED, TRANSITIONING, UNKNOWN }

data class TransportInfo(val state: TransportState)
