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

import android.content.Context
import android.net.wifi.WifiManager
import androidx.core.content.getSystemService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.DatagramPacket
import java.net.InetAddress
import java.net.MulticastSocket
import java.nio.charset.StandardCharsets

/**
 * UDP multicast SSDP listener + M-SEARCH sender. Emits each discovery
 * response's LOCATION URL on the [discoveries] SharedFlow; the
 * `UpnpDiscoveryController` follows up by fetching + parsing each
 * device description.
 *
 * Passive listen is always-on once [start] is called (NOTIFY packets
 * speakers send when they boot / refresh). Active discovery (an
 * explicit M-SEARCH request) is triggered by [requestActiveScan] —
 * called when the picker sheet opens so newly-paired devices appear
 * promptly.
 *
 * WifiManager.MulticastLock is held while the listener is running.
 * Released by [stop] / cancellation of the parent scope.
 */
class SsdpDiscovery(
    private val context: Context,
) {
    private val discoveriesInternal =
        MutableSharedFlow<String>(extraBufferCapacity = DISCOVERY_BUFFER_CAPACITY)
    val discoveries: SharedFlow<String> = discoveriesInternal.asSharedFlow()

    private var multicastLock: WifiManager.MulticastLock? = null
    private var socket: MulticastSocket? = null
    private var listenJob: Job? = null

    fun start(scope: CoroutineScope) {
        if (listenJob != null) return
        val wifi = context.getSystemService<WifiManager>() ?: return
        multicastLock = wifi.createMulticastLock(MULTICAST_LOCK_TAG).apply {
            setReferenceCounted(false)
            acquire()
        }
        val sock = MulticastSocket(ANY_LOCAL_PORT).apply {
            joinGroup(InetAddress.getByName(SSDP_MULTICAST_ADDR))
        }
        socket = sock
        listenJob = scope.launch(Dispatchers.IO) {
            val buf = ByteArray(SOCKET_READ_BUFFER_BYTES)
            val packet = DatagramPacket(buf, buf.size)
            while (isActive) {
                runCatching { sock.receive(packet) }.onSuccess {
                    val raw = String(packet.data, 0, packet.length, StandardCharsets.UTF_8)
                    parseLocation(raw)?.let { discoveriesInternal.tryEmit(it) }
                }
            }
        }
    }

    fun stop() {
        listenJob?.cancel()
        listenJob = null
        runCatching { socket?.close() }
        socket = null
        runCatching { multicastLock?.release() }
        multicastLock = null
    }

    /**
     * Send an M-SEARCH packet asking for MediaRenderer:1 devices.
     * Responses arrive on the listener socket and emit via
     * [discoveries] as their LOCATION URL.
     */
    suspend fun requestActiveScan() = withContext(Dispatchers.IO) {
        val sock = socket ?: return@withContext
        val payload = buildMSearchPayload().toByteArray(StandardCharsets.UTF_8)
        val packet = DatagramPacket(
            payload,
            payload.size,
            InetAddress.getByName(SSDP_MULTICAST_ADDR),
            SSDP_PORT,
        )
        runCatching { sock.send(packet) }
        Unit
    }

    private fun buildMSearchPayload(): String = buildString {
        append("M-SEARCH * HTTP/1.1\r\n")
        append("HOST: ").append(SSDP_MULTICAST_ADDR).append(":").append(SSDP_PORT).append("\r\n")
        append("MAN: \"ssdp:discover\"\r\n")
        append("MX: ").append(MSEARCH_MX_SECONDS).append("\r\n")
        append("ST: ").append(MEDIA_RENDERER_TARGET).append("\r\n")
        append("\r\n")
    }

    private fun parseLocation(raw: String): String? {
        raw.lineSequence().forEach { line ->
            val trimmed = line.trim()
            if (trimmed.startsWith(LOCATION_HEADER, ignoreCase = true)) {
                return trimmed.substring(LOCATION_HEADER.length).trim()
            }
        }
        return null
    }

    private companion object {
        const val SSDP_MULTICAST_ADDR = "239.255.255.250"
        const val SSDP_PORT = 1900
        const val ANY_LOCAL_PORT = 0
        const val SOCKET_READ_BUFFER_BYTES = 4096
        const val DISCOVERY_BUFFER_CAPACITY = 32
        const val MSEARCH_MX_SECONDS = 2
        const val MULTICAST_LOCK_TAG = "minstrel.upnp.ssdp"
        const val MEDIA_RENDERER_TARGET = "urn:schemas-upnp-org:device:MediaRenderer:1"
        const val LOCATION_HEADER = "LOCATION:"
    }
}
