// SPDX-FileCopyrightText: 2026 Reactorcoremeltdown
// SPDX-License-Identifier: GPL-3.0-or-later

package space.rcmd.android.sizzle.io

import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.File
import java.io.InputStream
import java.io.OutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream

/**
 * Whole-app backup: a single ZIP of everything the app persists under its private
 * files dir — saved songs, themes, instrument/effect presets (with their sample
 * WAVs), the autosaved project, and the DataStore blobs holding settings + input
 * bindings. Restore extracts into a staging folder first and only swaps it into
 * place once the archive looks valid, so a corrupt or wrong file can never destroy
 * the current data. Because the DataStore prefs are cached in memory while the app
 * runs, a restore only takes full effect after the app is restarted.
 */
object BackupIo {
    // Top-level content areas under filesDir. "datastore" holds the settings +
    // bindings Preferences files (see SettingsStore / BindingStore).
    private val CONTENT_DIRS = listOf("songs", "themes", "presets", "datastore")
    private const val AUTOSAVE = "autosave.sng"
    private const val STAGING = ".restore_tmp"

    /** Write a full backup of [filesDir] to [out] as a ZIP. */
    fun write(filesDir: File, out: OutputStream) {
        ZipOutputStream(BufferedOutputStream(out)).use { zip ->
            for (dir in CONTENT_DIRS) addTree(zip, File(filesDir, dir), dir)
            File(filesDir, AUTOSAVE).takeIf { it.isFile }?.let { addFile(zip, it, AUTOSAVE) }
        }
    }

    /**
     * Restore a backup ZIP from [input] into [filesDir], replacing the current data.
     * Returns false (leaving current data untouched) if the archive is empty or holds
     * none of the expected areas. Settings/bindings apply after an app restart.
     */
    fun read(filesDir: File, input: InputStream): Boolean {
        val staging = File(filesDir, STAGING)
        staging.deleteRecursively(); staging.mkdirs()
        val stagingRoot = staging.canonicalPath + File.separator

        var any = false
        try {
            ZipInputStream(BufferedInputStream(input)).use { zin ->
                var entry: ZipEntry? = zin.nextEntry
                while (entry != null) {
                    val name = entry.name
                    if (!entry.isDirectory && !name.contains("..")) {
                        val target = File(staging, name)
                        // Zip-slip guard: the resolved path must stay inside staging.
                        if (target.canonicalPath.startsWith(stagingRoot)) {
                            target.parentFile?.mkdirs()
                            target.outputStream().use { zin.copyTo(it) }
                            any = true
                        }
                    }
                    zin.closeEntry()
                    entry = zin.nextEntry
                }
            }
        } catch (e: Exception) {
            // A truncated / non-ZIP file: bail out without touching the live data.
            staging.deleteRecursively(); return false
        }

        val looksValid = any &&
            (CONTENT_DIRS.any { File(staging, it).exists() } || File(staging, AUTOSAVE).isFile)
        if (!looksValid) { staging.deleteRecursively(); return false }

        // Swap each restored area into place (delete the live one first so a rename
        // over it succeeds; fall back to a copy if rename can't cross the boundary).
        for (dir in CONTENT_DIRS) {
            val src = File(staging, dir)
            if (!src.exists()) continue
            val dst = File(filesDir, dir)
            dst.deleteRecursively()
            if (!src.renameTo(dst)) src.copyRecursively(dst, overwrite = true)
        }
        File(staging, AUTOSAVE).takeIf { it.isFile }?.let { src ->
            val dst = File(filesDir, AUTOSAVE)
            dst.delete()
            if (!src.renameTo(dst)) src.copyTo(dst, overwrite = true)
        }
        staging.deleteRecursively()
        return true
    }

    private fun addTree(zip: ZipOutputStream, dir: File, prefix: String) {
        if (!dir.isDirectory) return
        dir.walkTopDown().filter { it.isFile }.forEach { f ->
            addFile(zip, f, prefix + "/" + f.relativeTo(dir).path.replace(File.separatorChar, '/'))
        }
    }

    private fun addFile(zip: ZipOutputStream, f: File, entryName: String) {
        zip.putNextEntry(ZipEntry(entryName))
        f.inputStream().use { it.copyTo(zip) }
        zip.closeEntry()
    }
}
