package de.usbserver.bridge

import android.content.Context
import android.util.Log
import org.json.JSONObject
import java.io.File

/**
 * Runs the usb-client binary as a child process.
 *
 * The binary ships in jniLibs as libusbclient.so. That naming is not
 * cosmetic: Android extracts and grants execute permission only to files in
 * the native library directory, so a binary placed in assets could not be run
 * at all on modern releases.
 */
class ClientProcess(private val context: Context) {

    private var process: Process? = null
    private var logThread: Thread? = null

    val socketPath: String get() = UsbBridge.defaultSocketPath(context)

    /** Writes the config the client reads on startup. */
    fun writeConfig(relayAddr: String, tokens: Triple<String, String, String>, name: String) {
        val config = JSONObject().apply {
            put("relay_addr", relayAddr)
            put("hash", hashOf(tokens))
            // The tokens themselves must be present, not just the hash:
            // without them the client cannot derive the tunnel key and falls
            // back to unencrypted, relay-only operation.
            put("token1", tokens.first)
            put("token2", tokens.second)
            put("token3", tokens.third)
            // Only share: Android kernels have no vhci-hcd, so this device
            // can offer its USB hardware but not receive anyone else's.
            put("mode", "share")
            put("name", name)
            put("bridge_socket", socketPath)
            // The web UI would be reachable by any app on the device.
            put("web_port", 0)
        }

        configFile().writeText(config.toString())
    }

    fun start(): Result<Unit> {
        if (process?.isAlive == true) {
            return Result.success(Unit)
        }

        val binary = File(context.applicationInfo.nativeLibraryDir, "libusbclient.so")
        if (!binary.exists()) {
            return Result.failure(
                IllegalStateException("libusbclient.so is missing; build it into jniLibs first")
            )
        }

        // A socket left over from a previous run would stop the client binding.
        File(socketPath).delete()

        return try {
            val started = ProcessBuilder(
                binary.absolutePath,
                "share",
                "--config", configFile().absolutePath,
                "--no-gui",
            )
                .redirectErrorStream(true)
                .start()

            process = started
            logThread = Thread { drainLog(started) }.apply {
                isDaemon = true
                start()
            }

            Log.i(TAG, "usb-client started")
            Result.success(Unit)
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    fun stop() {
        process?.destroy()
        process = null
        File(socketPath).delete()
    }

    val isRunning: Boolean get() = process?.isAlive == true

    /**
     * Forwards the client's output to logcat.
     *
     * Without this the process's diagnostics are simply lost, which makes any
     * failure — a wrong relay address, a rejected device — invisible.
     */
    private fun drainLog(process: Process) {
        try {
            process.inputStream.bufferedReader().forEachLine { line ->
                Log.i(TAG, line)
            }
        } catch (e: Exception) {
            Log.d(TAG, "log stream ended: ${e.message}")
        }
    }

    private fun configFile() = File(context.filesDir, "config.json")

    /** SHA-256 over the three tokens joined by colons — must match token.Hash. */
    private fun hashOf(tokens: Triple<String, String, String>): String {
        val combined = "${tokens.first}:${tokens.second}:${tokens.third}"
        val digest = java.security.MessageDigest.getInstance("SHA-256")
            .digest(combined.toByteArray())
        return digest.joinToString("") { "%02x".format(it) }
    }

    companion object {
        private const val TAG = "UsbClientProcess"
    }
}
