package club.clubk.ktag.apps.konfigurator

import android.annotation.SuppressLint
import android.bluetooth.le.AdvertiseCallback
import android.bluetooth.le.AdvertiseSettings
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanResult
import android.content.Context
import android.util.Log
import androidx.preference.PreferenceManager
import club.clubk.ktag.apps.sharedservices.DeviceInfo
import club.clubk.ktag.apps.sharedservices.DeviceInfoMqttSync
import club.clubk.ktag.apps.sharedservices.DeviceInfoRepository
import club.clubk.ktag.apps.sharedservices.GamePreferenceKeys
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import club.clubk.ktag.apps.core.HexUtils
import club.clubk.ktag.apps.core.ble.Packet
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update

private const val TAG_BLE_SCAN = "BLE Scanner"
private const val TAG_BLE_AD = "BLE Advertiser"
private const val TAG_STATE_MACHINE = "StateMachineVM"

class StateMachineViewModel(context: Context) : ViewModel() {

    private val _appState = MutableStateFlow<AppState>(AppState.GameSettings)
    val appState: StateFlow<AppState> = _appState.asStateFlow()

    private val _devices = MutableStateFlow<List<Device>>(emptyList())
    val devices: StateFlow<List<Device>> = _devices.asStateFlow()

    private val _allDevicesReady = MutableStateFlow<Boolean>(false)
    val allDevicesReady = _allDevicesReady.asStateFlow()

    private val _pendingApply = MutableStateFlow(true)
    val pendingApply = _pendingApply.asStateFlow()

    private val _isConfiguring = MutableStateFlow(false)
    val isConfiguring = _isConfiguring.asStateFlow()

    private val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context)
    private val _currentGameConfig = MutableStateFlow(
        GameConfig(
            gameDurationMin = sharedPrefs.getString(GamePreferenceKeys.GAME_DURATION, GamePreferenceKeys.DEFAULT_GAME_DURATION)?.toIntOrNull()
                ?: GamePreferenceKeys.DEFAULT_GAME_DURATION.toInt(),
            timeUntilCountdownS = sharedPrefs.getString(GamePreferenceKeys.TIME_UNTIL_COUNTDOWN, GamePreferenceKeys.DEFAULT_TIME_UNTIL_COUNTDOWN)?.toIntOrNull()
                ?: GamePreferenceKeys.DEFAULT_TIME_UNTIL_COUNTDOWN.toInt()
        )
    )
    val currentGameConfig = _currentGameConfig.asStateFlow()
    val gameLengthMs: Long get() = _currentGameConfig.value.gameDurationMin.toLong() * 60 * 1000
    val pregameLengthMs: Long get() = _currentGameConfig.value.timeUntilCountdownS.toLong() * 1000

    // State for configuration progress
    private val _configurationProgress = MutableStateFlow<ConfigurationProgress?>(null)
    val configurationProgress: StateFlow<ConfigurationProgress?> = _configurationProgress.asStateFlow()

    // State for individual device configuration status
    private val _deviceConfigStatus = MutableStateFlow<Map<String, Boolean>>(emptyMap())
    val deviceConfigStatus: StateFlow<Map<String, Boolean>> = _deviceConfigStatus.asStateFlow()

    private val bleManager = BleManager.getInstance(context)
    private val multiDeviceConfigurator = MultiDeviceConfigurator(bleManager)

    private val soundManager = SoundManager(context)

    private val deviceInfoRepository = DeviceInfoRepository.getInstance(context)
    private val mqttSync = DeviceInfoMqttSync(deviceInfoRepository)

    init {
        mqttSync.connect()
    }

    override fun onCleared() {
        mqttSync.cleanup()
        soundManager.release()
        super.onCleared()
    }

    @SuppressLint("MissingPermission")
    private val scanCallbackDefault = object : ScanCallback() {
        override fun onScanResult(callbackType: Int, result: ScanResult) {
            if (Packet.IsKTagPacket(result)) {
                val scannedDevice = Device(address = result.device.address)
                when {
                    Packet.IsKTagHelloPacket(result) -> {
                        val hello = Packet.Hello(result)
                        scannedDevice.name = hello.device_name
                        scannedDevice.deviceType = hello.device_type
                        scannedDevice.team = hello.team_ID.toInt()
                        scannedDevice.broadcastedTeam = hello.team_ID.toInt()
                        scannedDevice.deviceState = DeviceState.Configurable
                        scannedDevice.systemkMajorVersion = hello.systemK_major_version
                        scannedDevice.systemkMinorVersion = hello.systemK_minor_version
                        scannedDevice.maxHealth = _currentGameConfig.value.maxHealth
                        scannedDevice.specialWeaponsOnReentry = _currentGameConfig.value.specialWeaponsOnReentry
                        addOrRefreshDevice(scannedDevice)
                        val info = DeviceInfo(
                            name = hello.device_name,
                            deviceType = hello.device_type,
                            deviceTypeName = Packet.getDeviceTypeName(hello.device_type),
                            majorVersion = hello.systemK_major_version,
                            minorVersion = hello.systemK_minor_version
                        )
                        if (deviceInfoRepository.setInfo(scannedDevice.address, info)) {
                            mqttSync.publishCurrentInfo()
                        }
                    }
                    Packet.IsKTagStatusPacket(result) -> {
                        val status = Packet.Status(result)
                        scannedDevice.broadcastedTeam = status.team_ID.toInt()
                        when (status.getSystemK_top_level_state()) {
                            3 -> scannedDevice.deviceState = DeviceState.Ready
                            7 -> scannedDevice.deviceState = DeviceState.Playing
                            9 -> scannedDevice.deviceState = DeviceState.WrapUp
                        }
                        addOrRefreshDevice(scannedDevice)
                    }
                }
            }
        }

        override fun onScanFailed(errorCode: Int) {
            val errorMessage = when (errorCode) {
                SCAN_FAILED_ALREADY_STARTED -> "Already started"
                SCAN_FAILED_APPLICATION_REGISTRATION_FAILED -> "App registration failed"
                SCAN_FAILED_FEATURE_UNSUPPORTED -> "Feature unsupported"
                SCAN_FAILED_INTERNAL_ERROR -> "Internal error"
                else -> "Unknown error $errorCode"
            }
            Log.e(TAG_BLE_SCAN, "BLE Scan failed: $errorMessage")
        }
    }

    private val advertisingCallbackDefault = object : AdvertiseCallback() {
        override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {
            super.onStartSuccess(settingsInEffect)
            Log.i(
                TAG_BLE_AD, "Advertisement started successfully with settings: " +
                        "Mode=${settingsInEffect.mode}, TxPower=${settingsInEffect.txPowerLevel}"
            )
        }

        override fun onStartFailure(errorCode: Int) {
            super.onStartFailure(errorCode)
            val errorMessage = when (errorCode) {
                ADVERTISE_FAILED_ALREADY_STARTED -> "Already started"
                ADVERTISE_FAILED_DATA_TOO_LARGE -> "Data too large"
                ADVERTISE_FAILED_TOO_MANY_ADVERTISERS -> "Too many advertisers"
                ADVERTISE_FAILED_INTERNAL_ERROR -> "Internal error"
                ADVERTISE_FAILED_FEATURE_UNSUPPORTED -> "Feature unsupported"
                else -> "Unknown error $errorCode"
            }
            Log.e(TAG_BLE_AD, "Failed to start advertising: $errorMessage")
        }
    }

    private fun addOrRefreshDevice(newDevice: Device) {
        val currentDevices = _devices.value.toMutableList()
        val index = currentDevices.indexOfFirst { it.address == newDevice.address }
        if (index == -1) {
            currentDevices += newDevice
        }
        else {
            val oldDevice = currentDevices[index]
            newDevice.name = oldDevice.name
            newDevice.deviceType = oldDevice.deviceType ?: newDevice.deviceType
            newDevice.team = oldDevice.team ?: newDevice.team
            newDevice.broadcastedTeam = newDevice.broadcastedTeam ?: oldDevice.broadcastedTeam
            newDevice.playerID = oldDevice.playerID ?: newDevice.playerID
            newDevice.deviceState = newDevice.deviceState ?: oldDevice.deviceState
            newDevice.systemkMajorVersion = newDevice.systemkMajorVersion ?: oldDevice.systemkMajorVersion
            newDevice.systemkMinorVersion = newDevice.systemkMinorVersion ?: oldDevice.systemkMinorVersion
            newDevice.maxHealth = oldDevice.maxHealth ?: newDevice.maxHealth
            newDevice.broadcastedMaxHealth = newDevice.broadcastedMaxHealth ?: oldDevice.broadcastedMaxHealth
            newDevice.specialWeaponsOnReentry = oldDevice.specialWeaponsOnReentry ?: newDevice.specialWeaponsOnReentry
            newDevice.broadcastedSpecialWeaponsOnReentry = newDevice.broadcastedSpecialWeaponsOnReentry ?: oldDevice.broadcastedSpecialWeaponsOnReentry
            currentDevices[index] = newDevice
        }
        _devices.value = currentDevices
        Log.d("AddRefresh", _devices.value.toString())
        _allDevicesReady.value = allDevicesReady()
        Log.d("AddRefresh", _allDevicesReady.value.toString())
    }

    /** Applies [update] to the device with the given [address] and marks settings as pending. */
    private fun updateDeviceField(address: String, update: (Device) -> Device) {
        _devices.update { list -> list.map { if (it.address == address) update(it) else it } }
        _pendingApply.value = true
    }

    fun updateDeviceTeam(deviceAddress: String, newTeam: Int) {
        updateDeviceField(deviceAddress) { it.copy(team = newTeam) }
        _allDevicesReady.value = allDevicesReady()
    }

    fun renameDevice(address: String, name: String) {
        deviceInfoRepository.setName(address, name)
        mqttSync.publishCurrentInfo()
    }

    fun getDeviceUserDefinedName(address: String): String {
        return deviceInfoRepository.getInfo(address)?.userDefinedName ?: ""
    }

    fun updateDeviceMaxHealth(deviceAddress: String, value: Int) =
        updateDeviceField(deviceAddress) { it.copy(maxHealth = value) }

    fun updateDeviceSpecialWeapons(deviceAddress: String, value: Int) =
        updateDeviceField(deviceAddress) { it.copy(specialWeaponsOnReentry = value) }

    fun cycleDeviceTeam(device: Device) {
        Log.d("STATEMACHINE", "cycling device team")
        var newTeam = device.team ?: -1
        newTeam++
        if (newTeam > 2) {
            newTeam = 0
        }
        updateDeviceTeam(device.address, newTeam)
    }

    private fun allDevicesReady(): Boolean {
        if (_devices.value.isEmpty()) { return false }
        for (device in _devices.value) {
            if (device.deviceState != DeviceState.Ready) {
                return false
            }
        }
        return true
    }

    private var readyRetryJob: Job? = null

    fun readyAllDevices() {
        readyRetryJob?.cancel()
        readyRetryJob = viewModelScope.launch {
            while (_appState.value is AppState.TeamSettings && !_allDevicesReady.value) {
                val broadcastAddr = HexUtils.hexStringToByteArray("FF:FF:FF:FF:FF:FF", ':')
                val eventPacket = Packet.Event(broadcastAddr, Packet.EVENT_CONFIGURED, 0)
                bleManager.startAdvertising(eventPacket.GetBytes(), advertisingCallbackDefault)
                delay(2000)
            }
        }
    }

    fun updateGameConfig(newGameConfig: GameConfig) {
        _currentGameConfig.value = newGameConfig
        sharedPrefs.edit()
            .putString(GamePreferenceKeys.GAME_DURATION, newGameConfig.gameDurationMin.toString())
            .putString(GamePreferenceKeys.TIME_UNTIL_COUNTDOWN, newGameConfig.timeUntilCountdownS.toString())
            .apply()
    }

    /**
     * Initiates the configuration of all devices using the MultiDeviceConfigurator.
     */
    fun configureDevices() {
        val currentDevicesToConfigure = _devices.value
        val gameCfg = _currentGameConfig.value

        if (currentDevicesToConfigure.isEmpty()) {
            Log.i(TAG_STATE_MACHINE, "No devices eligible for configuration.")
            _configurationProgress.value = ConfigurationProgress(0, 0, true)
            return
        }

        // Build the packet list and a parallel list of confirmation lambdas.
        // Each lambda is applied to the device when its specific packet is ACK'd,
        // so a failed packet never blocks confirmation of packets that did succeed.
        val confirmOnSuccess = mutableListOf<(Device) -> Device>()
        val deviceConfigPackets = currentDevicesToConfigure.flatMap { device ->
            val targetAddr = HexUtils.hexStringToByteArray(device.address, ':')
            val teamId = device.team ?: 0
            val health = device.maxHealth ?: gameCfg.maxHealth
            val specWeapons = device.specialWeaponsOnReentry ?: gameCfg.specialWeaponsOnReentry
            confirmOnSuccess += { d -> d.copy(
                broadcastedTeam      = d.team      ?: d.broadcastedTeam,
                broadcastedMaxHealth = d.maxHealth ?: d.broadcastedMaxHealth
            )}
            confirmOnSuccess += { d -> d.copy(
                broadcastedSpecialWeaponsOnReentry = d.specialWeaponsOnReentry ?: d.broadcastedSpecialWeaponsOnReentry
            )}
            listOf(
                Pair(device.address, Packet.Parameters(targetAddr, Packet.PARAMETER_SUBTYPE_REQUEST_CHANGE,
                    Packet.PARAMETER_KEY_TEAM_ID, teamId,
                    Packet.PARAMETER_KEY_MAX_HEALTH, health)),
                Pair(device.address, Packet.Parameters(targetAddr, Packet.PARAMETER_SUBTYPE_REQUEST_CHANGE,
                    Packet.PARAMETER_KEY_SPECIAL_WEAPONS_ON_REENTRY, specWeapons,
                    Packet.PARAMETER_KEY_NONE, 0))
            )
        }

        if (deviceConfigPackets.isEmpty() && currentDevicesToConfigure.isNotEmpty()) {
            Log.w(TAG_STATE_MACHINE, "Could not generate config packets for any selected device.")
            _configurationProgress.value = ConfigurationProgress(0, currentDevicesToConfigure.size, true)
            return
        }

        Log.i(TAG_STATE_MACHINE, "Starting configuration: ${deviceConfigPackets.size} packets for ${currentDevicesToConfigure.size} devices.")
        readyRetryJob?.cancel()
        _isConfiguring.value = true
        _pendingApply.value = false
        _deviceConfigStatus.value = emptyMap() // Reset status for the new session
        _configurationProgress.value = ConfigurationProgress(0, deviceConfigPackets.size, false)

        viewModelScope.launch {
            var packetIndex = 0
            val results = multiDeviceConfigurator.configureMultipleDevices(
                deviceConfigs = deviceConfigPackets,
                onDeviceConfigured = { deviceAddress, success ->
                    val confirm = confirmOnSuccess.getOrNull(packetIndex++)
                    Log.i(TAG_STATE_MACHINE, "Device $deviceAddress packet ${packetIndex - 1} configured: $success")
                    _deviceConfigStatus.update { it + (deviceAddress to success) }
                    if (success && confirm != null) {
                        _devices.update { list ->
                            list.map { device ->
                                if (device.address == deviceAddress) confirm(device) else device
                            }
                        }
                    }
                },
                onProgressUpdate = { configuredCount, totalCount ->
                    Log.i(TAG_STATE_MACHINE, "Configuration progress: $configuredCount/$totalCount")
                    _configurationProgress.value = ConfigurationProgress(configuredCount, totalCount, configuredCount == totalCount)
                }
            )

            // 3. Process overall results
            Log.i(TAG_STATE_MACHINE, "All device configurations attempted by MultiDeviceConfigurator. Final Results map: $results")
            val allSucceeded = results.values.all { it }
            if (allSucceeded) {
                Log.i(TAG_STATE_MACHINE, "All devices configured successfully (overall check).")
            } else {
                Log.w(TAG_STATE_MACHINE, "One or more devices failed to configure (overall check).")
            }

            // Resume device discovery. configureDevice() stops scanning in its finally block.
            bleManager.startScanning(scanCallbackDefault)
            _isConfiguring.value = false
        }
    }

    fun nextState() {
        soundManager.playDoubleChirp()
        when (_appState.value) {
            is AppState.GameSettings -> {
                _appState.value = AppState.TeamSettings
                _pendingApply.value = true
                bleManager.startScanning(scanCallbackDefault)
            }
            is AppState.TeamSettings -> {
                readyRetryJob?.cancel()
                _appState.value = AppState.PregameTimer
                val instigatePacket = Packet.InstigatingGame(
                    gameLengthMs.toInt(),
                    pregameLengthMs.toInt()
                )
                bleManager.stopScanning()
                bleManager.startAdvertising(instigatePacket.GetBytes(), advertisingCallbackDefault)
            }
            is AppState.PregameTimer -> {
                _appState.value = AppState.Countdown
                bleManager.stopAdvertising()
            }
            is AppState.Countdown -> {
                _appState.value = AppState.GameTimer
            }
            is AppState.GameTimer -> {
                _appState.value = AppState.WrapUp
                val broadcastAddr = HexUtils.hexStringToByteArray("FF:FF:FF:FF:FF:FF", ':')
                val gameOverPacket = Packet.Event(broadcastAddr, Packet.EVENT_GAME_OVER, 0)
                bleManager.startAdvertising(gameOverPacket.GetBytes(), advertisingCallbackDefault)
            }
            AppState.WrapUp -> {
                _appState.value = AppState.GameSettings
                val broadcastAddr = HexUtils.hexStringToByteArray("FF:FF:FF:FF:FF:FF", ':')
                val wrapupCompletePacket = Packet.Event(broadcastAddr, Packet.EVENT_WRAPUP_COMPLETE, 2)
                bleManager.startAdvertising(wrapupCompletePacket.GetBytes(), advertisingCallbackDefault)
            }
        }
    }
}
