package eu.darken.sdmse.deduplicator.core.deleter

import eu.darken.sdmse.common.debug.logging.Logging.Priority.VERBOSE
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.local.LocalPath
import eu.darken.sdmse.common.flow.throttleLatest
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.deduplicator.core.Deduplicator
import eu.darken.sdmse.deduplicator.core.Duplicate
import eu.darken.sdmse.deduplicator.core.arbiter.ArbiterStrategy
import eu.darken.sdmse.deduplicator.core.arbiter.DuplicatesArbiter
import eu.darken.sdmse.deduplicator.core.tasks.DeduplicatorDeleteTask
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import javax.inject.Inject

class DuplicatesDeleter @Inject constructor(
    private val gatewaySwitch: GatewaySwitch,
    private val arbiter: DuplicatesArbiter,
    private val mediaStoreTool: MediaStoreTool,
) : Progress.Host, Progress.Client {

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

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

    suspend fun delete(
        task: DeduplicatorDeleteTask,
        data: Deduplicator.Data
    ): Deleted {
        log(TAG) { "Processing $task" }

        updateProgressPrimary(eu.darken.sdmse.common.R.string.general_progress_deleting)

        // Snapshot strategy once at the start to avoid redundant DataStore access
        val strategy = arbiter.getStrategy()

        val deletedDupes = when (task.mode) {
            is DeduplicatorDeleteTask.TargetMode.All -> data.targetAll(strategy)
            is DeduplicatorDeleteTask.TargetMode.Clusters -> data.targetClusters(
                targets = task.mode.targets,
                deleteAll = task.mode.deleteAll,
                strategy = strategy,
            )

            is DeduplicatorDeleteTask.TargetMode.Groups -> data.targetGroups(
                targets = task.mode.targets,
                deleteAll = task.mode.deleteAll,
                strategy = strategy,
            )

            is DeduplicatorDeleteTask.TargetMode.Duplicates -> data.targetDuplicates(
                targets = task.mode.targets,
            )
        }

        log(TAG) { "Deletion finished, deleted ${deletedDupes.size} duplicates" }

        mediaStoreTool.flush()

        return Deleted(success = deletedDupes.toSet())
    }

    private suspend fun Deduplicator.Data.targetAll(strategy: ArbiterStrategy): Collection<Duplicate> {
        log(TAG, VERBOSE) { "targetAll()" }
        return targetClusters(
            targets = clusters.map { it.identifier }.toSet(),
            strategy = strategy,
        )
    }

    private suspend fun Deduplicator.Data.targetClusters(
        targets: Set<Duplicate.Cluster.Id>,
        deleteAll: Boolean = false,
        strategy: ArbiterStrategy,
    ): Collection<Duplicate> {
        log(TAG, VERBOSE) { "#targetClusters(deleteAll=$deleteAll): $targets" }
        return clusters
            .filter { cluster -> targets.contains(cluster.identifier) }
            .map { cluster ->
                log(TAG, VERBOSE) { "_targetClusters(): Deleting from ${cluster.identifier} (groups=${cluster.count})" }
                if (deleteAll) {
                    targetGroups(
                        targets = cluster.groups.map { it.identifier }.toSet(),
                        deleteAll = true,
                        strategy = strategy,
                    )
                } else {
                    val favoriteId = cluster.favoriteGroupIdentifier
                    val (favorite, rest) = if (favoriteId != null) {
                        val fav = cluster.groups.first { it.identifier == favoriteId }
                        fav to cluster.groups.filter { it.identifier != favoriteId }.toSet()
                    } else {
                        arbiter.decideGroups(cluster.groups, strategy)
                    }
                    log(TAG, VERBOSE) { "_targetClusters(): Our favorite is $favorite" }

                    val favoriteResult = targetGroups(
                        targets = setOf(favorite.identifier),
                        deleteAll = false,
                        strategy = strategy,
                        isKeptGroup = true,
                    )
                    val restResult = targetGroups(
                        targets = rest.map { it.identifier }.toSet(),
                        deleteAll = true,
                        strategy = strategy,
                    )
                    favoriteResult + restResult
                }
            }
            .flatten()
    }

    private suspend fun Deduplicator.Data.targetGroups(
        targets: Set<Duplicate.Group.Id>,
        deleteAll: Boolean = false,
        strategy: ArbiterStrategy,
        isKeptGroup: Boolean = false,
    ): Collection<Duplicate> {
        log(TAG, VERBOSE) { "#_targetGroups(deleteAll=$deleteAll, isKeptGroup=$isKeptGroup): $targets" }
        return clusters
            .flatMap { it.groups }
            .filter { group -> targets.contains(group.identifier) }
            .map { group ->
                log(TAG, VERBOSE) { "__targetGroups(): Deleting from ${group.identifier} (dupes=${group.count})" }
                // A directly targeted single-member group is deleted whole - the user explicitly
                // picked it (e.g. a similar-image group in the details view). But a cluster's kept
                // group must never lose its sole member that way: it is the copy being kept, and
                // deleting it could remove the last remaining copy of the file.
                if (deleteAll || (group.duplicates.size == 1 && !isKeptGroup)) {
                    targetDuplicates(group.duplicates.map { it.identifier }.toSet())
                } else {
                    val keeperId = group.keeperIdentifier
                    val rest = if (keeperId != null) {
                        group.duplicates.filter { it.identifier != keeperId }
                    } else {
                        val (favorite, nonKeepers) = arbiter.decideDuplicates(group.duplicates, strategy)
                        log(TAG, VERBOSE) { "__targetGroups(): Our favorite is $favorite" }
                        nonKeepers
                    }
                    targetDuplicates(rest.map { it.identifier }.toSet())
                }
            }
            .flatten()
    }

    private suspend fun Deduplicator.Data.targetDuplicates(
        targets: Set<Duplicate.Id>
    ): Collection<Duplicate> {
        log(TAG, VERBOSE) { "#__targetDuplicates(): $targets" }
        return clusters
            .flatMap { it.groups }
            .flatMap { it.duplicates }
            .filter { targets.contains(it.identifier) }
            .onEach { dupe ->
                // targetDuplicates() get's direct targets, no need to make a specific selection
                log(TAG, VERBOSE) { "___targetDuplicates(): Deleting ${dupe.identifier}" }
                updateProgressSecondary(dupe.lookup.userReadablePath)
                dupe.path.delete(gatewaySwitch)
                (dupe.path as? LocalPath)?.let { mediaStoreTool.notifyDeleted(it) }
            }
    }

    data class Deleted(
        val success: Set<Duplicate> = emptySet(),
    )

    companion object {
        private val TAG = logTag("Deduplicator", "Deleter")
    }
}