package eu.darken.sdmse.swiper.core.scanner

import eu.darken.sdmse.common.coroutine.DispatcherProvider
import eu.darken.sdmse.common.debug.logging.Logging.Priority.*
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.APathGateway
import eu.darken.sdmse.common.files.APathLookup
import eu.darken.sdmse.common.files.FileType
import eu.darken.sdmse.common.files.GatewaySwitch
import eu.darken.sdmse.common.files.isFile
import eu.darken.sdmse.common.files.walk
import eu.darken.sdmse.common.flow.throttleLatest
import eu.darken.sdmse.common.progress.Progress
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.exclusion.core.ExclusionManager
import eu.darken.sdmse.exclusion.core.pathExclusions
import eu.darken.sdmse.exclusion.core.types.match
import eu.darken.sdmse.main.core.SDMTool
import eu.darken.sdmse.swiper.core.FileTypeFilter
import eu.darken.sdmse.swiper.core.SessionState
import eu.darken.sdmse.swiper.core.SortOrder
import eu.darken.sdmse.swiper.core.db.SwipeItemEntity
import eu.darken.sdmse.swiper.core.db.SwipeSessionEntity
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapMerge
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import java.time.Instant
import java.util.UUID
import javax.inject.Inject

class SwiperScanner @Inject constructor(
    private val exclusionManager: ExclusionManager,
    private val dispatcherProvider: DispatcherProvider,
    private val gatewaySwitch: GatewaySwitch,
) : 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)
    }

    data class Options(
        val paths: Set<APath>,
        val itemLimit: Int?,
        val fileTypeFilter: FileTypeFilter = FileTypeFilter.EMPTY,
        val sortOrder: SortOrder = SortOrder.DEFAULT,
    )

    data class Result(
        val session: SwipeSessionEntity,
        val items: List<SwipeItemEntity>,
        val lookups: List<APathLookup<*>>,
    )

    suspend fun scan(options: Options): Result {
        log(TAG) { "scan($options)" }

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

        val exclusions = exclusionManager.pathExclusions(SDMTool.Type.SWIPER)
        log(TAG) { "Swiper exclusions are: $exclusions" }

        val searchFlow = options.paths.asFlow()
            .flatMapMerge(2) { path ->
                val filter: suspend (APathLookup<*>) -> Boolean = filter@{ toCheck: APathLookup<*> ->
                    exclusions.none { it.match(toCheck) }
                }

                // A source path can vanish or become unreadable between picking and scanning (more
                // likely now that sources can be many individually-picked files). Skip a bad source
                // instead of failing the whole scan — guard both the eager lookup and the lazy walk.
                try {
                    val lookup = gatewaySwitch.lookup(path)
                    if (lookup.fileType == FileType.FILE) {
                        if (filter(lookup)) flowOf(lookup) else emptyFlow()
                    } else {
                        path.walk(
                            gatewaySwitch,
                            options = APathGateway.WalkOptions(
                                onFilter = filter,
                            ),
                        ).catch { e ->
                            if (e is CancellationException) throw e
                            log(TAG, WARN) { "Source walk failed, skipping remainder: $path\n${e.asLog()}" }
                        }
                    }
                } catch (e: CancellationException) {
                    throw e
                } catch (e: Exception) {
                    log(TAG, WARN) { "Source path no longer accessible, skipping: $path\n${e.asLog()}" }
                    emptyFlow()
                }
            }
            .flowOn(dispatcherProvider.IO)
            .buffer(1024)
            .filter { it.isFile }
            .let { flow ->
                if (options.fileTypeFilter.isEmpty) flow
                else flow.filter { lookup ->
                    val ext = lookup.name.substringAfterLast('.', "").lowercase()
                    options.fileTypeFilter.matchesExtension(ext)
                }
            }

        val files = mutableListOf<APathLookup<*>>()

        searchFlow.collect { lookup ->
            currentCoroutineContext().ensureActive()
            files.add(lookup)
            updateProgressSecondary(lookup.userReadablePath)
            updateProgressCount(Progress.Count.Counter(files.size))
            log(TAG, VERBOSE) { "Found file #${files.size}: ${lookup.path}" }
        }

        // Path-based tiebreaker keeps order stable across rescans
        // (flatMapMerge yields files in non-deterministic order)
        val comparator: Comparator<APathLookup<*>> = when (options.sortOrder) {
            SortOrder.OLDEST_FIRST -> compareBy({ it.modifiedAt }, { it.path })
            SortOrder.NEWEST_FIRST -> compareByDescending<APathLookup<*>> { it.modifiedAt }.thenBy { it.path }
            SortOrder.NAME_ASC -> compareBy({ it.name.lowercase() }, { it.path })
            SortOrder.SIZE_DESC -> compareByDescending<APathLookup<*>> { it.size }.thenBy { it.path }
        }
        files.sortWith(comparator)

        // Apply the free-tier item limit AFTER sorting so the user actually
        // gets the top-N according to the chosen order, not the first N walked.
        val finalFiles = options.itemLimit?.let { files.take(it) } ?: files

        log(TAG) { "Scan found ${files.size} files, keeping ${finalFiles.size}" }

        val sessionId = UUID.randomUUID().toString()
        val now = Instant.now()

        val session = SwipeSessionEntity(
            sessionId = sessionId,
            sourcePaths = options.paths.toList(),
            currentIndex = 0,
            totalItems = finalFiles.size,
            createdAt = now,
            lastModifiedAt = now,
            state = SessionState.READY,
        )

        val items = finalFiles.mapIndexed { index, lookup ->
            SwipeItemEntity(
                sessionId = sessionId,
                itemIndex = index,
                path = lookup.lookedUp,
            )
        }

        return Result(
            session = session,
            items = items,
            lookups = finalFiles,
        )
    }

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