/*
 * Copyright (c) 2025-2026 Meshtastic LLC
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
package com.geeksville.mesh.service

import android.app.Notification
import co.touchlab.kermit.Logger
import com.geeksville.mesh.concurrent.handledLaunch
import com.geeksville.mesh.repository.radio.RadioInterfaceService
import com.meshtastic.core.strings.getString
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.meshtastic.core.analytics.DataPair
import org.meshtastic.core.analytics.platform.PlatformAnalytics
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.data.repository.RadioConfigRepository
import org.meshtastic.core.prefs.ui.UiPrefs
import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.service.MeshServiceNotifications
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.connected_count
import org.meshtastic.core.strings.connecting
import org.meshtastic.core.strings.device_sleeping
import org.meshtastic.core.strings.disconnected
import org.meshtastic.proto.ConfigProtos
import org.meshtastic.proto.MeshProtos.ToRadio
import org.meshtastic.proto.TelemetryProtos
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.time.Duration.Companion.seconds

@Suppress("LongParameterList", "TooManyFunctions")
@Singleton
class MeshConnectionManager
@Inject
constructor(
    private val radioInterfaceService: RadioInterfaceService,
    private val connectionStateHolder: ConnectionStateHandler,
    private val serviceBroadcasts: MeshServiceBroadcasts,
    private val serviceNotifications: MeshServiceNotifications,
    private val uiPrefs: UiPrefs,
    private val packetHandler: PacketHandler,
    private val nodeRepository: NodeRepository,
    private val locationManager: MeshLocationManager,
    private val mqttManager: MeshMqttManager,
    private val historyManager: MeshHistoryManager,
    private val radioConfigRepository: RadioConfigRepository,
    private val commandSender: MeshCommandSender,
    private val nodeManager: MeshNodeManager,
    private val analytics: PlatformAnalytics,
) {
    private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
    private var sleepTimeout: Job? = null
    private var locationRequestsJob: Job? = null
    private var connectTimeMsec = 0L

    fun start(scope: CoroutineScope) {
        this.scope = scope
        radioInterfaceService.connectionState.onEach(::onRadioConnectionState).launchIn(scope)

        nodeRepository.myNodeInfo
            .onEach { myNodeEntity ->
                locationRequestsJob?.cancel()
                if (myNodeEntity != null) {
                    locationRequestsJob =
                        uiPrefs
                            .shouldProvideNodeLocation(myNodeEntity.myNodeNum)
                            .onEach { shouldProvide ->
                                if (shouldProvide) {
                                    locationManager.start(scope) { pos -> commandSender.sendPosition(pos) }
                                } else {
                                    locationManager.stop()
                                }
                            }
                            .launchIn(scope)
                }
            }
            .launchIn(scope)
    }

    private fun onRadioConnectionState(newState: ConnectionState) {
        scope.handledLaunch {
            val localConfig = radioConfigRepository.localConfigFlow.first()
            val isRouter = localConfig.device.role == ConfigProtos.Config.DeviceConfig.Role.ROUTER
            val lsEnabled = localConfig.power.isPowerSaving || isRouter

            val effectiveState =
                when (newState) {
                    is ConnectionState.Connected -> ConnectionState.Connected
                    is ConnectionState.DeviceSleep ->
                        if (lsEnabled) ConnectionState.DeviceSleep else ConnectionState.Disconnected
                    is ConnectionState.Connecting -> ConnectionState.Connecting
                    is ConnectionState.Disconnected -> ConnectionState.Disconnected
                }
            onConnectionChanged(effectiveState)
        }
    }

    private fun onConnectionChanged(c: ConnectionState) {
        if (connectionStateHolder.connectionState.value == c && c !is ConnectionState.Connected) return
        Logger.d { "onConnectionChanged: ${connectionStateHolder.connectionState.value} -> $c" }

        sleepTimeout?.cancel()
        sleepTimeout = null

        when (c) {
            is ConnectionState.Connecting -> connectionStateHolder.setState(ConnectionState.Connecting)
            is ConnectionState.Connected -> handleConnected()
            is ConnectionState.DeviceSleep -> handleDeviceSleep()
            is ConnectionState.Disconnected -> handleDisconnected()
        }
        updateStatusNotification()
    }

    private fun handleConnected() {
        connectionStateHolder.setState(ConnectionState.Connecting)
        serviceBroadcasts.broadcastConnection()
        Logger.d { "Starting connect" }
        connectTimeMsec = System.currentTimeMillis()
        startConfigOnly()
    }

    private fun handleDeviceSleep() {
        connectionStateHolder.setState(ConnectionState.DeviceSleep)
        packetHandler.stopPacketQueue()
        locationManager.stop()
        mqttManager.stop()

        if (connectTimeMsec != 0L) {
            val now = System.currentTimeMillis()
            val duration = now - connectTimeMsec
            connectTimeMsec = 0L
            analytics.track(
                EVENT_CONNECTED_SECONDS,
                DataPair(EVENT_CONNECTED_SECONDS, duration / MILLISECONDS_IN_SECOND),
            )
        }

        sleepTimeout =
            scope.handledLaunch {
                try {
                    val localConfig = radioConfigRepository.localConfigFlow.first()
                    val timeout = (localConfig.power?.lsSecs ?: 0) + DEVICE_SLEEP_TIMEOUT_SECONDS
                    Logger.d { "Waiting for sleeping device, timeout=$timeout secs" }
                    delay(timeout.seconds)
                    Logger.w { "Device timeout out, setting disconnected" }
                    onConnectionChanged(ConnectionState.Disconnected)
                } catch (_: CancellationException) {
                    Logger.d { "device sleep timeout cancelled" }
                }
            }

        serviceBroadcasts.broadcastConnection()
    }

    private fun handleDisconnected() {
        connectionStateHolder.setState(ConnectionState.Disconnected)
        packetHandler.stopPacketQueue()
        locationManager.stop()
        mqttManager.stop()

        analytics.track(
            EVENT_MESH_DISCONNECT,
            DataPair(KEY_NUM_NODES, nodeManager.nodeDBbyNodeNum.size),
            DataPair(KEY_NUM_ONLINE, nodeManager.nodeDBbyNodeNum.values.count { it.isOnline }),
        )
        analytics.track(EVENT_NUM_NODES, DataPair(KEY_NUM_NODES, nodeManager.nodeDBbyNodeNum.size))

        serviceBroadcasts.broadcastConnection()
    }

    fun startConfigOnly() {
        packetHandler.sendToRadio(ToRadio.newBuilder().apply { wantConfigId = CONFIG_ONLY_NONCE })
    }

    fun startNodeInfoOnly() {
        packetHandler.sendToRadio(ToRadio.newBuilder().apply { wantConfigId = NODE_INFO_NONCE })
    }

    fun onHasSettings() {
        commandSender.processQueuedPackets()

        // Start MQTT if enabled
        scope.handledLaunch {
            val moduleConfig = radioConfigRepository.moduleConfigFlow.first()
            mqttManager.start(scope, moduleConfig.mqtt.enabled, moduleConfig.mqtt.proxyToClientEnabled)
        }

        reportConnection()

        val myNodeNum = nodeManager.myNodeNum ?: 0
        // Request history
        scope.handledLaunch {
            val moduleConfig = radioConfigRepository.moduleConfigFlow.first()
            historyManager.requestHistoryReplay("onHasSettings", myNodeNum, moduleConfig.storeForward, "Unknown")
        }

        // Set time
        commandSender.sendAdmin(myNodeNum) {
            setTimeOnly = (System.currentTimeMillis() / MILLISECONDS_IN_SECOND).toInt()
        }
    }

    private fun reportConnection() {
        val myNode = nodeManager.getMyNodeInfo()
        val radioModel = DataPair(KEY_RADIO_MODEL, myNode?.model ?: "unknown")
        analytics.track(
            EVENT_MESH_CONNECT,
            DataPair(KEY_NUM_NODES, nodeManager.nodeDBbyNodeNum.size),
            DataPair(KEY_NUM_ONLINE, nodeManager.nodeDBbyNodeNum.values.count { it.isOnline }),
            radioModel,
        )
    }

    fun updateTelemetry(telemetry: TelemetryProtos.Telemetry) {
        updateStatusNotification(telemetry)
    }

    fun updateStatusNotification(telemetry: TelemetryProtos.Telemetry? = null): Notification {
        val summary =
            when (connectionStateHolder.connectionState.value) {
                is ConnectionState.Connected ->
                    getString(Res.string.connected_count)
                        .format(nodeManager.nodeDBbyNodeNum.values.count { it.isOnline })
                is ConnectionState.Disconnected -> getString(Res.string.disconnected)
                is ConnectionState.DeviceSleep -> getString(Res.string.device_sleeping)
                is ConnectionState.Connecting -> getString(Res.string.connecting)
            }
        return serviceNotifications.updateServiceStateNotification(summary, telemetry = telemetry)
    }

    companion object {
        private const val CONFIG_ONLY_NONCE = 69420
        private const val NODE_INFO_NONCE = 69421
        private const val MILLISECONDS_IN_SECOND = 1000.0
        private const val DEVICE_SLEEP_TIMEOUT_SECONDS = 30

        private const val EVENT_CONNECTED_SECONDS = "connected_seconds"
        private const val EVENT_MESH_DISCONNECT = "mesh_disconnect"
        private const val EVENT_NUM_NODES = "num_nodes"
        private const val EVENT_MESH_CONNECT = "mesh_connect"

        private const val KEY_NUM_NODES = "num_nodes"
        private const val KEY_NUM_ONLINE = "num_online"
        private const val KEY_RADIO_MODEL = "radio_model"
    }
}
