package com.ledgrab.android

import android.util.Log
import java.io.BufferedReader
import java.io.InputStreamReader
import java.util.concurrent.TimeUnit

/**
 * Lightweight root-access utilities.
 *
 * Detection is two-phase:
 *   1. Cheap check for a `su` binary in common paths (no process spawn).
 *   2. On demand, a real `su -c id` execution that returns true only when
 *      the shell actually runs as UID 0. This triggers Magisk's grant
 *      dialog the first time, exactly like any other root request.
 *
 * The heavier check is cached after it succeeds so we don't ask Magisk
 * repeatedly mid-session.
 */
object Root {
    private const val TAG = "Root"

    private val SU_PATHS = listOf(
        "/system/bin/su",
        "/system/xbin/su",
        "/sbin/su",
        "/su/bin/su",
        "/debug_ramdisk/su",
        "/vendor/bin/su",
    )

    @Volatile private var cachedGranted: Boolean? = null

    /** Cheap probe: true if a known `su` binary exists. Doesn't run anything. */
    @JvmStatic
    fun looksRooted(): Boolean = SU_PATHS.any { java.io.File(it).exists() }

    /**
     * Actually exercise `su`. Triggers Magisk's grant dialog on first call.
     * Returns true if the shell reports UID 0 within the timeout; false
     * otherwise (no root, user denied, or timeout).
     */
    @JvmStatic
    fun requestGrant(timeoutSeconds: Long = 10): Boolean {
        cachedGranted?.let { return it }

        if (!looksRooted()) {
            cachedGranted = false
            return false
        }

        val granted = try {
            // redirectErrorStream merges stderr into stdout so a single
            // drain thread is enough — avoids the classic pipe-buffer
            // deadlock where waitFor() blocks because stderr filled up.
            val process = ProcessBuilder("su", "-c", "id")
                .redirectErrorStream(true)
                .start()
            val outputBuilder = StringBuilder()
            val drain = Thread({
                try {
                    BufferedReader(InputStreamReader(process.inputStream)).use { r ->
                        val buf = CharArray(512)
                        while (true) {
                            val n = r.read(buf)
                            if (n < 0) break
                            synchronized(outputBuilder) { outputBuilder.append(buf, 0, n) }
                        }
                    }
                } catch (_: Exception) {
                    // Process gone — drain ends.
                }
            }, "Root-su-drain").apply { isDaemon = true; start() }

            val finished = process.waitFor(timeoutSeconds, TimeUnit.SECONDS)
            if (!finished) {
                process.destroyForcibly()
                drain.join(500)
                Log.w(TAG, "su -c id timed out after ${timeoutSeconds}s")
                false
            } else {
                drain.join(500)
                val output = synchronized(outputBuilder) { outputBuilder.toString() }
                if (process.exitValue() != 0) {
                    Log.w(TAG, "su -c id exited with ${process.exitValue()} output='${output.trim()}'")
                    false
                } else {
                    val rooted = output.contains("uid=0")
                    Log.i(TAG, "su -c id → '${output.trim()}' → rooted=$rooted")
                    rooted
                }
            }
        } catch (e: Exception) {
            Log.w(TAG, "su invocation failed: ${e.message}")
            false
        }

        cachedGranted = granted
        return granted
    }

    /**
     * Run a command as root.
     *
     * The [argv] array is passed to `su -c` as **a single string** built by
     * shell-quoting each element. This prevents the shell-injection class
     * of bug where a caller passes user-influenced data containing
     * spaces, semicolons, or backticks: each element is treated as a
     * single shell token regardless of contents.
     *
     * Returns true on exit-zero. Failure invalidates the cached grant so
     * the next [requestGrant] re-checks (covers cases like Magisk grant
     * being revoked mid-session).
     */
    @JvmStatic
    @JvmOverloads
    fun runAsRoot(argv: Array<String>, timeoutSeconds: Long = 5): Boolean {
        require(argv.isNotEmpty()) { "runAsRoot called with empty argv" }
        val quoted = argv.joinToString(" ") { shellQuote(it) }
        return execSu(quoted, timeoutSeconds)
    }

    /**
     * Convenience for fully-trusted constant commands (e.g.
     * ``runAsRoot("pkill -TERM screenrecord")``). DO NOT pass anything
     * derived from user input through this overload — use [runAsRoot]
     * with an argv array instead so each token is quoted individually.
     */
    @JvmStatic
    @JvmOverloads
    fun runAsRoot(command: String, timeoutSeconds: Long = 5): Boolean {
        return execSu(command, timeoutSeconds)
    }

    private fun execSu(shellLine: String, timeoutSeconds: Long): Boolean {
        return try {
            val process = ProcessBuilder("su", "-c", shellLine)
                .redirectErrorStream(true)
                .start()
            val finished = process.waitFor(timeoutSeconds, TimeUnit.SECONDS)
            if (!finished) {
                process.destroyForcibly()
                cachedGranted = null
                false
            } else if (process.exitValue() != 0) {
                cachedGranted = null
                false
            } else {
                true
            }
        } catch (e: Exception) {
            Log.w(TAG, "runAsRoot('$shellLine') failed: ${e.message}")
            cachedGranted = null
            false
        }
    }

    /**
     * POSIX-shell-style single-quote escape. Wraps in single quotes and
     * escapes embedded single quotes as ``'\''`` so shell metacharacters
     * inside [s] are inert.
     */
    private fun shellQuote(s: String): String {
        if (s.isEmpty()) return "''"
        // Optimisation: if the string contains only safe characters,
        // skip the quoting overhead. The set is intentionally narrow —
        // notably `=` is excluded because an unquoted "FOO=bar" at the
        // start of a command would be parsed as a shell variable
        // assignment, not a literal arg. Quoting it forces literal use.
        if (s.all { it.isLetterOrDigit() || it in "_-./" }) return s
        val sb = StringBuilder(s.length + 2)
        sb.append('\'')
        for (ch in s) {
            if (ch == '\'') sb.append("'\\''") else sb.append(ch)
        }
        sb.append('\'')
        return sb.toString()
    }

    /** Forget the cached grant result — useful if Magisk permission was revoked. */
    @JvmStatic
    fun invalidateCache() {
        cachedGranted = null
    }
}
