package club.clubk.ktag.apps.sharedservices

import android.content.Context
import android.net.nsd.NsdManager
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import android.net.nsd.NsdServiceInfo
import android.os.Build
import android.util.Log
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.eclipse.paho.client.mqttv3.IMqttActionListener
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken
import org.eclipse.paho.client.mqttv3.IMqttToken
import org.eclipse.paho.client.mqttv3.MqttCallbackExtended
import org.eclipse.paho.client.mqttv3.MqttClient
import org.eclipse.paho.client.mqttv3.MqttConnectOptions
import org.eclipse.paho.client.mqttv3.MqttMessage
import info.mqtt.android.service.Ack
import info.mqtt.android.service.MqttAndroidClient
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit

sealed class MqttClientStatus {
    data object Disconnected : MqttClientStatus()
    data object Connecting : MqttClientStatus()
    data object Connected : MqttClientStatus()
    data class Error(val message: String) : MqttClientStatus()
}

interface MqttMessageListener {
    fun onMessageReceived(topic: String, payload: ByteArray)
}

object SharedMqttClient {

    private const val TAG = "SharedMqttClient"
    private const val DISCOVERY_TIMEOUT_MS = 5000L
    private const val DISCOVERY_RETRY_DELAY_MS = 5000L

    @Volatile
    private var autodiscoveryActive = false

    private val _status = MutableStateFlow<MqttClientStatus>(MqttClientStatus.Disconnected)
    val status: StateFlow<MqttClientStatus> = _status.asStateFlow()

    private var client: MqttAndroidClient? = null
    private var currentConfig: MqttConnectionConfig? = null

    // topic -> set of listeners
    private val subscriptions = ConcurrentHashMap<String, MutableSet<MqttMessageListener>>()

    val battlefield: String
        get() = currentConfig?.battlefield ?: MqttPreferenceKeys.DEFAULT_BATTLEFIELD

    val currentDeviceId: String
        get() = currentConfig?.deviceId ?: android.os.Build.MODEL

    fun connect(context: Context) {
        val config = MqttConnectionConfig.fromPreferences(context)

        if (config.autodiscovery) {
            connectWithAutodiscovery(context, config)
        } else {
            connectWithConfig(context, config)
        }
    }

    private fun connectWithConfig(context: Context, config: MqttConnectionConfig) {
        // If already connected with same config, no-op
        if (client?.isConnected == true && config == currentConfig) {
            Log.d(TAG, "Already connected with same config, skipping")
            return
        }

        // Tear down existing connection
        disconnectInternal()

        currentConfig = config
        _status.value = MqttClientStatus.Connecting

        connectToServer(context, config.serverUri, config.username, config.password)
    }

    private fun connectWithAutodiscovery(context: Context, config: MqttConnectionConfig) {
        // For autodiscovery, the effective credentials are KTag / battlefield
        val effectiveUsername = "KTag"
        val effectivePassword = config.battlefield

        // If already connected via autodiscovery with same battlefield, no-op
        if (client?.isConnected == true && currentConfig?.autodiscovery == true
            && currentConfig?.battlefield == config.battlefield
            && currentConfig?.deviceId == config.deviceId) {
            Log.d(TAG, "Already connected via autodiscovery with same battlefield, skipping")
            return
        }

        disconnectInternal()
        currentConfig = config
        _status.value = MqttClientStatus.Connecting
        autodiscoveryActive = true

        // Run mDNS discovery on a background thread, retrying until found
        Thread {
            while (autodiscoveryActive) {
                val discoveredUri = discoverBroker(context)
                if (discoveredUri != null) {
                    Log.i(TAG, "Autodiscovery found broker at $discoveredUri")
                    if (autodiscoveryActive) {
                        connectToServer(context, discoveredUri, effectiveUsername, effectivePassword)
                    }
                    return@Thread
                }

                Log.w(TAG, "Autodiscovery found no brokers on LAN, retrying in ${DISCOVERY_RETRY_DELAY_MS}ms")
                _status.value = MqttClientStatus.Error("No broker found on LAN, retrying\u2026")

                try {
                    Thread.sleep(DISCOVERY_RETRY_DELAY_MS)
                } catch (_: InterruptedException) {
                    return@Thread
                }
            }
        }.start()
    }

    private fun discoverBroker(context: Context): String? {
        val nsdManager = context.getSystemService(Context.NSD_SERVICE) as? NsdManager
        if (nsdManager == null) {
            Log.e(TAG, "NSD service not available")
            return null
        }

        val latch = CountDownLatch(1)
        var resultUri: String? = null

        val discoveryListener = object : NsdManager.DiscoveryListener {
            override fun onDiscoveryStarted(serviceType: String) {
                Log.d(TAG, "mDNS discovery started")
            }

            override fun onServiceFound(serviceInfo: NsdServiceInfo) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
                    val callback = object : NsdManager.ServiceInfoCallback {
                        override fun onServiceInfoCallbackRegistrationFailed(errorCode: Int) {
                            Log.w(TAG, "ServiceInfoCallback registration failed: $errorCode")
                        }

                        override fun onServiceUpdated(si: NsdServiceInfo) {
                            nsdManager.unregisterServiceInfoCallback(this)
                            handleResolvedService(si, resultUri) { uri ->
                                resultUri = uri
                                latch.countDown()
                            }
                        }

                        override fun onServiceLost() {
                            nsdManager.unregisterServiceInfoCallback(this)
                        }

                        override fun onServiceInfoCallbackUnregistered() {}
                    }
                    nsdManager.registerServiceInfoCallback(serviceInfo, { it.run() }, callback)
                } else {
                    @Suppress("DEPRECATION")
                    nsdManager.resolveService(serviceInfo, object : NsdManager.ResolveListener {
                        override fun onResolveFailed(si: NsdServiceInfo, errorCode: Int) {
                            Log.w(TAG, "Failed to resolve service: $errorCode")
                        }

                        override fun onServiceResolved(si: NsdServiceInfo) {
                            handleResolvedService(si, resultUri) { uri ->
                                resultUri = uri
                                latch.countDown()
                            }
                        }
                    })
                }
            }

            override fun onServiceLost(serviceInfo: NsdServiceInfo) {}
            override fun onDiscoveryStopped(serviceType: String) {}
            override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
                Log.e(TAG, "mDNS discovery start failed: $errorCode")
                latch.countDown()
            }
            override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {}
        }

        nsdManager.discoverServices("_mqtt._tcp.", NsdManager.PROTOCOL_DNS_SD, discoveryListener)

        // Wait for first result or timeout
        latch.await(DISCOVERY_TIMEOUT_MS, TimeUnit.MILLISECONDS)

        try {
            nsdManager.stopServiceDiscovery(discoveryListener)
        } catch (_: Exception) {}

        return resultUri
    }

    private fun handleResolvedService(
        si: NsdServiceInfo,
        currentResult: String?,
        onFound: (String) -> Unit
    ) {
        val purpose = si.attributes["purpose"]?.let { String(it) }
        if (purpose != "KTag MQTT Broker") {
            Log.d(TAG, "Skipping non-KTag broker: ${si.serviceName} (purpose=$purpose)")
            return
        }
        val host = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
            si.hostAddresses.firstOrNull()?.hostAddress
        } else {
            @Suppress("DEPRECATION")
            si.host?.hostAddress
        } ?: return
        val port = si.port
        Log.i(TAG, "Resolved KTag MQTT broker: $host:$port")
        if (currentResult == null) {
            onFound("tcp://$host:$port")
        }
    }

    private fun connectToServer(context: Context, serverUri: String, username: String, password: String) {
        try {
            val clientId = currentConfig?.deviceId ?: android.os.Build.MODEL
            val newClient = MqttAndroidClient(
                context.applicationContext,
                serverUri,
                clientId,
                Ack.AUTO_ACK
            )

            newClient.setCallback(object : MqttCallbackExtended {
                override fun connectComplete(reconnect: Boolean, serverURI: String) {
                    Log.i(TAG, "Connected to $serverURI (reconnect=$reconnect)")
                    _status.value = MqttClientStatus.Connected
                    resubscribeAll()
                }

                override fun connectionLost(cause: Throwable?) {
                    Log.w(TAG, "Connection lost: ${cause?.message}")
                    _status.value = MqttClientStatus.Disconnected
                }

                override fun messageArrived(topic: String, message: MqttMessage) {
                    val listeners = subscriptions[topic]
                    if (listeners != null) {
                        synchronized(listeners) {
                            for (listener in listeners) {
                                try {
                                    listener.onMessageReceived(topic, message.payload)
                                } catch (e: Exception) {
                                    Log.e(TAG, "Error in message listener: ${e.message}")
                                }
                            }
                        }
                    }
                }

                override fun deliveryComplete(token: IMqttDeliveryToken?) {}
            })

            val options = MqttConnectOptions().apply {
                isCleanSession = true
                isAutomaticReconnect = true
                keepAliveInterval = 15
                userName = username
                this.password = password.toCharArray()
            }

            newClient.connect(options)?.setActionCallback(object : IMqttActionListener {
                override fun onSuccess(asyncActionToken: IMqttToken?) {
                    Log.i(TAG, "MQTT connection successful")
                }

                override fun onFailure(asyncActionToken: IMqttToken?, exception: Throwable?) {
                    Log.e(TAG, "MQTT connection failed: ${exception?.message}")
                    _status.value = MqttClientStatus.Error(
                        exception?.message ?: "Connection failed"
                    )
                }
            })

            client = newClient
        } catch (e: Exception) {
            Log.e(TAG, "Failed to initialize MQTT: ${e.message}")
            _status.value = MqttClientStatus.Error(e.message ?: "Initialization failed")
        }
    }

    fun disconnect() {
        disconnectInternal()
        _status.value = MqttClientStatus.Disconnected
    }

    private fun disconnectInternal() {
        autodiscoveryActive = false
        try {
            client?.unregisterResources()
            client?.disconnect()
        } catch (e: Exception) {
            Log.e(TAG, "Error during disconnect: ${e.message}")
        }
        client = null
    }

    fun subscribe(topic: String, listener: MqttMessageListener) {
        val listeners = subscriptions.getOrPut(topic) { mutableSetOf() }
        val needsBrokerSubscribe: Boolean
        synchronized(listeners) {
            needsBrokerSubscribe = listeners.isEmpty()
            listeners.add(listener)
        }

        if (needsBrokerSubscribe) {
            subscribeToBroker(topic)
        }
    }

    fun unsubscribe(topic: String, listener: MqttMessageListener) {
        val listeners = subscriptions[topic] ?: return
        val needsBrokerUnsubscribe: Boolean
        synchronized(listeners) {
            listeners.remove(listener)
            needsBrokerUnsubscribe = listeners.isEmpty()
        }

        if (needsBrokerUnsubscribe) {
            subscriptions.remove(topic)
            unsubscribeFromBroker(topic)
        }
    }

    /**
     * Publishes a timestamped hello message to "KTag/<battlefield>/<topicPath>/Hello".
     * [appName] is used as the subject of the message, e.g. "KTag Medic App".
     */
    fun publishHello(appName: String, topicPath: String) {
        try {
            val bf = battlefield
            val sdf = SimpleDateFormat("EEE, MMM d, yyyy HH:mm", Locale.getDefault())
            val helloString = "$appName started on ${sdf.format(Date())}."
            publish("KTag/$bf/$topicPath/Hello", helloString.toByteArray(), qos = 2, retained = false)
        } catch (e: Exception) {
            Log.e(TAG, "Failed to publish hello: ${e.message}")
        }
    }

    fun publish(topic: String, payload: ByteArray, qos: Int = 1, retained: Boolean = false) {
        val c = client
        if (c == null || !c.isConnected) {
            Log.w(TAG, "Not connected, skipping publish to $topic")
            return
        }

        try {
            val message = MqttMessage(payload).apply {
                this.qos = qos
                isRetained = retained
            }
            c.publish(topic, message)
            Log.d(TAG, "Published to $topic")
        } catch (e: Exception) {
            Log.e(TAG, "Failed to publish to $topic: ${e.message}")
        }
    }

    private fun subscribeToBroker(topic: String) {
        val c = client
        if (c == null || !c.isConnected) {
            Log.d(TAG, "Not connected, will subscribe to $topic on reconnect")
            return
        }

        try {
            c.subscribe(topic, 1)?.setActionCallback(object : IMqttActionListener {
                override fun onSuccess(asyncActionToken: IMqttToken?) {
                    Log.i(TAG, "Subscribed to $topic")
                }

                override fun onFailure(asyncActionToken: IMqttToken?, exception: Throwable?) {
                    Log.e(TAG, "Failed to subscribe to $topic: ${exception?.message}")
                }
            })
        } catch (e: Exception) {
            Log.e(TAG, "Error subscribing to $topic: ${e.message}")
        }
    }

    private fun unsubscribeFromBroker(topic: String) {
        val c = client
        if (c == null || !c.isConnected) return

        try {
            c.unsubscribe(topic)
        } catch (e: Exception) {
            Log.e(TAG, "Error unsubscribing from $topic: ${e.message}")
        }
    }

    private fun resubscribeAll() {
        for (topic in subscriptions.keys) {
            subscribeToBroker(topic)
        }
    }
}
