package de.arxsilex.maptools.converter.desktop

import java.nio.file.Files
import java.nio.file.Path
import java.util.Comparator
import java.util.UUID

internal data class PortableRuntimeDirectories(
    val applicationDirectory: Path,
    val sessionTemporaryDirectory: Path,
    val removedLegacyBytes: Long,
)

internal data class PortableStorageStatus(
    val directory: Path,
    val usableBytes: Long,
    val requiredBytes: Long,
) {
    val hasEnoughSpace: Boolean get() = usableBytes >= requiredBytes
}

internal object PlatformDirectories {
    private const val APP_DIRECTORY_NAME = "MapTools Converter"
    private const val PREVIEW_FIXED_RESERVE_BYTES = 128L * 1024L * 1024L
    private const val CONVERSION_FIXED_RESERVE_BYTES = 512L * 1024L * 1024L
    private const val PREVIEW_SOURCE_MULTIPLIER = 2L
    private const val CONVERSION_SOURCE_MULTIPLIER = 6L

    @Volatile
    private var activeRuntime: PortableRuntimeDirectories? = null

    fun applicationDirectory(
        configuredRoot: String? = System.getProperty("maptools.portable.root"),
        jpackageApplicationPath: String? = System.getProperty("jpackage.app-path"),
        workingDirectory: String = System.getProperty("user.dir"),
    ): Path {
        configuredRoot?.takeIf(String::isNotBlank)?.let { return Path.of(it).toAbsolutePath().normalize() }
        jpackageApplicationPath?.takeIf(String::isNotBlank)?.let { launcher ->
            return Path.of(launcher).toAbsolutePath().normalize().parent
                ?: error("Der Programmordner konnte nicht bestimmt werden.")
        }
        return Path.of(workingDirectory).toAbsolutePath().normalize()
    }

    fun cacheDirectory(applicationDirectory: Path = applicationDirectory()): Path =
        applicationDirectory.toAbsolutePath().normalize().resolve("cache")

    fun conversionCacheDirectory(applicationDirectory: Path = applicationDirectory()): Path =
        cacheDirectory(applicationDirectory).resolve("conversion")

    fun previewCacheDirectory(applicationDirectory: Path = applicationDirectory()): Path =
        cacheDirectory(applicationDirectory).resolve("preview")

    fun logDirectory(applicationDirectory: Path = applicationDirectory()): Path =
        applicationDirectory.toAbsolutePath().normalize().resolve("logs")

    fun temporaryRootDirectory(applicationDirectory: Path = applicationDirectory()): Path =
        applicationDirectory.toAbsolutePath().normalize().resolve("temp")

    fun sessionTemporaryDirectory(): Path = requireNotNull(activeRuntime) {
        "Die portablen Laufzeitverzeichnisse wurden noch nicht vorbereitet."
    }.sessionTemporaryDirectory

    fun prepareRuntime(
        applicationDirectory: Path = applicationDirectory(),
        legacyDirectories: List<Path> = legacyDataDirectories(),
        processId: Long = ProcessHandle.current().pid(),
    ): PortableRuntimeDirectories {
        val root = applicationDirectory.toAbsolutePath().normalize()
        Files.createDirectories(root)
        require(Files.isWritable(root)) { "Der Programmordner ist nicht beschreibbar: $root" }
        Files.createDirectories(previewCacheDirectory(root))
        Files.createDirectories(conversionCacheDirectory(root))
        Files.createDirectories(logDirectory(root))
        cleanupTransientCacheEntries(root)
        cleanupStaleTemporaryDirectories(root)
        val temporaryRoot = temporaryRootDirectory(root)
        Files.createDirectories(temporaryRoot)
        val session = temporaryRoot.resolve("session-$processId-${UUID.randomUUID()}")
        Files.createDirectories(session)
        val removedLegacyBytes = legacyDirectories.sumOf { legacy -> deleteLegacyDirectory(legacy, root) }
        return PortableRuntimeDirectories(root, session, removedLegacyBytes).also { activeRuntime = it }
    }

    fun cleanupRuntime(runtime: PortableRuntimeDirectories? = activeRuntime) {
        val selected = runtime ?: return
        val temporaryRoot = temporaryRootDirectory(selected.applicationDirectory)
        val session = selected.sessionTemporaryDirectory.toAbsolutePath().normalize()
        if (session.parent == temporaryRoot && session.fileName.toString().startsWith("session-")) {
            deleteTree(session)
        }
        deleteDirectoryIfEmpty(temporaryRoot)
        if (activeRuntime == selected) activeRuntime = null
    }

    fun previewStorageStatus(
        sourceBytes: Long,
        applicationDirectory: Path = applicationDirectory(),
        usableBytes: Long = usableSpace(applicationDirectory),
    ): PortableStorageStatus = PortableStorageStatus(
        directory = applicationDirectory.toAbsolutePath().normalize(),
        usableBytes = usableBytes,
        requiredBytes = saturatedAdd(
            saturatedMultiply(sourceBytes.coerceAtLeast(0L), PREVIEW_SOURCE_MULTIPLIER),
            PREVIEW_FIXED_RESERVE_BYTES,
        ),
    )

    fun conversionStorageStatus(
        sourceBytes: Long,
        applicationDirectory: Path = applicationDirectory(),
        usableBytes: Long = usableSpace(applicationDirectory),
    ): PortableStorageStatus = PortableStorageStatus(
        directory = applicationDirectory.toAbsolutePath().normalize(),
        usableBytes = usableBytes,
        requiredBytes = saturatedAdd(
            saturatedMultiply(sourceBytes.coerceAtLeast(0L), CONVERSION_SOURCE_MULTIPLIER),
            CONVERSION_FIXED_RESERVE_BYTES,
        ),
    )

    fun legacyDataDirectories(
        osName: String = System.getProperty("os.name"),
        userHome: String = System.getProperty("user.home"),
        environment: Map<String, String> = System.getenv(),
    ): List<Path> = when {
        osName.startsWith("Windows", ignoreCase = true) -> {
            val localAppData = environment["LOCALAPPDATA"]?.takeIf(String::isNotBlank)?.let(Path::of)
                ?: Path.of(userHome, "AppData", "Local")
            listOf(localAppData.resolve("ArxSilex").resolve(APP_DIRECTORY_NAME))
        }
        osName.startsWith("Mac", ignoreCase = true) -> listOf(
            Path.of(userHome, "Library", "Caches", "ArxSilex", APP_DIRECTORY_NAME),
            Path.of(userHome, "Library", "Logs", "ArxSilex", APP_DIRECTORY_NAME),
        )
        else -> listOf(
            (environment["XDG_CACHE_HOME"]?.takeIf(String::isNotBlank)?.let(Path::of)
                ?: Path.of(userHome, ".cache")).resolve("arxsilex").resolve("maptools-converter"),
            (environment["XDG_STATE_HOME"]?.takeIf(String::isNotBlank)?.let(Path::of)
                ?: Path.of(userHome, ".local", "state")).resolve("arxsilex").resolve("maptools-converter"),
        )
    }

    private fun usableSpace(directory: Path): Long = runCatching {
        Files.createDirectories(directory)
        Files.getFileStore(directory).usableSpace
    }.getOrDefault(0L)

    private fun cleanupStaleTemporaryDirectories(applicationDirectory: Path) {
        val temporaryRoot = temporaryRootDirectory(applicationDirectory)
        if (!Files.isDirectory(temporaryRoot)) return
        Files.list(temporaryRoot).use { entries ->
            entries.filter(Files::isDirectory).forEach { directory ->
                val name = directory.fileName.toString()
                val processId = name.removePrefix("session-").substringBefore('-').toLongOrNull()
                val running = processId?.let { id ->
                    ProcessHandle.of(id).map(ProcessHandle::isAlive).orElse(false)
                } ?: false
                if (name.startsWith("session-") && !running) deleteTree(directory)
            }
        }
    }

    private fun cleanupTransientCacheEntries(applicationDirectory: Path) {
        val conversionCache = conversionCacheDirectory(applicationDirectory)
        if (!Files.isDirectory(conversionCache)) return
        Files.list(conversionCache).use { entries ->
            entries.forEach { entry ->
                val name = entry.fileName.toString()
                if (
                    name.startsWith("selection-") || name.startsWith(".selection-") ||
                    name.contains(".partial-")
                ) {
                    deleteTree(entry)
                }
            }
        }
    }

    private fun deleteLegacyDirectory(directory: Path, applicationDirectory: Path): Long {
        val target = directory.toAbsolutePath().normalize()
        if (
            !Files.exists(target) || target == applicationDirectory ||
            target.startsWith(applicationDirectory) || applicationDirectory.startsWith(target)
        ) return 0L
        if (target.fileName?.toString() != APP_DIRECTORY_NAME && target.fileName?.toString() != "maptools-converter") {
            return 0L
        }
        val bytes = Files.walk(target).use { paths ->
            paths.filter(Files::isRegularFile)
                .mapToLong { path -> runCatching { Files.size(path) }.getOrDefault(0L) }
                .sum()
        }
        deleteTreeStrict(target)
        deleteDirectoryIfEmpty(target.parent)
        return bytes
    }

    private fun deleteTreeStrict(path: Path) {
        val target = path.toAbsolutePath().normalize()
        if (!Files.exists(target)) return
        Files.walk(target).use { paths ->
            paths.sorted(Comparator.reverseOrder()).forEach(Files::deleteIfExists)
        }
    }

    private fun deleteTree(path: Path) {
        val target = path.toAbsolutePath().normalize()
        if (!Files.exists(target)) return
        Files.walk(target).use { paths ->
            paths.sorted(Comparator.reverseOrder()).forEach { child ->
                runCatching { Files.deleteIfExists(child) }
            }
        }
    }

    private fun deleteDirectoryIfEmpty(directory: Path?) {
        if (directory == null || !Files.isDirectory(directory)) return
        Files.list(directory).use { entries ->
            if (!entries.findAny().isPresent) runCatching { Files.deleteIfExists(directory) }
        }
    }

    private fun saturatedMultiply(value: Long, multiplier: Long): Long =
        if (value > Long.MAX_VALUE / multiplier) Long.MAX_VALUE else value * multiplier

    private fun saturatedAdd(first: Long, second: Long): Long =
        if (first > Long.MAX_VALUE - second) Long.MAX_VALUE else first + second
}
