package eu.darken.sdmse.analyzer.core.storage

import eu.darken.sdmse.analyzer.core.content.ContentGroup
import eu.darken.sdmse.analyzer.core.content.ContentItem
import eu.darken.sdmse.analyzer.core.device.DeviceStorage
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.common.adb.AdbManager
import eu.darken.sdmse.common.adb.canUseAdbNow
import eu.darken.sdmse.common.areas.DataArea
import eu.darken.sdmse.common.areas.DataAreaManager
import eu.darken.sdmse.common.areas.currentAreas
import eu.darken.sdmse.common.areas.hasFlags
import eu.darken.sdmse.common.ca.toCaString
import eu.darken.sdmse.common.clutter.Marker
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.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.APath
import eu.darken.sdmse.common.files.APathLookup
import eu.darken.sdmse.common.files.GatewaySwitch
import eu.darken.sdmse.common.files.ReadException
import eu.darken.sdmse.common.files.listFiles
import eu.darken.sdmse.common.files.local.LocalPath
import eu.darken.sdmse.common.flow.throttleLatest
import eu.darken.sdmse.common.forensics.FileForensics
import eu.darken.sdmse.common.forensics.OwnerInfo
import eu.darken.sdmse.common.pkgs.PkgRepo
import eu.darken.sdmse.common.pkgs.current
import eu.darken.sdmse.common.progress.Progress
import eu.darken.sdmse.common.progress.increaseProgress
import eu.darken.sdmse.common.progress.updateProgressCount
import eu.darken.sdmse.common.progress.updateProgressPrimary
import eu.darken.sdmse.common.progress.updateProgressSecondary
import eu.darken.sdmse.common.root.RootManager
import eu.darken.sdmse.common.root.canUseRootNow
import eu.darken.sdmse.common.storage.StorageManager2
import eu.darken.sdmse.common.storage.StorageVolumeX
import eu.darken.sdmse.common.user.UserHandle2
import eu.darken.sdmse.common.user.UserManager2
import eu.darken.sdmse.setup.SetupModule
import eu.darken.sdmse.setup.isComplete
import eu.darken.sdmse.setup.SetupBinding
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import javax.inject.Inject


class StorageScanner @Inject constructor(
    private val storageManager2: StorageManager2,
    private val pkgRepo: PkgRepo,
    private val rootManager: RootManager,
    private val adbManager: AdbManager,
    private val userManager2: UserManager2,
    private val gatewaySwitch: GatewaySwitch,
    private val fileForensics: FileForensics,
    private val dataAreaManager: DataAreaManager,
    private val appScannerFactory: AppStorageScanner.Factory,
    private val systemStorageScanner: SystemStorageScanner,
    @SetupBinding(SetupModule.Type.INVENTORY) private val inventorySetupModule: SetupModule,
    @SetupBinding(SetupModule.Type.USAGE_STATS) private val usageStatsSetupModule: SetupModule,
) : Progress.Host, Progress.Client {

    private val progressPub = MutableStateFlow<Progress.Data?>(
        Progress.Data(primary = eu.darken.sdmse.common.R.string.general_progress_preparing.toCaString())
    )

    override val progress: Flow<Progress.Data?> = progressPub.throttleLatest(50)

    override fun updateProgress(update: (Progress.Data?) -> Progress.Data?) {
        progressPub.value = update(progressPub.value)
    }

    private val topLevelDirs = mutableSetOf<OwnerInfo>()

    private var useRoot = false
    private var useAdb = false
    private var dataAreas = mutableSetOf<DataArea>()
    private var volume: StorageVolumeX? = null
    private lateinit var currentUser: UserHandle2

    suspend fun init(storage: DeviceStorage) {
        log(TAG) { "init(): $storage" }
        useRoot = rootManager.canUseRootNow()
        useAdb = adbManager.canUseAdbNow()
        currentUser = userManager2.currentUser().handle

        val availableVolumes = storageManager2.storageVolumes
        log(TAG) { "Available volumes:\n${availableVolumes.joinToString("\n")}" }

        val candidates = availableVolumes.filter { it.uuid == storage.id.internalId }

        volume = when {
            candidates.isEmpty() -> {
                log(TAG, WARN) { "No volume found for $storage" }
                null
            }

            candidates.size == 1 -> {
                candidates.single()
            }

            else -> {
                // Weird behavior on `samsung/beyond1ltexx/beyond1:12/SP1A.210812.016/G973FXXSGHWC1:user/release-keys` with dual apps set up:
                // scan(DeviceStorage(id=StorageId(internalId=null, externalId=41217664-9172-527a-b3d5-edabb50a7d69), label=CachedCaString("Primary storage"), type=PRIMARY, hardware=BUILT_IN, spaceCapacity=137438953472, spaceFree=36861681664, setupIncomplete=false))
                // StorageVolumeX(uuid=null, directory=/storage/emulated/0, userlabel=Internal shared storage, volumeX=StorageVolume: Internal shared storage, rootUri=content://com.android.externalstorage.documents/root/primary)
                // StorageVolumeX(uuid=null, directory=/storage/emulated/95, userlabel=Internal shared storage, volumeX=StorageVolume: Internal shared storage, rootUri=content://com.android.externalstorage.documents/root/primary)
                log(TAG, WARN) { "Multiple matches for ${storage.id}" }
                candidates.first()
            }
        }

        log(TAG) { "Volume <> Storage mapping:\nVolume: $volume\nfor\nStorage: $storage" }

        dataAreaManager.currentAreas()
            .filter { it.hasFlags(DataArea.Flag.PRIMARY) == volume?.isPrimary }
            .let {
                dataAreas.clear()
                dataAreas.addAll(it)
            }
    }

    suspend fun scan(storage: DeviceStorage): Collection<ContentCategory> {
        log(TAG) { "scan($storage)" }

        updateProgressPrimary(storage.label)
        updateProgressSecondary(eu.darken.sdmse.common.R.string.general_progress_loading)

        init(storage)

        return gatewaySwitch.useRes {
            fileForensics.useRes {
                val storageDir = volume?.directory
                    ?.let { LocalPath.build(it) }
                    ?.let { gatewaySwitch.lookup(it, type = GatewaySwitch.Type.AUTO) }
                log(TAG, INFO) { "Public storage dir is $storageDir for $volume" }

                updateProgressSecondary(eu.darken.sdmse.analyzer.R.string.analyzer_progress_scanning_storage)

                val storageChildren = storageDir?.lookedUp
                    ?.listFiles(gatewaySwitch)
                    ?.filter { storage.type == DeviceStorage.Type.PORTABLE || it.name != "Android" }
                    ?: emptySet()

                // Owner forensics needs a working app inventory. Skip it when setup is incomplete, and treat a
                // wholesale failure as a degraded scan too: on some OEMs attribution can still throw even when setup
                // reports complete (e.g. a stale PkgRepo error or a forensics/CSI failure), and that must not abort
                // the whole storage overview.
                val owners: Collection<OwnerInfo>? = when {
                    !inventorySetupModule.isComplete() -> null
                    else -> try {
                        storageChildren
                            .mapNotNull { fileForensics.findOwners(it) }
                            .filter { setOf(DataArea.Type.SDCARD, DataArea.Type.PORTABLE).contains(it.areaInfo.type) }
                            .onEach { log(TAG) { "Top level dir: $it" } }
                    } catch (e: CancellationException) {
                        throw e
                    } catch (e: Exception) {
                        log(TAG, ERROR) { "Top-level owner forensics failed, degrading storage scan: ${e.asLog()}" }
                        null
                    }
                }

                topLevelDirs.clear()
                topLevelDirs.addAll(owners ?: emptySet())

                val appScan = scanForApps(storage)
                val apps = appScan.category

                // Media stays writable only while owner attribution is trustworthy end to end: top-level forensics
                // resolved AND the app scan didn't hit an inventory failure (which leaves app-owned top-level dirs
                // undrained — they must not surface as writable user files).
                val ownersResolved = owners != null && !appScan.inventoryFailed

                updateProgressSecondary("Scanning media files")
                val media = storageDir
                    ?.let { scanForMedia(storage, it, storageChildren, ownersResolved) }
                    ?: MediaCategory(storage.id, emptySet())

                updateProgressSecondary("Scanning system data")
                val system = scanForSystem(storage, apps, media)

                log(TAG) { "Apps: ${apps?.spaceUsed}" }
                log(TAG) { "Media: ${media.spaceUsed}" }
                log(TAG) { "System: ${system?.spaceUsed}" }

                setOfNotNull(apps, media, system)
            }
        }
    }

    private data class AppScanResult(
        val category: AppCategory?,
        // The pkg inventory threw despite setup reporting complete. Unlike plain setup-incomplete (e.g. missing
        // usage stats, where owner attribution is still trustworthy), this must also degrade the media scan.
        val inventoryFailed: Boolean = false,
    )

    private suspend fun scanForApps(storage: DeviceStorage): AppScanResult {
        log(TAG) { "scanForApps($storage)" }
        if (!inventorySetupModule.isComplete()) {
            log(TAG, WARN) { "Inventory setup is incomplete, can't scan apps." }
            return AppScanResult(
                AppCategory(
                    storageId = storage.id,
                    setupIncomplete = true,
                    pkgStats = emptyMap()
                )
            )
        }

        if (!usageStatsSetupModule.isComplete()) {
            log(TAG, WARN) { "Usagestats setup is incomplete, can't scan apps." }
            return AppScanResult(
                AppCategory(
                    storageId = storage.id,
                    setupIncomplete = true,
                    pkgStats = emptyMap()
                )
            )
        }

        updateProgressPrimary(eu.darken.sdmse.analyzer.R.string.analyzer_progress_scanning_apps)

        // Same failure mode as the top-level owner forensics: setup can report complete while the pkg repo holds a
        // stale error. Degrade to "setup incomplete" instead of aborting the whole storage scan.
        val targetPkgs = try {
            pkgRepo.current()
                .filter { it.packageName != "android" }
                .filter { it.applicationInfo != null }
        } catch (e: CancellationException) {
            throw e
        } catch (e: Exception) {
            log(TAG, ERROR) { "App inventory unavailable despite completed setup, degrading app scan: ${e.asLog()}" }
            return AppScanResult(
                AppCategory(
                    storageId = storage.id,
                    setupIncomplete = true,
                    pkgStats = emptyMap()
                ),
                inventoryFailed = true,
            )
        }

        updateProgressCount(Progress.Count.Percent(targetPkgs.size))

        val appScanner = appScannerFactory.create(
            useRoot = useRoot,
            useAdb = useAdb,
            currentUser = currentUser,
            dataAreas = dataAreas,
            storage = storage,
        )

        val pkgStats = targetPkgs
            .map { pkg ->
                updateProgressSecondary(pkg.label ?: pkg.packageName.toCaString())

                // TODO This does not include nested markers, e.g. Android/something or DCIM/something
                // Supporting this is not trivial:
                // If a Top level folder has app owned sub folders we need to prevent deleting it
                // We would need to hide Android/data and Android/media, but not Android/<other>
                // How deep would this nesting logic have to go?

                val matchingTlds = topLevelDirs.filter {
                    val owner = it.getOwner(pkg.id) ?: return@filter false
                    !owner.hasFlag(Marker.Flag.CUSTODIAN) && !owner.hasFlag(Marker.Flag.COMMON)
                }.toSet()
                val request = AppStorageScanner.Request.Initial(
                    pkg = pkg,
                    extraData = matchingTlds.map { it.item }
                )
                val result = appScanner.process(request)
                topLevelDirs.removeAll(matchingTlds)
                increaseProgress()
                result.pkgStat
            }
            .filter { it.totalSize > 0L }
            .associateBy { it.id }

        return AppScanResult(
            if (pkgStats.isNotEmpty()) {
                AppCategory(storageId = storage.id, pkgStats = pkgStats)
            } else {
                null
            }
        )
    }

    private suspend fun scanForMedia(
        storage: DeviceStorage,
        mediaDir: APathLookup<*>,
        storageChildren: Collection<APath>,
        ownersResolved: Boolean,
    ): MediaCategory {
        log(TAG) { "scanForMedia($storage)" }
        updateProgressPrimary(eu.darken.sdmse.analyzer.R.string.analyzer_progress_scanning_userfiles)

        if (!ownersResolved) {
            log(TAG, WARN) { "Owner forensics unavailable, sizing media folders directly (read-only)." }
            updateProgressCount(Progress.Count.Percent(storageChildren.size))

            val topLevelContents: Collection<ContentItem> = storageChildren.mapNotNull { path ->
                updateProgressSecondary(path.userReadablePath)

                try {
                    path.sizeContentItem(gatewaySwitch)
                } catch (e: ReadException) {
                    log(TAG, ERROR) { "Failed to size top-level dir $path: ${e.asLog()}" }
                    null
                } finally {
                    increaseProgress()
                }
            }

            // All sized items are direct children of the storage root, so build the tree directly instead of
            // relying on segment-based nesting — a stray non-child path must not abort the degraded scan.
            val rootItem = ContentItem.fromLookup(mediaDir).copy(children = topLevelContents)

            val group = ContentGroup(
                label = eu.darken.sdmse.analyzer.R.string.analyzer_storage_content_type_media_label.toCaString(),
                contents = setOf(rootItem),
            )

            return MediaCategory(
                storageId = storage.id,
                groups = setOf(group),
                isReadOnly = true,
            )
        }

        updateProgressCount(Progress.Count.Percent(topLevelDirs.size))

        val topLevelContents: Collection<ContentItem> = topLevelDirs.mapNotNull { ownerInfo ->
            updateProgressSecondary(ownerInfo.areaInfo.file.userReadablePath)

            try {
                ownerInfo.areaInfo.file.walkContentItem(gatewaySwitch)
            } catch (e: ReadException) {
                log(TAG, ERROR) { "Failed to look up top-level dir ${ownerInfo.areaInfo.file}: ${e.asLog()}" }
                return@mapNotNull null
            } finally {
                increaseProgress()
            }
        }

        val rootItem = topLevelContents.plus(ContentItem.fromLookup(mediaDir)).toNestedContent().single()

        val group = ContentGroup(
            label = eu.darken.sdmse.analyzer.R.string.analyzer_storage_content_type_media_label.toCaString(),
            contents = setOf(rootItem),
        )

        return MediaCategory(
            storageId = storage.id,
            groups = setOf(group),
        )
    }

    private suspend fun scanForSystem(
        storage: DeviceStorage,
        appCategory: AppCategory?,
        mediaCategory: MediaCategory
    ): SystemCategory? {
        log(TAG) { "scanForSystem($storage)" }
        updateProgressPrimary(eu.darken.sdmse.analyzer.R.string.analyzer_progress_scanning_system)
        updateProgressSecondary(Progress.Data().secondary)
        updateProgressCount(Progress.Count.Indeterminate())

        if (storage.type != DeviceStorage.Type.PRIMARY) {
            log(TAG) { "Not a primary storage: $storage" }
            return null
        }

        // In the degraded scan media folders are du-sized (no owner forensics), which can overcount and push this
        // below zero. Clamp so the system category never reports a negative size.
        val unaccountedFor = (storage.spaceUsed - (appCategory?.spaceUsed ?: 0L) - mediaCategory.spaceUsed)
            .coerceAtLeast(0L)

        val contentItems = setOf(
            ContentItem.fromInaccessible(LocalPath.build("system")),
            ContentItem.fromInaccessible(LocalPath.build("data"))
        )

        val groups = setOf(
            ContentGroup(
                label = eu.darken.sdmse.analyzer.R.string.analyzer_storage_content_type_system_label.toCaString(),
                contents = contentItems
            )
        )

        return SystemCategory(
            storageId = storage.id,
            groups = groups,
            spaceUsedOverride = unaccountedFor,
            isBrowsable = useRoot || useAdb,
        )
    }

    suspend fun deepScanApp(storage: DeviceStorage, app: AppCategory.PkgStat): AppCategory.PkgStat {
        log(TAG) { "deepScanApp(): On $storage: $app" }
        init(storage)

        updateProgressPrimary(app.label)
        updateProgressSecondary(eu.darken.sdmse.common.R.string.general_progress_loading)

        val appScanner = appScannerFactory.create(
            useRoot = useRoot,
            useAdb = useAdb,
            currentUser = currentUser,
            dataAreas = dataAreas,
            storage = storage,
        )


        updateProgressSecondary(eu.darken.sdmse.common.R.string.general_progress_searching)

        val request = AppStorageScanner.Request.Reprocessing(
            pkgStat = app
        )
        val rescanned = appScanner.process(request)

        log(TAG) { "Rescanned ${app.id}: $rescanned" }

        return rescanned.pkgStat
    }

    suspend fun deepScanSystem(
        storage: DeviceStorage,
        existingGroupId: ContentGroup.Id,
        existingSpaceUsedOverride: Long?,
    ): SystemCategory {
        log(TAG) { "deepScanSystem(): On $storage" }
        init(storage)

        updateProgressPrimary(eu.darken.sdmse.analyzer.R.string.analyzer_storage_content_type_system_label)
        updateProgressSecondary(eu.darken.sdmse.common.R.string.general_progress_searching)

        return systemStorageScanner.scan(storage.id, existingGroupId, existingSpaceUsedOverride, dataAreas)
    }

    companion object {
        private val TAG = logTag("Analyzer", "Storage", "Scanner")
    }
}
