package pl.firmatpp.kierowca.diagnostics

import android.Manifest
import android.app.ActivityManager
import android.app.NotificationManager
import android.content.Context
import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import android.os.BatteryManager
import android.os.Environment
import android.os.PowerManager
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.WorkQuery
import com.google.firebase.FirebaseApp
import com.google.firebase.messaging.FirebaseMessaging
import java.io.File
import java.security.MessageDigest
import java.time.Instant
import java.util.concurrent.TimeUnit
import kotlin.coroutines.resume
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import pl.firmatpp.kierowca.BuildConfig
import pl.firmatpp.kierowca.data.DeviceInfoProvider
import pl.firmatpp.kierowca.data.TokenStore
import pl.firmatpp.kierowca.data.upload.DriverDatabase

data class DiagnosticEntry(
    val label: String,
    val value: String,
    val severity: DiagnosticSeverity = DiagnosticSeverity.Neutral,
)

enum class DiagnosticSeverity { Neutral, Good, Warning, Error }

data class DiagnosticSection(
    val title: String,
    val entries: List<DiagnosticEntry>,
)

data class DiagnosticSnapshot(
    val generatedAtEpochMillis: Long,
    val sections: List<DiagnosticSection>,
) {
    fun asPlainText(): String = buildString {
        appendLine("TPP Kierowca, raport diagnostyczny")
        appendLine("Wygenerowano: ${Instant.ofEpochMilli(generatedAtEpochMillis)}")
        sections.forEach { section ->
            appendLine()
            appendLine("[${section.title}]")
            section.entries.forEach { entry -> appendLine("${entry.label}: ${entry.value}") }
        }
        appendLine()
        append("Tokeny są celowo przedstawione jako fingerprinty, raport nie zawiera danych pozwalających przejąć sesję.")
    }
}

class DiagnosticSnapshotProvider(private val context: Context) {
    private val database = DriverDatabase.get(context)
    private val tokenStore = TokenStore(context)

    suspend fun capture(runtimeSections: List<DiagnosticSection>): DiagnosticSnapshot = withContext(Dispatchers.IO) {
        val token = tokenStore.read()
        val fcmToken = withTimeoutOrNull(5_000L) { readFirebaseToken() }
            ?: Result.failure(IllegalStateException("timeout po 5 s"))
        val device = DeviceInfoProvider(context, tokenStore).currentDeviceInfo()
        val network = currentNetworkEntries()
        val photos = database.photoUploadDao().allRecords()
        val dispatchSheets = database.dispatchSheetUploadDao().allRecords()
        val actions = database.routeActionDao().allUnsent()
        val points = database.routePointDao().allUnsent()
        val cacheDao = database.driverCacheDao()
        val bootstrapCaches = cacheDao.allBootstrapCaches()
        val routeCaches = cacheDao.allRouteCaches()
        val syncStates = cacheDao.allSyncStates()
        val queuedFiles = (photos.map { it.localPath } + dispatchSheets.map { it.localPath }).map(::File)
        val activeWork = activeWorkInfo()
        val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
        val firebaseOptions = FirebaseApp.getApps(context).firstOrNull()?.options
        val databaseFile = context.getDatabasePath("driver-local-outbox.db")

        val sections = buildList {
            add(
                DiagnosticSection(
                    "Aplikacja",
                    listOf(
                        DiagnosticEntry("Pakiet", BuildConfig.APPLICATION_ID),
                        DiagnosticEntry("Wersja", "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})"),
                        DiagnosticEntry("Typ buildu", BuildConfig.BUILD_TYPE),
                        DiagnosticEntry("Debug", yesNo(BuildConfig.DEBUG)),
                        DiagnosticEntry("API", BuildConfig.API_BASE_URL),
                        DiagnosticEntry("Reverb URL", BuildConfig.REVERB_WS_BASE_URL),
                        DiagnosticEntry("Reverb app key", secureIdentifierSummary(BuildConfig.REVERB_APP_KEY)),
                        DiagnosticEntry("Firebase App ID", firebaseOptions?.applicationId ?: "brak"),
                        DiagnosticEntry("Firebase Project ID", firebaseOptions?.projectId ?: "brak"),
                        DiagnosticEntry("Firebase Sender ID", firebaseOptions?.gcmSenderId ?: "brak"),
                        DiagnosticEntry("Pierwsza instalacja", Instant.ofEpochMilli(packageInfo.firstInstallTime).toString()),
                        DiagnosticEntry("Ostatnia aktualizacja", Instant.ofEpochMilli(packageInfo.lastUpdateTime).toString()),
                    ),
                ),
            )
            add(
                DiagnosticSection(
                    "Sesja i identyfikatory",
                    listOf(
                        DiagnosticEntry("Token sesji", secureIdentifierSummary(token), if (token == null) DiagnosticSeverity.Error else DiagnosticSeverity.Good),
                        DiagnosticEntry("Token FCM", fcmToken.fold(::secureIdentifierSummary) { "Błąd odczytu: ${it.message ?: it::class.java.simpleName}" }),
                        DiagnosticEntry("Device ID", secureIdentifierSummary(device.deviceId)),
                        DiagnosticEntry("Android ID", secureIdentifierSummary(device.androidId)),
                        DiagnosticEntry("Numer seryjny", secureIdentifierSummary(device.serialNumber)),
                    ),
                ),
            )
            add(
                DiagnosticSection(
                    "Urządzenie",
                    listOf(
                        DiagnosticEntry("Producent", device.manufacturer.orEmpty().ifBlank { "brak" }),
                        DiagnosticEntry("Marka / model", listOfNotNull(device.brand, device.model).joinToString(" ").ifBlank { "brak" }),
                        DiagnosticEntry("Device / product", listOfNotNull(device.device, device.product).joinToString(" / ").ifBlank { "brak" }),
                        DiagnosticEntry("Android", "${device.androidVersion ?: "?"}, API ${device.sdkInt}"),
                        DiagnosticEntry("Build fingerprint", Build.FINGERPRINT),
                        DiagnosticEntry("Sprzęt / bootloader", "${Build.HARDWARE} / ${Build.BOOTLOADER}"),
                        DiagnosticEntry("ABI", Build.SUPPORTED_ABIS.joinToString()),
                        DiagnosticEntry("Język", context.resources.configuration.locales.toLanguageTags().ifBlank { "brak" }),
                        DiagnosticEntry("Strefa czasowa", java.util.TimeZone.getDefault().id),
                        DiagnosticEntry("Pamięć aplikacji", storageSummary(context.filesDir)),
                        DiagnosticEntry("Pamięć współdzielona", storageSummary(Environment.getExternalStorageDirectory())),
                        DiagnosticEntry("RAM", memorySummary()),
                        DiagnosticEntry("Bateria", batterySummary()),
                    ),
                ),
            )
            add(DiagnosticSection("Sieć urządzenia", network))
            add(
                DiagnosticSection(
                    "Uprawnienia i system",
                    listOf(
                        permissionEntry("Aparat", Manifest.permission.CAMERA),
                        permissionEntry("Lokalizacja dokładna", Manifest.permission.ACCESS_FINE_LOCATION),
                        permissionEntry("Lokalizacja przybliżona", Manifest.permission.ACCESS_COARSE_LOCATION),
                        notificationPermissionEntry(),
                        DiagnosticEntry("Powiadomienia systemowe", yesNo(NotificationManagerCompat.from(context).areNotificationsEnabled())),
                        DiagnosticEntry("Optymalizacja baterii wyłączona", yesNo(isIgnoringBatteryOptimizations())),
                        DiagnosticEntry("Kanały powiadomień", notificationChannelSummary()),
                    ),
                ),
            )
            addAll(runtimeSections)
            add(
                DiagnosticSection(
                    "WorkManager",
                    buildList {
                        add(DiagnosticEntry("Aktywne zadania", activeWork.size.toString(), if (activeWork.isEmpty()) DiagnosticSeverity.Good else DiagnosticSeverity.Warning))
                        activeWork.forEach { work ->
                            add(
                                DiagnosticEntry(
                                    work.tags.map { it.substringAfterLast('.') }.sorted().joinToString().ifBlank { work.id.toString() },
                                    "${work.state}, próba ${work.runAttemptCount}, id ${work.id}",
                                ),
                            )
                        }
                    },
                ),
            )
            add(
                DiagnosticSection(
                    "Lokalna kolejka",
                    buildList {
                        add(DiagnosticEntry("Zdjęcia", statusSummary(photos.map { it.status }), queueSeverity(photos.map { it.status })))
                        add(DiagnosticEntry("Karty spedycyjne", statusSummary(dispatchSheets.map { it.status }), queueSeverity(dispatchSheets.map { it.status })))
                        add(DiagnosticEntry("Akcje kursu", statusSummary(actions.map { it.status }), queueSeverity(actions.map { it.status })))
                        add(DiagnosticEntry("Punkty GPS", statusSummary(points.map { it.status }), queueSeverity(points.map { it.status })))
                        add(DiagnosticEntry("Pliki lokalne", "${queuedFiles.count(File::exists)}/${queuedFiles.size} obecnych, ${formatBytes(queuedFiles.filter(File::exists).sumOf(File::length))}"))
                        add(DiagnosticEntry("Brakujące pliki", queuedFiles.filterNot(File::exists).joinToString { it.name }.ifBlank { "brak" }, if (queuedFiles.any { !it.exists() }) DiagnosticSeverity.Error else DiagnosticSeverity.Good))
                        val errors = (photos.mapNotNull { it.lastError } + dispatchSheets.mapNotNull { it.lastError } + actions.mapNotNull { it.lastError } + points.mapNotNull { it.lastError }).distinct()
                        add(DiagnosticEntry("Błędy kolejki", errors.joinToString(" | ").ifBlank { "brak" }, if (errors.isEmpty()) DiagnosticSeverity.Good else DiagnosticSeverity.Warning))
                    },
                ),
            )
            add(
                DiagnosticSection(
                    "Cache i rewizje",
                    buildList {
                        add(DiagnosticEntry("Wersja schematu Room", databaseSchemaVersion().toString()))
                        add(DiagnosticEntry("Plik bazy", "${if (databaseFile.exists()) formatBytes(databaseFile.length()) else "brak"}, ${databaseFile.absolutePath}"))
                        add(DiagnosticEntry("Snapshoty list tras", "${bootstrapCaches.size}: ${bootstrapCaches.joinToString { "${it.date}@v${it.version ?: "?"}" }.ifBlank { "brak" }}"))
                        add(DiagnosticEntry("Snapshoty tras", "${routeCaches.size}: ${routeCaches.joinToString { "${it.routeId}@v${it.version ?: "?"}" }.ifBlank { "brak" }}"))
                        add(DiagnosticEntry("Scope synchronizacji", syncStates.size.toString()))
                        syncStates.forEach { sync ->
                            add(
                                DiagnosticEntry(
                                    sync.scope,
                                    listOfNotNull(sync.date, sync.routeId).joinToString(" / ").let { target ->
                                        "${target.ifBlank { "globalny" }}, v${sync.version}, checksum ${shortFingerprint(sync.checksum)}, zapis ${Instant.ofEpochMilli(sync.syncedAtEpochMillis)}"
                                    },
                                ),
                            )
                        }
                    },
                ),
            )
        }

        DiagnosticSnapshot(System.currentTimeMillis(), sections)
    }

    private fun currentNetworkEntries(): List<DiagnosticEntry> {
        val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
        val activeNetwork = manager.activeNetwork
        val capabilities = activeNetwork?.let(manager::getNetworkCapabilities)
        if (capabilities == null) {
            return listOf(DiagnosticEntry("Aktywna sieć", "brak", DiagnosticSeverity.Error))
        }
        val transports = buildList {
            if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) add("Wi-Fi")
            if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) add("sieć komórkowa")
            if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) add("Ethernet")
            if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) add("VPN")
            if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH)) add("Bluetooth")
        }.ifEmpty { listOf("inny") }
        val validated = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
            capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
        val links = activeNetwork?.let(manager::getLinkProperties)
        return listOf(
            DiagnosticEntry("Transport", transports.joinToString()),
            DiagnosticEntry("Internet zweryfikowany", yesNo(validated), if (validated) DiagnosticSeverity.Good else DiagnosticSeverity.Error),
            DiagnosticEntry("NET_CAPABILITY_INTERNET", yesNo(capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET))),
            DiagnosticEntry("NET_CAPABILITY_VALIDATED", yesNo(capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED))),
            DiagnosticEntry("Sieć mierzona", yesNo(!capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED))),
            DiagnosticEntry("Roaming", yesNo(!capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING))),
            DiagnosticEntry("Pasmo downstream / upstream", "${capabilities.linkDownstreamBandwidthKbps} / ${capabilities.linkUpstreamBandwidthKbps} kb/s"),
            DiagnosticEntry("Interfejs", links?.interfaceName ?: "brak"),
            DiagnosticEntry("Adresy IP", links?.linkAddresses?.joinToString { it.toString() }?.ifBlank { "brak" } ?: "brak"),
            DiagnosticEntry("DNS", links?.dnsServers?.joinToString { it.hostAddress ?: it.toString() }?.ifBlank { "brak" } ?: "brak"),
            DiagnosticEntry("MTU", links?.mtu?.toString() ?: "brak"),
            DiagnosticEntry("Proxy", links?.httpProxy?.toString() ?: "brak"),
        )
    }

    private fun activeWorkInfo(): List<WorkInfo> = runCatching {
        val query = WorkQuery.Builder.fromStates(
            listOf(WorkInfo.State.ENQUEUED, WorkInfo.State.RUNNING, WorkInfo.State.BLOCKED),
        ).build()
        WorkManager.getInstance(context).getWorkInfos(query).get(3, TimeUnit.SECONDS)
    }.getOrDefault(emptyList())

    private fun databaseSchemaVersion(): Int = runCatching {
        database.openHelper.readableDatabase.query("PRAGMA user_version").use { cursor ->
            if (cursor.moveToFirst()) cursor.getInt(0) else -1
        }
    }.getOrDefault(-1)

    private fun memorySummary(): String {
        val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
        val info = ActivityManager.MemoryInfo()
        manager.getMemoryInfo(info)
        return "wolne ${formatBytes(info.availMem)} z ${formatBytes(info.totalMem)}, niski stan: ${yesNo(info.lowMemory)}"
    }

    private fun batterySummary(): String {
        val manager = context.getSystemService(Context.BATTERY_SERVICE) as BatteryManager
        val level = manager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY).takeIf { it >= 0 }?.let { "$it%" } ?: "brak"
        return "$level, ładowanie: ${yesNo(manager.isCharging)}"
    }

    private fun notificationChannelSummary(): String {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return "niewspierane"
        val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        val channels = manager.notificationChannels
        return if (channels.isEmpty()) "0" else channels.joinToString { "${it.id}:${it.importance}" }
    }

    private fun permissionEntry(label: String, permission: String): DiagnosticEntry {
        val granted = ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
        return DiagnosticEntry(label, if (granted) "przyznane" else "brak", if (granted) DiagnosticSeverity.Good else DiagnosticSeverity.Warning)
    }

    private fun notificationPermissionEntry(): DiagnosticEntry =
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
            DiagnosticEntry("Uprawnienie powiadomień", "niewymagane na tej wersji Androida")
        } else {
            permissionEntry("Uprawnienie powiadomień", Manifest.permission.POST_NOTIFICATIONS)
        }

    private fun isIgnoringBatteryOptimizations(): Boolean {
        val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
        return powerManager.isIgnoringBatteryOptimizations(context.packageName)
    }

    private suspend fun readFirebaseToken(): Result<String?> = suspendCancellableCoroutine { continuation ->
        runCatching { FirebaseMessaging.getInstance().token }
            .onFailure { continuation.resume(Result.failure(it)) }
            .onSuccess { task ->
                task.addOnCompleteListener { completed ->
                    if (!continuation.isActive) return@addOnCompleteListener
                    if (completed.isSuccessful) continuation.resume(Result.success(completed.result))
                    else continuation.resume(Result.failure(completed.exception ?: IllegalStateException("Nieznany błąd FCM")))
                }
            }
    }
}

internal fun secureIdentifierSummary(value: String?): String {
    if (value.isNullOrBlank()) return "brak"
    val fingerprint = MessageDigest.getInstance("SHA-256")
        .digest(value.toByteArray())
        .joinToString("") { "%02x".format(it) }
        .take(12)
    val suffix = value.takeLast(4)
    return "obecny, długość ${value.length}, SHA-256 $fingerprint, końcówka …$suffix"
}

private fun shortFingerprint(value: String): String =
    MessageDigest.getInstance("SHA-256").digest(value.toByteArray()).joinToString("") { "%02x".format(it) }.take(10)

private fun statusSummary(statuses: List<String>): String =
    if (statuses.isEmpty()) "0" else statuses.groupingBy { it }.eachCount().entries.sortedBy { it.key }.joinToString { "${it.key}: ${it.value}" }

private fun queueSeverity(statuses: List<String>): DiagnosticSeverity = when {
    statuses.any { it.contains("PERMANENT") || it.contains("CONFLICT") } -> DiagnosticSeverity.Error
    statuses.any { it.contains("FAILED") } -> DiagnosticSeverity.Warning
    statuses.isNotEmpty() -> DiagnosticSeverity.Warning
    else -> DiagnosticSeverity.Good
}

private fun storageSummary(directory: File): String =
    "wolne ${formatBytes(directory.usableSpace)} z ${formatBytes(directory.totalSpace)}"

private fun formatBytes(bytes: Long): String {
    if (bytes < 1024) return "$bytes B"
    val units = listOf("KB", "MB", "GB", "TB")
    var value = bytes.toDouble()
    var unit = -1
    while (value >= 1024 && unit < units.lastIndex) {
        value /= 1024
        unit++
    }
    return "%.1f %s".format(java.util.Locale.US, value, units[unit])
}

private fun yesNo(value: Boolean): String = if (value) "tak" else "nie"
