package com.fmdxconnector

import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.util.Log
import androidx.core.app.NotificationCompat
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableSharedFlow
import android.net.Uri
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.*
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import java.net.DatagramPacket
import java.net.DatagramSocket
import java.net.InetAddress
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.math.round

// Data classes are in Models.kt and imported automatically

class NetworkService : Service() {

    // ---------------- Lifecycle / Scope ----------------
    private val serviceJob = Job()
    private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)

    // ---------------- Sockets ----------------
    private var textWebSocket: WebSocket? = null
    private var pluginsWebSocket: WebSocket? = null
    private var udpSenderSocket: DatagramSocket? = null
    private var udpListenerSocket: DatagramSocket? = null

    // ---------------- State ----------------
    @Volatile private var isSessionAuthenticated = false
    @Volatile private var isAuthFinalized = false // Lock auth state after first response
    @Volatile private var currentFreqKhz: Double = 0.0
    @Volatile private var activeServerId: Int = 0
    private var activeServerIndex: Int = 0

    // Search-Command-Überwachung
    private var pendingSearchCommand = false
    private var searchTimeoutJob: Job? = null

    private var hostWithPort: String = ""
    private var currentAdminPassword: String = ""

    // Scanner-State + Debounce
    private var scannerStatusFlag = "0"
    companion object {
        const val NOTIFICATION_CHANNEL_ID = "FMDXConnectorChannel"
        const val NOTIFICATION_ID = 1
        const val TAG = "NetworkService"
        const val ACTION_STATUS_UPDATE = "com.example.fmdxconnector.STATUS_UPDATE"
        const val ACTION_UDP_COMMAND = "com.example.fmdxconnector.UDP_COMMAND"
        const val UDP_TARGET_PORT = 9100
        const val UDP_LISTEN_PORT = 9031

        // Scanner-Debounce
        @Volatile private var lastScannerState: String? = null   // "on" | "off"
        @Volatile private var lastScannerEventMs: Long = 0L
        private const val SCANNER_DEBOUNCE_MS = 500L

        // App-Log-Bridge
        var logFlow: MutableSharedFlow<Pair<String, String>>? = null
    }

    // Reconnect-Logik
    private val isReconnecting = AtomicBoolean(false)
    private var reconnectJob: Job? = null
    private val maxReconnectAttempts = 5
    private val reconnectDelayMs = 15000L


    @Volatile private var lastRadio: RadioSnapshot? = null
    private var senderLoopJob: Job? = null

    // ---------------- GPS Handling ----------------
    @Volatile private var lastGpsEventMs: Long = 0L
    @Volatile private var lastGpsLogMs: Long = 0L
    private val GPS_LOG_INTERVAL_MS = 15_000L

    private val STATIC_QTH_WAIT_MS = 2_000L
    @Volatile private var staticQthResolved = false
    private var staticQthProbeJob: Job? = null
    private var gpsWebViewHelper: GpsWebViewHelper? = null

    // JSON
    private val json = Json { ignoreUnknownKeys = true; isLenient = true }

    // ======================= Android Service =======================

    override fun onCreate() {
        super.onCreate()
        createNotificationChannel()
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        // 🧹 Logs immer leeren, wenn ein neuer Connect/Reconnect startet
        LogCenter.clear()
        LogCenter.info("Logs cleared due to new connect/reconnect.")

        // Stop everything before setting new state
        reconnectJob?.cancel()
        isReconnecting.set(false)
        shutdown()

        // Now, set the new state
        hostWithPort = intent?.getStringExtra("HOST_ADDRESS") ?: ""
        currentAdminPassword = intent?.getStringExtra("ADMIN_PASSWORD") ?: ""
        activeServerId = intent?.getIntExtra("ACTIVE_HOST_ID", 0) ?: 0
        activeServerIndex = intent?.getIntExtra("ACTIVE_SERVER_INDEX", 0) ?: 0

        val notification = createNotification("Connecting to $hostWithPort...")
        startForeground(NOTIFICATION_ID, notification)

        // Reset all session-specific states
        isSessionAuthenticated = false
        isAuthFinalized = false
        lastGpsEventMs = 0L
        staticQthResolved = false
        stopStaticQthWebViewIfRunning()

        GpsStore.update(GpsData("", "", "0", "2", 0L))

        if (hostWithPort.isNotBlank()) {
            serviceScope.launch { startNetworkCommunication(hostWithPort, currentAdminPassword) }
        }
        return START_STICKY
    }

    override fun onDestroy() {
        super.onDestroy()
        serviceJob.cancel()
        shutdown()
    }

    override fun onBind(intent: Intent?): IBinder? = null

    private val okHttpClientSingleton: OkHttpClient by lazy {
        OkHttpClient.Builder()
            .retryOnConnectionFailure(true)
            .build()
    }

    fun round6(value: String): String =
        value.toDoubleOrNull()?.let { String.format(Locale.US, "%.6f", it) } ?: value

    fun round1(value: String): String =
        value.toDoubleOrNull()?.let { String.format(Locale.US, "%.1f", it) } ?: value

    // ======================= Core Network =======================

    private fun startNetworkCommunication(hostWithPortRaw: String, adminPassword: String) {
        // Shutdown ist in onStartCommand, hier nur Session-Status resetten
        isSessionAuthenticated = false
        isAuthFinalized = false

        // UDP-Sockets starten
        try {
            udpSenderSocket = DatagramSocket().apply { broadcast = true }
            udpListenerSocket = DatagramSocket(UDP_LISTEN_PORT)
            logToApp("UDP listener started on port $UDP_LISTEN_PORT", "INFO")

            serviceScope.launch {
                listenForUdpCommands(adminPassword)
            }

        } catch (e: Exception) {
            Log.e(TAG, "Error initializing UDP sockets: ${e.message}", e)
            logToApp("UDP socket error: ${e.message}", "ERROR")
            return
        }

        // ===== URL normalisieren (inkl. Pfad + Fragment) =====
        val sanitized = hostWithPortRaw.trim()
        val urlWithScheme = if (
            sanitized.startsWith("http://", true) ||
            sanitized.startsWith("https://", true) ||
            sanitized.startsWith("ws://", true) ||
            sanitized.startsWith("wss://", true)
        ) {
            sanitized
        } else {
            "http://$sanitized"
        }

        val uri = try {
            Uri.parse(urlWithScheme)
        } catch (e: Exception) {
            logToApp("Invalid host URL: $hostWithPortRaw", "ERROR")
            return
        }

        val host = uri.host
        if (host.isNullOrBlank()) {
            logToApp("Cannot parse host from URL: $hostWithPortRaw", "ERROR")
            return
        }

        val portPart = if (uri.port != -1) ":${uri.port}" else ""

        var pathPrefix = uri.path?.trimEnd('/') ?: ""

        // Wenn kein Pfad gesetzt ist, aber ein Fragment (#hensbroek), dann als Pfad benutzen
        if ((pathPrefix.isEmpty() || pathPrefix == "/") && !uri.fragment.isNullOrBlank()) {
            pathPrefix = "/${uri.fragment!!.trim('/')}"
        }

        // WebSocket-Schema bestimmen
        val scheme = if (urlWithScheme.startsWith("https://", true) ||
            urlWithScheme.startsWith("wss://", true)) {
            "wss"
        } else {
            "ws"
        }

        // Basis-WS-URL bauen: ws(s)://host[:port][/prefix]
        val baseWs = buildString {
            append(scheme)
            append("://")
            append(host)
            append(portPart)
            if (pathPrefix.isNotEmpty() && pathPrefix != "/") {
                append(pathPrefix)
            }
        }

        logToApp("Connecting to WebSockets at $baseWs", "INFO")

        val client = okHttpClientSingleton

        val textRequest = Request.Builder()
            .url("$baseWs/text")
            .build()

        val pluginsRequest = Request.Builder()
            .url("$baseWs/data_plugins")
            .build()

        textWebSocket = client.newWebSocket(textRequest, createWebSocketListener("text"))
        pluginsWebSocket = client.newWebSocket(pluginsRequest, createWebSocketListener("data_plugins"))

        // Sender-Loop starten
        startSenderLoop()

        // hostKey inkl. Pfad (ohne Schema) für static QTH, GPS usw.
        val hostKey = buildString {
            append(host)
            append(portPart)
            if (pathPrefix.isNotEmpty() && pathPrefix != "/") {
                append(pathPrefix)
            }
        }
        scheduleStaticQthProbe(hostKey)
    }


    private fun startSenderLoop() {
        senderLoopJob?.cancel()
        senderLoopJob = serviceScope.launch {
            val dateFormat = SimpleDateFormat("dd-MM-yyyy", Locale.GERMANY)
            val timeFormat = SimpleDateFormat("HH:mm:ss", Locale.GERMANY)
            while (isActive) {
                try {
                    lastRadio?.let { r ->
                        val dateStr = dateFormat.format(Date())
                        val timeStr = timeFormat.format(Date())
                        val freqStr = String.format(Locale.US, "%.2f", r.freqKhz)
                        val signalStr = String.format(Locale.US, "%.1f dBµV", r.sigDbuv)
                        val stereoStatus = if (r.st) "1" else "0"
                        val taStatus = if (r.ta == 1) "1" else "0"
                        val tpStatus = if (r.tp == 1) "1" else "0"
                        val ptyStr = if (r.pty == 0) "32" else r.pty.toString()
                        val afStr = r.afList.sorted().joinToString(";") {
                            String.format(Locale.US, "%.1f", it / 1000.0)
                        }

                        val hostKey = normalizeHostKey(hostWithPort)
                        val prefsQ = readQthPrefsForHost(hostKey)
                        val g = GpsStore.latest()

                        val effLat  = if (g.lat.isNotEmpty())  round6(g.lat)  else round6(prefsQ.a)
                        val effLon  = if (g.lon.isNotEmpty())  round6(g.lon)  else round6(prefsQ.b)
                        val effAlt  = if (g.alt.isNotEmpty())  round1(g.alt)  else round1(prefsQ.c)
                        val effMode = if (g.mode.isNotEmpty()) g.mode else prefsQ.d

                        val udpPayloadList = listOf(
                            buildUdpHeader(),
                            scannerStatusFlag, dateStr, timeStr,
                            "1", "",
                            freqStr, r.pi, signalStr,
                            stereoStatus, taStatus, tpStatus, "",
                            ptyStr, r.ecc, "",
                            r.stationName, r.radioText, afStr, "", ""
                        )
                        val baseUdp = udpPayloadList.joinToString(",")

                       // Index-basiert: 0 → "#0", 1 → "MEM #1", 2 → "MEM #2" ...
                        val serverTag = if (activeServerIndex == 0) {
                            "#0"
                        } else {
                            "#$activeServerIndex"
                        }

                        val gpsSuffix = listOf(serverTag, effLat, effLon, effAlt, effMode).joinToString(",")
                        val udpString = "$baseUdp,$gpsSuffix"

                        sendUdpBroadcast(udpString)
                    }
                } catch (e: Exception) {
                    logToApp("senderLoop error: ${e.message}", "WARNING")
                }
                delay(1000)
            }
        }
    }

    private fun processPluginsMessage(text: String) {
        try {
            val root = json.parseToJsonElement(text)
            if (root !is JsonObject) return

            val type = root["type"]?.jsonPrimitive?.content

            if (type != "Scanner" && type != "GPS") {
                return
            }

            val valueObj = root["value"]?.jsonObject ?: return

            when (type) {
                "Scanner" -> {
                    when (val status = valueObj["status"]?.jsonPrimitive?.content) {
                        "auth_success" -> {
                            if (!isAuthFinalized) {
                                isSessionAuthenticated = true
                                isAuthFinalized = true
                                logToApp("Session authenticated successfully. Auth state is now locked.", "SUCCESS")
                            } else {
                                logToApp("Ignoring subsequent auth_success message.", "INFO")
                            }
                        }
                        "auth_failed" -> {
                            if (!isAuthFinalized) {
                                isSessionAuthenticated = false
                                isAuthFinalized = true
                                logToApp("Scanner authentication failed. Auth state is now locked.", "ERROR")
                            } else {
                                logToApp("Ignoring subsequent auth_failed message.", "INFO")
                            }
                        }
                        "response" -> {
                            val scanValueRaw = valueObj["Scan"]?.jsonPrimitive?.content
                            val normalized = when {
                                scanValueRaw?.startsWith("on", ignoreCase = true) == true -> "on"
                                scanValueRaw?.startsWith("off", ignoreCase = true) == true -> "off"
                                else -> null
                            } ?: return

                            val now = System.currentTimeMillis()
                            if (normalized != lastScannerState || now - lastScannerEventMs > SCANNER_DEBOUNCE_MS) {
                                lastScannerState = normalized
                                lastScannerEventMs = now
                                scannerStatusFlag = if (normalized == "on") "1" else "0"
                                logToApp("Scanner ${if (normalized == "on") "activated" else "deactivated"}", "INFO")
                            }
                        }
                    }
                }
                "GPS" -> {
                    if (valueObj["status"]?.jsonPrimitive?.content != "active") return
                    val now = System.currentTimeMillis()
                    val la = valueObj["lat"]?.jsonPrimitive?.content?.trim()?.takeIf { it.isNotEmpty() }?.let { round6(it) } ?: ""
                    val lo = valueObj["lon"]?.jsonPrimitive?.content?.trim()?.takeIf { it.isNotEmpty() }?.let { round6(it) } ?: ""
                    val al = valueObj["alt"]?.jsonPrimitive?.content?.trim()?.takeIf { it.isNotEmpty() }?.let { round1(it) } ?: "0"
                    val md = valueObj["mode"]?.jsonPrimitive?.content?.trim()?.takeIf { it.isNotEmpty() } ?: "2"

                    GpsStore.update(GpsData(la, lo, al, md, now))
                    lastGpsEventMs = now

                    if (now - lastGpsLogMs > GPS_LOG_INTERVAL_MS) {
                        lastGpsLogMs = now
                        logToApp("GPS received → lat=$la lon=$lo alt=$al mode=$md", "INFO")
                    }

                    if (!staticQthResolved) {
                        stopStaticQthWebViewIfRunning()
                    }
                }
            }
        } catch (e: Exception) {
            Log.w(TAG, "Could not process plugin message: $text", e)
            logToApp("Plugin parse error: ${e.message}", "WARNING")
        }
    }


    private fun createWebSocketListener(socketName: String): WebSocketListener {
        return object : WebSocketListener() {
            override fun onOpen(webSocket: WebSocket, response: Response) {
                Log.i(TAG, "WebSocket '$socketName' connected successfully!")
                logToApp("WebSocket '$socketName' connected", "SUCCESS")

                sendStatusUpdate(socketName, true, activeServerId)

                if (isReconnecting.get()) {
                    logToApp("Reconnection successful.", "SUCCESS")
                    reconnectJob?.cancel()
                    isReconnecting.set(false)
                }

                if (socketName == "data_plugins") {
                    val authRequestPayload = ScannerCommand(
                        value = CommandValue(status = "auth_request", password = currentAdminPassword),
                        source = "TEF Logger App"
                    )
                    val jsonPayload = json.encodeToString(authRequestPayload)
                    Log.i(TAG, "[data_plugins] Sending authentication request...")
                    webSocket.send(jsonPayload)

                    // 🔥 Warten auf Antwort vom Scanner-Plugin (auth_success / auth_failed)
                    serviceScope.launch {
                        delay(3000) // z.B. 3 Sekunden warten
                        if (!isAuthFinalized) {
                            // ❌ Keine Antwort vom Scanner-Plugin
                            logToApp("No or wrong Scanner plugin version on the server", "ERROR")
                            // Optional: Auth-Status „einfrieren“, damit später nichts mehr überschreibt
                            isAuthFinalized = true
                            isSessionAuthenticated = false
                        }
                    }
                }
            }

            override fun onMessage(webSocket: WebSocket, text: String) {
                when (socketName) {
                    "text" -> {
                        try {
                            val radioData = json.decodeFromString<RadioData>(text)
                            currentFreqKhz = radioData.freq
                            lastRadio = RadioSnapshot(
                                freqKhz = radioData.freq,
                                pi = (radioData.pi ?: "").replace("?", "").trim(),
                                pty = if (radioData.pty == 0) 32 else radioData.pty,
                                sigDbuv = (radioData.sig ?: 0.0) - 10.875,
                                st = radioData.st,
                                ta = radioData.ta,
                                tp = radioData.tp,
                                ecc = radioData.ecc ?: "",
                                stationName = radioData.txInfo?.tx?.trim() ?: "",
                                radioText = "${radioData.rt0?.trim() ?: ""} ${radioData.rt1?.trim() ?: ""}".trim(),
                                afList = radioData.af
                            )
                        } catch (e: Exception) {
                            Log.e(TAG, "Error parsing radio data: ${e.message}")
                            logToApp("Radio data parse error: ${e.message}", "ERROR")
                        }
                    }
                    "data_plugins" -> {
                        processPluginsMessage(text)
                    }
                }
            }

            override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
                Log.e(TAG, "WebSocket '$socketName' error: ${t.message}", t)
                logToApp("WebSocket '$socketName' error: ${t.message}", "ERROR")
                isSessionAuthenticated = false
                isAuthFinalized = false
                sendStatusUpdate(socketName, false, activeServerId)
                triggerReconnect()
            }

            override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
                Log.w(TAG, "WebSocket '$socketName' closing.")
                logToApp("WebSocket '$socketName' closed", "WARNING")
                isSessionAuthenticated = false
                isAuthFinalized = false
                sendStatusUpdate(socketName, false, activeServerId)
            }
        }
    }

    @Synchronized
    private fun triggerReconnect() {
        if (isReconnecting.getAndSet(true)) {
            return
        }

        logToApp("Connection lost. Starting reconnect procedure...", "WARNING")

        reconnectJob = serviceScope.launch {
            for (attempt in 1..maxReconnectAttempts) {
                logToApp("Reconnecting... (attempt $attempt/$maxReconnectAttempts)", "WARNING")
                // In reconnect scenario, we also need a clean start.
                shutdown()
                startNetworkCommunication(hostWithPort, currentAdminPassword)
                delay(reconnectDelayMs)
                if (!isActive) break
            }

            if (isActive) {
                logToApp("Max reconnection attempts reached. Could not connect to server.", "ERROR")
                isReconnecting.set(false)
            }
        }
    }

    private fun shutdown() {
        // Use cancelAndJoin to wait for the loop to finish. Run in a blocking context
        // as shutdown can be called from different threads.
        runBlocking {
            senderLoopJob?.cancelAndJoin()
        }
        senderLoopJob = null

        // Cancel other jobs
        staticQthProbeJob?.cancel()
        staticQthProbeJob = null

        // Close sockets
        try { textWebSocket?.cancel() } catch (_: Exception) {}
        try { pluginsWebSocket?.cancel() } catch (_: Exception) {}
        textWebSocket = null
        pluginsWebSocket = null

        try { udpSenderSocket?.close() } catch (_: Exception) {}
        try { udpListenerSocket?.close() } catch (_: Exception) {}
        udpSenderSocket = null
        udpListenerSocket = null

        isSessionAuthenticated = false
        isAuthFinalized = false

        sendStatusUpdate("text", false, activeServerId)
        sendStatusUpdate("data_plugins", false, activeServerId)

        Log.i(TAG, "NetworkService: shutdown completed")
    }

    // ======================= UDP =======================

    private fun sendUdpBroadcast(message: String) {
        udpSenderSocket?.let { socket ->
            serviceScope.launch {
                try {
                    val address = InetAddress.getByName("127.0.0.1")
                    val buffer = message.toByteArray(Charsets.UTF_8)
                    val packet = DatagramPacket(buffer, buffer.size, address, UDP_TARGET_PORT)
                    socket.send(packet)
                } catch (e: Exception) {
                    logToApp("UDP send error: ${e.message}", "ERROR")
                }
            }
        }
    }

    private suspend fun listenForUdpCommands(adminPassword: String) {
        while (serviceScope.isActive) {
            try {
                val buffer = ByteArray(1024)
                val packet = DatagramPacket(buffer, buffer.size)
                udpListenerSocket?.receive(packet)

                var receivedString = String(packet.data, 0, packet.length).trim()
                if (receivedString.isBlank()) continue
                Log.i(TAG, "[UDP Listener] Received: '$receivedString'")

                if (receivedString.startsWith('*')) {
                    receivedString = receivedString.substring(1)
                }

                var scanValue = ""
                var searchValue = ""
                when (receivedString) {
                    "S" -> { scanValue = "on"; logToApp("UDP: Scan start command received", "INFO") }
                    "E" -> { scanValue = "off"; logToApp("UDP: Scan stop command received", "INFO") }
                    "<" -> { searchValue = "down"; logToApp("UDP: Search down", "INFO") }
                    ">" -> { searchValue = "up"; logToApp("UDP: Search up", "INFO") }
                }

                val isSearchCommand = searchValue.isNotEmpty()
                val isScanCommand = scanValue.isNotEmpty()

                if (isSearchCommand) {
                    val payload = ScannerCommand(
                        value = CommandValue(Scan = "", Search = searchValue, status = "command"),
                        source = "TEF Logger App"
                    )
                    pluginsWebSocket?.send(json.encodeToString(payload))
                    logToApp("UDP search command '$receivedString' forwarded.", "SUCCESS")
                    continue
                }

                if (isScanCommand) {
                    if (isSessionAuthenticated) {
                        val payload = ScannerCommand(
                            value = CommandValue(Scan = scanValue, Search = "", status = "command"),
                            source = "TEF Logger App"
                        )
                        pluginsWebSocket?.send(json.encodeToString(payload))
                        logToApp("UDP scan command '$receivedString' forwarded.", "SUCCESS")
                    } else {
                        logToApp("Blocked UDP scan command '$receivedString': Session not authenticated.", "ERROR")
                    }
                    continue
                }

                var commandToSend: String? = null
                if (receivedString.matches(Regex("^T\\d+$"))) {
                    var freqValue = receivedString.substring(1).toLongOrNull()
                    if (freqValue != null) {
                        if (freqValue < 87500) freqValue *= 10
                        commandToSend = "T$freqValue"
                    }
                } else if (currentFreqKhz > 0 && (receivedString == "D" || receivedString == "U")) {
                    val newFreqKHz = if (receivedString == "D") round((currentFreqKhz - 0.1) * 1000).toLong() else round((currentFreqKhz + 0.1) * 1000).toLong()
                    commandToSend = "T$newFreqKHz"
                }

                if (commandToSend != null) {
                    textWebSocket?.send(commandToSend)
                    logToApp("UDP frequency command '$commandToSend' forwarded.", "SUCCESS")
                }

            } catch (e: Exception) {
                if (serviceScope.isActive) {
                    Log.e(TAG, "UDP receive error: ${e.message}", e)
                }
            }
        }
    }


    // ======================= GPS Fallback einmalig nach 2s =======================

    private fun scheduleStaticQthProbe(endpoint: String) {
        staticQthProbeJob?.cancel()
        staticQthProbeJob = serviceScope.launch {
            delay(STATIC_QTH_WAIT_MS)
            if (lastGpsEventMs == 0L && !staticQthResolved) {
                logToApp("GPS fallback: using WebView to fetch static QTH…", "WARNING")
                startStaticQthWebViewOnce(endpoint)
            }
        }
    }

    private fun startStaticQthWebViewOnce(endpoint: String) {
        if (staticQthResolved) return
        stopStaticQthWebViewIfRunning()

        gpsWebViewHelper = GpsWebViewHelper(
            context = applicationContext,
            onLog = { msg -> Log.i("GpsWebViewHelper", msg) },
            onGps = { la, lo, al, md ->
                if (lastGpsEventMs == 0L) {
                    val roundLa = round6(la)
                    val roundLo = round6(lo)
                    val roundAl = round1(al)

                    if (!staticQthResolved && (roundLa.isNotEmpty() || roundLo.isNotEmpty())) {
                        staticQthResolved = true
                        GpsStore.update(GpsData(la, lo, al, md, System.currentTimeMillis()))
                        logToApp("NO GPS (fallback) → lat=$roundLa lon=$roundLo alt=$roundAl mode=$md", "SUCCESS")
                        stopStaticQthWebViewIfRunning()
                    }
                } else {
                    stopStaticQthWebViewIfRunning()
                }
            }
        )
        gpsWebViewHelper?.start(endpoint, watchMillis = 6_000L)
    }

    private fun stopStaticQthWebViewIfRunning() {
        try {
            gpsWebViewHelper?.stop()
        } catch (_: Exception) {}
        gpsWebViewHelper = null
    }

    // ======================= Utils & Helpers =======================

    private fun logToApp(message: String, level: String) {
        Log.i(TAG, "[$level] $message")
        serviceScope.launch { logFlow?.emit(Pair(message, level)) }
    }


    private fun sendStatusUpdate(socketName: String, isConnected: Boolean, hostId: Int) {
        val intent = Intent(ACTION_STATUS_UPDATE).apply {
            putExtra("socket_name", socketName)
            putExtra("is_connected", isConnected)
            putExtra("host_id", hostId)
        }
        sendBroadcast(intent)
    }

    private fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val serviceChannel = NotificationChannel(
                NOTIFICATION_CHANNEL_ID,
                "FMDX Connector Service",
                NotificationManager.IMPORTANCE_LOW
            )
            getSystemService(NotificationManager::class.java).createNotificationChannel(serviceChannel)
        }
    }

    private fun updateNotification(contentText: String) {
        val notification = createNotification(contentText)
        getSystemService(NotificationManager::class.java).notify(NOTIFICATION_ID, notification)
    }

    private fun createNotification(contentText: String): Notification {
        return NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
            .setContentTitle("FMDX Connector active")
            .setContentText(contentText)
            .setSmallIcon(R.mipmap.ic_launcher)
            .build()
    }

    private fun normalizeHostKey(raw: String): String {
        val a = raw.trim()
        val base = when {
            a.startsWith("http://", true) || a.startsWith("https://", true) -> a
            a.startsWith("ws://", true)  -> "http://" + a.removePrefix("ws://")
            a.startsWith("wss://", true) -> "https://" + a.removePrefix("wss://")
            else -> "http://$a"
        }
        return try {
            val uri = android.net.Uri.parse(base)
            val host = uri.host ?: a
            val port = if (uri.port != -1) uri.port else if (uri.scheme == "https") 443 else 80
            "$host:$port"
        } catch (_: Exception) { a }
    }

    private fun normalizeEndpoint(raw: String): String {
        val a = raw.trim()
        val base = when {
            a.startsWith("http://", true) ||
                    a.startsWith("https://", true) ||
                    a.startsWith("ws://", true) ||
                    a.startsWith("wss://", true) -> a
            else -> "http://$a"
        }

        return try {
            val uri = android.net.Uri.parse(base)
            val host = uri.host ?: return a
            val portPart = if (uri.port != -1) ":${uri.port}" else ""
            val path = uri.path?.trimEnd('/') ?: ""
            // host[:port][/path]
            host + portPart + path
        } catch (_: Exception) {
            a
        }
    }

    private fun readQthPrefsForHost(hostKey: String): Quad<String, String, String, String> {
        val p = applicationContext.getSharedPreferences("gps_cache", MODE_PRIVATE)
        val la = p.getString("qth_lat_$hostKey", "") ?: ""
        val lo = p.getString("qth_lon_$hostKey", "") ?: ""
        val al = p.getString("qth_alt_$hostKey", "0") ?: "0"
        val md = p.getString("qth_mode_$hostKey", "2") ?: "2"
        return Quad(la, lo, al, md)
    }

    private fun extractServerName(raw: String): String {
        if (raw.isBlank()) return "unknown"
        var s = raw.trim()
        s = s.removePrefix("ws://")
            .removePrefix("wss://")
            .removePrefix("http://")
            .removePrefix("https")
        val slash = s.indexOf('/')
        if (slash >= 0) s = s.substring(0, slash)
        return if (s.isBlank()) "unknown" else s
    }

    private fun buildUdpHeader(): String {
        val server = extractServerName(hostWithPort)
        return "FMDX Connector,$server"
    }
}

// These data classes are now defined here, at the top level, to avoid compiler errors.
data class RadioSnapshot(
    val freqKhz: Double = 0.0,
    val pi: String = "",
    val pty: Int = 32,
    val sigDbuv: Double = 0.0,
    val st: Boolean = false,
    val ta: Int = 0,
    val tp: Int = 0,
    val ecc: String = "",
    val stationName: String = "",
    val radioText: String = "",
    val afList: List<Int> = emptyList()
)

data class Quad<A,B,C,D>(val a:A, val b:B, val c:C, val d:D)