package eu.darken.sdmse.analyzer.core

import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.IntoSet
import eu.darken.sdmse.analyzer.core.content.ContentDeleteTask
import eu.darken.sdmse.analyzer.core.content.ContentGroup
import eu.darken.sdmse.analyzer.core.device.DeviceStorage
import eu.darken.sdmse.analyzer.core.device.DeviceStorageScanTask
import eu.darken.sdmse.analyzer.core.device.DeviceStorageScanner
import eu.darken.sdmse.analyzer.core.storage.AppDeepScanTask
import eu.darken.sdmse.analyzer.core.storage.StorageScanTask
import eu.darken.sdmse.analyzer.core.storage.SystemDeepScanTask
import eu.darken.sdmse.analyzer.core.storage.StorageScanner
import eu.darken.sdmse.analyzer.core.storage.categories.AppCategory
import eu.darken.sdmse.analyzer.core.storage.categories.ContentCategory
import eu.darken.sdmse.analyzer.core.storage.categories.MediaCategory
import eu.darken.sdmse.analyzer.core.storage.categories.SystemCategory
import eu.darken.sdmse.analyzer.core.storage.categories.isContentReadOnly
import eu.darken.sdmse.analyzer.core.storage.categories.ownsGroup
import eu.darken.sdmse.analyzer.core.storage.toFlatContent
import eu.darken.sdmse.analyzer.core.storage.toNestedContent
import eu.darken.sdmse.common.collections.mutate
import eu.darken.sdmse.common.coroutine.AppScope
import eu.darken.sdmse.common.debug.logging.Logging.Priority.ERROR
import eu.darken.sdmse.common.debug.logging.Logging.Priority.INFO
import eu.darken.sdmse.common.debug.logging.Logging.Priority.VERBOSE
import eu.darken.sdmse.common.debug.logging.Logging.Priority.WARN
import eu.darken.sdmse.common.debug.logging.asLog
import eu.darken.sdmse.common.debug.logging.log
import eu.darken.sdmse.common.debug.logging.logTag
import eu.darken.sdmse.common.files.GatewaySwitch
import eu.darken.sdmse.common.files.MediaStoreTool
import eu.darken.sdmse.common.files.delete
import eu.darken.sdmse.common.files.filterDistinctRoots
import eu.darken.sdmse.common.files.isAncestorOf
import eu.darken.sdmse.common.files.local.LocalPath
import eu.darken.sdmse.common.files.matches
import eu.darken.sdmse.common.flow.replayingShare
import eu.darken.sdmse.common.getQuantityString2
import eu.darken.sdmse.common.progress.Progress
import eu.darken.sdmse.common.progress.updateProgressPrimary
import eu.darken.sdmse.common.progress.updateProgressSecondary
import eu.darken.sdmse.common.progress.withProgress
import eu.darken.sdmse.common.sharedresource.SharedResource
import eu.darken.sdmse.common.sharedresource.keepResourceHoldersAlive
import eu.darken.sdmse.common.storage.StorageId
import eu.darken.sdmse.main.core.SDMTool
import eu.darken.sdmse.setup.IncompleteSetupException
import eu.darken.sdmse.setup.SetupModule
import eu.darken.sdmse.setup.isComplete
import eu.darken.sdmse.stats.core.SpaceTracker
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withTimeoutOrNull
import javax.inject.Inject
import kotlin.time.Duration.Companion.minutes
import eu.darken.sdmse.setup.SetupBinding
import javax.inject.Provider
import javax.inject.Singleton

@Singleton
class Analyzer @Inject constructor(
    @AppScope private val appScope: CoroutineScope,
    private val deviceScanner: Provider<DeviceStorageScanner>,
    private val storageScanner: Provider<StorageScanner>,
    private val gatewaySwitch: GatewaySwitch,
    @SetupBinding(SetupModule.Type.INVENTORY) private val appInventorySetupModule: SetupModule,
    @SetupBinding(SetupModule.Type.STORAGE) private val storageSetupModule: SetupModule,
    private val mediaStoreTool: MediaStoreTool,
    private val spaceTracker: SpaceTracker,
) : SDMTool, Progress.Client {

    override val type: SDMTool.Type = SDMTool.Type.ANALYZER

    override val sharedResource = SharedResource.createKeepAlive(TAG, appScope)

    private val progressPub = MutableStateFlow<Progress.Data?>(null)
    override val progress: Flow<Progress.Data?> = progressPub
    override fun updateProgress(update: (Progress.Data?) -> Progress.Data?) {
        progressPub.value = update(progressPub.value)
    }

    private val storageDevices = MutableStateFlow(emptySet<DeviceStorage>())
    private val storageCategories = MutableStateFlow(emptyMap<StorageId, Collection<ContentCategory>>())
    val data: Flow<Data> = combine(
        storageDevices,
        storageCategories,
    ) { storages, categories ->
        val allGroups = categories
            .map { category ->
                category.value
                    .map { it.groups }
                    .flatten()
                    .map { it.id to it }

            }
            .flatten()
            .toMap()

        Data(
            storages = storages,
            categories = categories,
            groups = allGroups
        )
    }

    override val state: Flow<State> = combine(
        data,
        progress,
    ) { data, progress ->
        State(
            data = data,
            progress = progress,
        )
    }.replayingShare(appScope)

    init {
        // Device-storage scans bake `setupIncomplete` into each DeviceStorage at scan time. If a scan
        // ran before the storage permission was granted, that stale flag (and the permission-limited
        // content) sticks in the cached data until the next scan — so the UI keeps showing
        // "permissions missing" and the storage card bounces through a Setup screen that immediately
        // closes. Whenever setup is complete AND the cached data carries the stale flag, re-scan.
        // Observing both flows covers setup completing mid-scan: the finishing scan publishes the
        // stale devices after the setup transition, which re-evaluates the condition. No loop: a
        // successful rescan bakes setupIncomplete=false into every device, and a failed one leaves
        // the devices cleared (scan start empties them) — either way the condition turns false.
        combine(
            storageSetupModule.state.map { it.isComplete }.distinctUntilChanged(),
            storageDevices,
        ) { setupComplete, devices ->
            setupComplete && devices.any { it.setupIncomplete }
        }
            .distinctUntilChanged()
            .filter { staleWhileComplete -> staleWhileComplete }
            .onEach {
                log(TAG, INFO) { "Storage setup is complete but cached storage data is stale; re-scanning" }
                try {
                    submit(DeviceStorageScanTask())
                } catch (e: CancellationException) {
                    throw e
                } catch (e: Exception) {
                    // Background heal only — appScope has no exception handler, so anything
                    // escaping here would kill the whole process. The next manual scan recovers.
                    log(TAG, ERROR) { "Automatic storage re-scan failed: ${e.asLog()}" }
                }
            }
            .launchIn(appScope)
    }

    private val jobLock = Mutex()
    override suspend fun submit(task: SDMTool.Task): SDMTool.Task.Result = jobLock.withLock {
        task as AnalyzerTask
        log(TAG) { "submit($task) starting..." }
        updateProgress { Progress.Data() }
        try {
            val result = keepResourceHoldersAlive(gatewaySwitch) {
                when (task) {
                    is DeviceStorageScanTask -> scanStorageDevices(task)
                    is StorageScanTask -> scanStorageContents(task)
                    is ContentDeleteTask -> deleteContent(task)
                    is AppDeepScanTask -> deepScanApp(task)
                    is SystemDeepScanTask -> deepScanSystem(task)
                    else -> throw UnsupportedOperationException("Unsupported task: $task")
                }
            }
            log(TAG, INFO) { "submit($task) finished: $result" }
            result
        } finally {
            updateProgress { null }
        }
    }

    private suspend fun scanStorageDevices(task: DeviceStorageScanTask): DeviceStorageScanTask.Result {
        log(TAG, VERBOSE) { "scanStorageDevices(): $task" }

        storageDevices.value = emptySet()
        storageCategories.value = emptyMap()

        val scanner = deviceScanner.get()
        val storages = scanner.withProgress(this) { scan() }

        storageDevices.value = storages
        spaceTracker.recordSnapshot(storages.map {
            SpaceTracker.StorageSnapshot(
                storageId = it.id.externalId.toString(),
                spaceFree = it.spaceFree,
                spaceCapacity = it.spaceCapacity,
            )
        }.toSet())

        return DeviceStorageScanTask.Result(itemCount = storages.size)
    }

    private suspend fun scanStorageContents(task: StorageScanTask): DeviceStorageScanTask.Result {
        log(TAG, VERBOSE) { "scanStorageContents(): $task" }

        // Inventory completeness is checked per-category inside StorageScanner so that media/system
        // scans still succeed when the app inventory is unavailable (e.g. Huawei TAF sandbox).

        val target = storageDevices.value.singleOrNull { it.id == task.target }
            ?: throw IllegalStateException("Couldn't find ${task.target}")

        val scanner = storageScanner.get()

        val start = System.currentTimeMillis()

        val categories = scanner.withProgress(this) { scan(target) }

        val stop = System.currentTimeMillis()
        log(TAG) { "scanStorageContents() took ${stop - start}ms" }

        storageCategories.value = storageCategories.value.mutate {
            this[target.id] = categories
        }

        return DeviceStorageScanTask.Result(itemCount = 0)
    }

    private suspend fun deleteContent(task: ContentDeleteTask): ContentDeleteTask.Result {
        log(TAG, VERBOSE) { "deleteContent(): $task" }

        val oldCategory: ContentCategory = storageCategories.value[task.storageId]
            ?.singleOrNull { it.ownsGroup(task.groupId) }
            ?: throw IllegalStateException("Can't find category and group for ${task.groupId}")
        val oldGroup = oldCategory.groups.single { it.id == task.groupId }

        if (oldCategory.isContentReadOnly) {
            val what = if (oldCategory is SystemCategory) "system content" else "read-only media content"
            log(TAG, WARN) { "deleteContent(): Blocked — $what is read-only" }
            throw UnsupportedOperationException("Deletion is not supported for $what")
        }

        // App tasks are rebuilt against their pkgStat after deletion — resolve it now so a malformed task
        // (missing/wrong targetPkg, foreign group) fails before any file is deleted, not after.
        val oldPkg = (oldCategory as? AppCategory)?.let { category ->
            val pkg = category.pkgStats[task.targetPkg]
                ?: throw IllegalStateException("Can't find pkgStat ${task.targetPkg} for ${task.groupId}")
            val groups = setOfNotNull(pkg.appCode, pkg.appData, pkg.appMedia, pkg.extraData)
            if (groups.none { it == oldGroup }) {
                throw IllegalStateException("${pkg.id} has no content group matching ${task.groupId}")
            }
            pkg
        }

        updateProgressPrimary {
            it.getString(
                eu.darken.sdmse.common.R.string.general_progress_deleting_x,
                it.getQuantityString2(eu.darken.sdmse.common.R.plurals.result_x_items, task.targets.size)
            )
        }

        task.targets
            .filterDistinctRoots()
            .forEach { target ->
                log(TAG) { "Deleting $target" }
                updateProgressSecondary(target.userReadablePath)
                target.delete(gatewaySwitch, recursive = true)
                (target as? LocalPath)?.let { mediaStoreTool.notifyDeleted(it) }
            }

        var freedSpace = 0L
        val newContents = oldGroup.contents
            .toFlatContent()
            .filter { item ->
                val deleted = task.targets.any { it.isAncestorOf(item.path) || it.matches(item.path) }
                if (deleted) freedSpace += item.itemSize ?: 0L
                !deleted
            }
            .toNestedContent()

        val newGroup = oldGroup.copy(contents = newContents)

        val newCategory = when (oldCategory) {
            is AppCategory -> {
                checkNotNull(oldPkg) { "pkgStat was preflight-resolved for app categories" }
                val newPkg = when {
                    oldPkg.appCode == oldGroup -> oldPkg.copy(appCode = newGroup)
                    oldPkg.appData == oldGroup -> oldPkg.copy(appData = newGroup)
                    oldPkg.appMedia == oldGroup -> oldPkg.copy(appMedia = newGroup)
                    oldPkg.extraData == oldGroup -> oldPkg.copy(extraData = newGroup)
                    else -> error("unreachable: group membership was preflight-validated")
                }
                oldCategory.copy(
                    pkgStats = oldCategory.pkgStats.mutate {
                        this[oldPkg.id] = newPkg
                    }
                )
            }

            is MediaCategory -> oldCategory.copy(groups = oldCategory.groups.minus(oldGroup).plus(newGroup))

            is SystemCategory -> {
                throw UnsupportedOperationException("Deletion is not supported for system content")
            }
        }

        storageCategories.value = storageCategories.value.mutate {
            this[task.storageId] = this[task.storageId]!!.minus(oldCategory).plus(newCategory)
        }

        mediaStoreTool.flush()

        return ContentDeleteTask.Result(
            affectedSpace = freedSpace,
            affectedPaths = task.targets,
        )
    }

    private suspend fun deepScanApp(task: AppDeepScanTask): AppDeepScanTask.Result {
        log(TAG, VERBOSE) { "deepScanApp(): $task" }

        if (!appInventorySetupModule.isComplete()) {
            log(TAG, WARN) { "SetupModule INVENTORY is not complete" }
            throw IncompleteSetupException(SetupModule.Type.INVENTORY)
        }

        val targetStorage = storageDevices.value.singleOrNull { it.id == task.storageId }
            ?: throw IllegalStateException("Couldn't find ${task.storageId}")
        val targetCategory = storageCategories.first()[targetStorage.id]!!.filterIsInstance<AppCategory>().single()
        val targetApp = targetCategory.pkgStats[task.installId]!!

        val start = System.currentTimeMillis()

        val updatedApp = withTimeoutOrNull(DEEP_SCAN_TIMEOUT) {
            storageScanner.get().withProgress(this@Analyzer) { deepScanApp(targetStorage, targetApp) }
        }

        val stop = System.currentTimeMillis()
        log(TAG) { "deepScanApp() took ${stop - start}ms" }

        if (updatedApp == null) {
            log(TAG, WARN) { "deepScanApp() timed out after ${stop - start}ms, keeping shallow data" }
            return AppDeepScanTask.Result(false)
        }

        storageCategories.value = storageCategories.value.mutate {
            this[targetStorage.id] = this[targetStorage.id]!!.map { category ->
                if (category !is AppCategory) return@map category
                category.copy(pkgStats = category.pkgStats.mutate { replace(task.installId, updatedApp) })
            }
        }

        return AppDeepScanTask.Result(true)
    }

    private suspend fun deepScanSystem(task: SystemDeepScanTask): SystemDeepScanTask.Result {
        log(TAG, VERBOSE) { "deepScanSystem(): $task" }

        val targetStorage = storageDevices.value.singleOrNull { it.id == task.storageId }
            ?: throw IllegalStateException("Couldn't find ${task.storageId}")
        val existingCategory = storageCategories.first()[targetStorage.id]
            ?.filterIsInstance<SystemCategory>()
            ?.singleOrNull()
            ?: throw IllegalStateException("No SystemCategory for ${task.storageId}")

        val existingGroupId = existingCategory.groups.singleOrNull()?.id
            ?: throw IllegalStateException("No group in SystemCategory for ${task.storageId}")

        val start = System.currentTimeMillis()

        val updatedCategory = withTimeoutOrNull(DEEP_SCAN_TIMEOUT) {
            storageScanner.get().withProgress(this@Analyzer) {
                deepScanSystem(targetStorage, existingGroupId, existingCategory.spaceUsedOverride)
            }
        }

        val stop = System.currentTimeMillis()
        log(TAG) { "deepScanSystem() took ${stop - start}ms" }

        if (updatedCategory == null) {
            log(TAG, WARN) { "deepScanSystem() timed out after ${stop - start}ms, keeping existing data" }
            return SystemDeepScanTask.Result(false)
        }

        storageCategories.value = storageCategories.value.mutate {
            this[targetStorage.id] = this[targetStorage.id]!!.map { category ->
                if (category !is SystemCategory) return@map category
                updatedCategory
            }
        }

        return SystemDeepScanTask.Result(true)
    }

    data class State(
        val data: Data,
        val progress: Progress.Data?,
    ) : SDMTool.State

    data class Data(
        val storages: Set<DeviceStorage> = emptySet(),
        val categories: Map<StorageId, Collection<ContentCategory>> = emptyMap(),
        val groups: Map<ContentGroup.Id, ContentGroup> = emptyMap(),
    )

    @InstallIn(SingletonComponent::class)
    @Module
    abstract class DIM {
        @Binds @IntoSet abstract fun mod(mod: Analyzer): SDMTool
    }

    companion object {
        private val DEEP_SCAN_TIMEOUT = 5.minutes
        private val TAG = logTag("Analyzer")
    }
}