package cloud.molberg.picar

import android.annotation.SuppressLint
import android.content.Context
import android.location.Location
import android.os.BatteryManager
import android.provider.Settings
import com.google.android.gms.location.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.json.JSONObject
import java.nio.charset.StandardCharsets
import java.util.UUID

class CompanionRepository(private val context: Context) {
    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
    private val tokenStore = TokenStore(context)
    private val ble = BleClient(context)
    private val locationClient = LocationServices.getFusedLocationProviderClient(context)
    private val mutableState = MutableStateFlow(CompanionUiState(paired = tokenStore.load() != null))
    val state = mutableState.asStateFlow()
    private var polling: Job? = null

    init {
        scope.launch { ble.state.collect { connection ->
            mutableState.value = mutableState.value.copy(connection = connection)
            if (connection == ConnectionState.CONNECTED) startPolling()
        } }
    }

    fun connect() = ble.connect()
    fun stop() { polling?.cancel(); ble.disconnect() }

    fun pair(code: String, onComplete: (Boolean) -> Unit) {
        scope.launch {
            runCatching {
                val deviceId = stableDeviceId()
                val payload = JSONObject().put("code", code).put("deviceId", deviceId).put("deviceName", android.os.Build.MODEL.take(80))
                val response = ble.request("pair", null, payload)
                tokenStore.save(response.getString("token"))
                mutableState.value = mutableState.value.copy(paired = true, error = null)
            }.onFailure { mutableState.value = mutableState.value.copy(error = it.message) }
            withContext(Dispatchers.Main) { onComplete(tokenStore.load() != null) }
        }
    }

    fun forget() {
        tokenStore.clear()
        mutableState.value = mutableState.value.copy(paired = false, snapshot = null)
    }

    fun refresh() = scope.launch { fetchSnapshot() }

    fun launchShortcut(shortcut: Shortcut) {
        val token = tokenStore.load() ?: return
        scope.launch {
            mutableState.value = mutableState.value.copy(busyShortcut = shortcut.id, error = null)
            runCatching { ble.request("launchShortcut", token, JSONObject().put("id", shortcut.id)) }
                .onFailure { mutableState.value = mutableState.value.copy(error = it.message) }
            mutableState.value = mutableState.value.copy(busyShortcut = null)
        }
    }

    private fun startPolling() {
        polling?.cancel()
        polling = scope.launch {
            while (isActive) { fetchSnapshot(); delay(5_000) }
        }
    }

    private suspend fun fetchSnapshot() {
        val token = tokenStore.load() ?: return
        runCatching { parseSnapshot(ble.request("snapshot", token)) }
            .onSuccess { mutableState.value = mutableState.value.copy(snapshot = it, error = null) }
            .onFailure { mutableState.value = mutableState.value.copy(error = it.message) }
    }

    @SuppressLint("MissingPermission")
    fun startLocation() {
        val request = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 2_000)
            .setMinUpdateIntervalMillis(1_000).setMinUpdateDistanceMeters(3f).build()
        locationClient.requestLocationUpdates(request, locationCallback, context.mainLooper)
    }

    fun stopLocation() = locationClient.removeLocationUpdates(locationCallback)

    private val locationCallback = object : LocationCallback() {
        override fun onLocationResult(result: LocationResult) { result.lastLocation?.let(::sendLocation) }
    }

    private fun sendLocation(location: Location) {
        val token = tokenStore.load() ?: return
        if (ble.state.value != ConnectionState.CONNECTED) return
        val battery = context.getSystemService(BatteryManager::class.java).getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
        val payload = JSONObject()
            .put("capturedAt", java.time.Instant.ofEpochMilli(location.time).toString())
            .put("latitude", location.latitude).put("longitude", location.longitude)
            .put("accuracyMeters", location.accuracy.toDouble())
            .put("altitudeMeters", if (location.hasAltitude()) location.altitude else JSONObject.NULL)
            .put("speedKph", if (location.hasSpeed()) location.speed * 3.6 else JSONObject.NULL)
            .put("bearingDegrees", if (location.hasBearing()) location.bearing.toDouble() else JSONObject.NULL)
            .put("batteryPercent", if (battery in 0..100) battery else JSONObject.NULL)
            .put("networkType", networkType())
        scope.launch { runCatching { ble.request("location", token, payload) } }
    }

    private fun networkType(): String {
        val manager = context.getSystemService(android.net.ConnectivityManager::class.java)
        val capabilities = manager.getNetworkCapabilities(manager.activeNetwork) ?: return "offline"
        return when {
            capabilities.hasTransport(android.net.NetworkCapabilities.TRANSPORT_WIFI) -> "wifi"
            capabilities.hasTransport(android.net.NetworkCapabilities.TRANSPORT_CELLULAR) -> "cellular"
            capabilities.hasTransport(android.net.NetworkCapabilities.TRANSPORT_ETHERNET) -> "ethernet"
            else -> "other"
        }
    }

    private fun stableDeviceId(): String {
        val source = Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID) ?: android.os.Build.MODEL
        return UUID.nameUUIDFromBytes(source.toByteArray(StandardCharsets.UTF_8)).toString()
    }

    private fun parseSnapshot(json: JSONObject): Snapshot {
        val system = json.getJSONObject("system")
        val memory = system.getJSONObject("memory")
        val disk = system.getJSONObject("disk")
        val diskValue = if (disk.optBoolean("available")) disk.optJSONObject("value") else null
        val car = json.optJSONObject("car")
        val stats = json.getJSONObject("statistics")
        val lifetime = stats.getJSONObject("lifetime")
        val records = stats.getJSONObject("records")
        fun availableNumber(name: String) = car?.optJSONObject(name)?.takeIf { it.optBoolean("available") }?.optDouble("value")
        fun availableString(name: String) = car?.optJSONObject(name)?.takeIf { it.optBoolean("available") }?.optString("value")
        val shortcutsJson = json.getJSONArray("shortcuts")
        val shortcuts = (0 until shortcutsJson.length()).map { index -> shortcutsJson.getJSONObject(index).let { Shortcut(it.getInt("id"), it.getString("name"), it.getString("value")) } }
        return Snapshot(
            json.getString("collectedAt"),
            SystemSummary(
                system.getString("hostname"),
                system.getJSONObject("cpu").getJSONObject("temperatureCelsius").takeIf { it.optBoolean("available") }?.optDouble("value"),
                ((memory.getDouble("usedBytes") / memory.getDouble("totalBytes")) * 100).toInt(),
                diskValue?.let { ((it.getDouble("usedBytes") / it.getDouble("totalBytes")) * 100).toInt() },
                system.getDouble("uptimeSeconds").toLong(),
                system.getJSONObject("headUnit").optBoolean("available")
            ),
            CarSummary(availableNumber("speedKph"), availableString("gear"), availableNumber("wheelAngleDegrees"),
                availableNumber("batteryPercent"), availableNumber("rangeKm"), availableNumber("batteryVoltage"),
                availableNumber("totalConsumptionKwh"), lifetime.optDouble("trackedDistanceKm", 0.0),
                lifetime.optNullableDouble("averageMovingSpeedKph"), records.optNullableDouble("peakSpeedKph")),
            shortcuts
        )
    }

    private fun JSONObject.optNullableDouble(name: String): Double? = if (isNull(name) || !has(name)) null else getDouble(name)
}
