package invalid.lena.rsend

import android.content.Context
import java.io.File

// Keys manages the on-device ed25519 identity and the pinned host keys. The
// private key is stored Keystore-encrypted (KeyVault) and only ever exists in
// plaintext in memory, passed to rsh through RSH_KEY_DATA.
object Keys {

    private fun keyEnc(ctx: Context): File = File(ctx.filesDir, "id_ed25519.enc")
    private fun publicKey(ctx: Context): File = File(ctx.filesDir, "id_ed25519.pub")

    fun knownHosts(ctx: Context): File = File(ctx.filesDir, "known_hosts")

    fun exists(ctx: Context): Boolean = keyEnc(ctx).exists()

    // generate creates the key pair, stores the private key encrypted, and
    // returns the public key in authorized_keys format.
    fun generate(ctx: Context): String {
        val tmp = File(ctx.cacheDir, "keygen").apply { mkdirs() }
        Native.run(Native.rsh(ctx), listOf("-keygen", tmp.absolutePath))
        keyEnc(ctx).writeBytes(KeyVault.encrypt(File(tmp, "id_ed25519").readBytes()))
        File(tmp, "id_ed25519.pub").copyTo(publicKey(ctx), overwrite = true)
        tmp.deleteRecursively()
        return publicKeyText(ctx)
    }

    // importKey stores a user-supplied private key, replacing any current one.
    // The bundled rsh derives and validates the public key (-pubkey uses the
    // same parser as the transport), so a key that imports here will also sync.
    // Throws IllegalArgumentException with rsh's message if the key is unusable
    // (wrong type, or passphrase-protected: rsend needs an unencrypted key).
    fun importKey(ctx: Context, pem: ByteArray) {
        val r = Native.run(Native.rsh(ctx), listOf("-pubkey"), mapOf("RSH_KEY_DATA" to String(pem)))
        if (r.code != 0) throw IllegalArgumentException(r.output.trim().ifEmpty { "invalid private key" })
        val pub = r.output.trim()
        keyEnc(ctx).writeBytes(KeyVault.encrypt(pem))
        publicKey(ctx).writeText(pub + "\n")
    }

    fun publicKeyText(ctx: Context): String =
        if (publicKey(ctx).exists()) publicKey(ctx).readText().trim() else ""

    // env returns the RSH_* environment rsh needs. The decrypted key is passed
    // by value so it never touches the filesystem.
    fun env(ctx: Context, port: Int): Map<String, String> = mapOf(
        "RSH_KEY_DATA" to String(KeyVault.decrypt(keyEnc(ctx).readBytes())),
        "RSH_KNOWN_HOSTS" to knownHosts(ctx).absolutePath,
        "RSH_PORT" to port.toString(),
    )

    data class Scan(val ok: Boolean, val fingerprint: String, val line: String, val error: String)

    // scan connects (proving the key is installed), captures the host key, and
    // returns its fingerprint and the known_hosts line to pin. pin() writes it
    // once the user accepts.
    fun scan(ctx: Context, remote: Remote): Scan {
        val r = Native.run(
            Native.rsh(ctx),
            listOf("-scan", "${remote.user}@${remote.host}"),
            env(ctx, remote.port),
        )
        if (r.code != 0) return Scan(false, "", "", r.output.trim())
        val lines = r.output.trim().lines()
        if (lines.size < 2) return Scan(false, "", "", "unexpected scan output:\n${r.output}")
        return Scan(true, lines[0], lines[1], "")
    }

    fun pin(ctx: Context, line: String) {
        knownHosts(ctx).writeText(line + "\n")
    }

    fun pinned(ctx: Context): Boolean = knownHosts(ctx).let { it.exists() && it.length() > 0 }
}
