package eu.darken.sdmse.systemcleaner.core.filter.custom

import android.content.Context
import eu.darken.sdmse.common.datastore.value
import eu.darken.sdmse.common.debug.logging.Logging.Priority.ERROR
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.systemcleaner.core.SystemCleanerSettings
import eu.darken.sdmse.systemcleaner.core.filter.FilterIdentifier
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.json.Json
import java.io.File
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class CustomFilterRepo @Inject constructor(
    private val context: Context,
    private val json: Json,
    private val settings: SystemCleanerSettings,
    private val legacyImporter: LegacyFilterSupport,
) {

    private val lock = Mutex()
    private val filterDir by lazy {
        File(context.filesDir, "systemcleaner/customfilter2").apply {
            if (!exists() && mkdirs()) log(TAG) { "Created $this" }
        }
    }

    // FilterIdentifier is a plain String and restore feeds us identifiers from a user-supplied
    // archive — the containment check stops `../`-style identifiers from escaping the filter dir.
    private val FilterIdentifier.configPath: File
        get() = File(filterDir, "$this.json").also {
            require(it.canonicalFile.parentFile == filterDir.canonicalFile) {
                "Invalid filter identifier: $this"
            }
        }

    private val refreshTrigger = MutableStateFlow(UUID.randomUUID())

    val configs: Flow<Collection<CustomFilterConfig>> = refreshTrigger
        .flatMapLatest { _ ->
            flow {
                val configFiles = filterDir.listFiles()!!.let { dirContent ->
                    dirContent.filterNot { it.name.endsWith(".json") }.forEach {
                        log(TAG, WARN) { "Unexpected file: $it" }
                    }
                    dirContent.filter { it.name.endsWith(".json") }
                }
                emit(configFiles)
            }
        }
        .map { files ->
            lock.withLock {
                val configs = files.map { configPath ->
                    json.decodeFromString<CustomFilterConfig>(configPath.readText()).also {
                        log(TAG) { "Loaded config $configPath -> $it" }
                    }
                }

                val orphaned = settings.enabledCustomFilter.value().filter { id ->
                    configs.none { it.identifier == id }
                }
                orphaned.forEach {
                    log(TAG, WARN) { "Clearing orphaned filter ID: $it" }
                    settings.clearCustomFilter(it)
                }
                configs
            }
        }

    suspend fun refresh() {
        log(TAG, VERBOSE) { "refresh()" }
        refreshTrigger.value = UUID.randomUUID()
    }

    suspend fun save(configs: Set<CustomFilterConfig>) {
        log(TAG) { "save($configs)" }
        lock.withLock {
            configs.forEach { config ->
                val path = config.identifier.configPath
                path.writeText(json.encodeToString(config))
                log(TAG) { "Saved to $path" }
            }
        }
        refresh()
    }

    /**
     * Swaps the whole filter set in one locked critical section: the complete new set is written
     * first, stale config files are only removed after every write succeeded. A failure mid-way can
     * therefore never delete filters that weren't replaced — worst case is a superset (new files
     * written, stale ones still present), never a loss.
     */
    suspend fun replaceAll(configs: Set<CustomFilterConfig>) {
        log(TAG) { "replaceAll(${configs.size} configs)" }
        lock.withLock {
            val newIds = configs.map { it.identifier }.toSet()
            configs.forEach { config ->
                val path = config.identifier.configPath
                path.writeText(json.encodeToString(config))
                log(TAG) { "Saved to $path" }
            }
            filterDir.listFiles().orEmpty()
                .filter { it.name.endsWith(".json") }
                .map { it.name.removeSuffix(".json") }
                .filter { it !in newIds }
                .forEach { staleId ->
                    val path = staleId.configPath
                    if (!path.delete() && path.exists()) log(TAG, ERROR) { "Failed to delete $path" }
                    settings.clearCustomFilter(staleId)
                }
        }
        refresh()
    }

    suspend fun remove(ids: Set<FilterIdentifier>) {
        log(TAG) { "remove($ids)" }

        lock.withLock {
            ids.forEach { id ->
                val path = id.configPath
                if (!path.delete()) {
                    if (path.exists()) {
                        log(TAG, ERROR) { "Failed to delete $path" }
                    } else {
                        log(TAG, WARN) { "Config does not exist on disk: $path" }
                    }
                }
                settings.clearCustomFilter(id)
            }
        }

        refresh()
    }

    fun generateIdentifier() = UUID.randomUUID().toString()

    suspend fun importFilter(rawFilters: List<RawFilter>) {
        log(TAG) { "importFilter($rawFilters)" }
        val configs = rawFilters.map { rawFilter ->
            try {
                json.decodeFromString<CustomFilterConfig>(rawFilter.payload)
            } catch (ogError: Exception) {
                try {
                    legacyImporter.import(rawFilter.payload)!!
                } catch (_: Exception) {
                    log(TAG, ERROR) { "Failed to import $rawFilter: ${ogError.asLog()}" }
                    throw ogError
                }
            }
        }.toSet()
        save(configs)
    }

    suspend fun exportFilters(identifiers: Collection<FilterIdentifier>): Collection<RawFilter> {
        log(TAG) { "exportFilters($identifiers)" }
        val configs = currentConfigs().filter { identifiers.contains(it.identifier) }

        return configs.map {
            val rawJson = json.encodeToString(it)
            RawFilter("${it.label} - ${it.identifier.takeLast(10)}.json", rawJson)
        }
    }

    companion object {
        private val TAG = logTag("SystemCleaner", "CustomFilter", "Repo")
    }
}