package invalid.lena.rsend

import android.content.Context
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.concurrent.TimeUnit

// RsyncRunner builds and runs the rsync invocation for one folder, streaming
// rsync's output into the log. It uses the bundled rsync and rsh binaries and
// passes key, known_hosts, and port to rsh through the environment.
object RsyncRunner {

    // args builds the rsync argument vector for one folder. Flags are tuned for
    // media backup: recursive, preserve mtimes, resume partial files, and skip
    // the ownership and permission bits that mean nothing across Android and a
    // server.
    //
    // No --mkpath: rsync forwards it to the remote when sending, and remotes
    // older than 3.2.3 (notably stock macOS) reject it. rsync creates the
    // final component of the destination path on its own; deeper missing
    // parents need a one-time mkdir on the server (see README).
    fun args(rsh: String, remote: Remote, f: Folder): List<String> {
        val a = ArrayList<String>()
        a.add("-rt")
        a.add("--partial")
        // Abort rather than hang if the network stalls for 5 minutes.
        a.add("--timeout=300")
        a.add("--no-perms")
        a.add("--no-owner")
        a.add("--no-group")
        a.add("--omit-dir-times")
        a.add("-e")
        a.add(rsh)
        for (e in f.excludes) a.add("--exclude=$e")
        if (f.delete) a.add("--delete")
        a.add(withSlash(f.local))
        a.add("${remote.user}@${remote.host}:${withSlash(f.remote)}")
        return a
    }

    private fun withSlash(p: String): String = if (p.endsWith("/")) p else "$p/"

    // runFolder execs rsync for one folder, appending every output line to the
    // log, and returns rsync's exit code. Blocks the calling thread (run it on
    // an IO dispatcher). If the caller is cancelled (worker stopped, schedule
    // replaced, constraints lost) the watchdog kills rsync rather than leave it
    // running detached; killing it also closes its pipes, which unblocks the
    // log reader below. destroy() on an already-exited process is a no-op.
    suspend fun runFolder(ctx: Context, remote: Remote, f: Folder, log: SyncLog): Int = coroutineScope {
        val cmd = listOf(Native.rsync(ctx).absolutePath) + args(Native.rsh(ctx).absolutePath, remote, f)
        log.line("rsync ${cmd.drop(1).joinToString(" ")}")
        val pb = ProcessBuilder(cmd).redirectErrorStream(true)
        pb.environment().putAll(Keys.env(ctx, remote.port))
        val p = pb.start()
        val watchdog = launch(Dispatchers.IO) { try { awaitCancellation() } finally { terminate(p) } }
        try {
            p.inputStream.bufferedReader().forEachLine { log.line(it) }
            p.waitFor()
        } finally {
            watchdog.cancelAndJoin()
        }
    }

    private fun terminate(p: Process) {
        p.destroy()
        if (!p.waitFor(5, TimeUnit.SECONDS)) {
            p.destroyForcibly()
            p.waitFor()
        }
    }
}
