package invalid.lena.rsend

import android.os.Bundle
import android.util.Base64
import android.view.View
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import java.security.MessageDigest
import kotlin.concurrent.thread

// RemoteActivity adds or edits one named remote and runs Test connection,
// which scans the host key and stores the pin on this remote when the user
// accepts it. index < 0 means a new remote.
class RemoteActivity : AppCompatActivity() {

    private var index: Int = -1
    private lateinit var name: EditText
    private lateinit var host: EditText
    private lateinit var port: EditText
    private lateinit var user: EditText
    private lateinit var status: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_remote)
        setSupportActionBar(findViewById(R.id.toolbar))
        title = "Remote"
        fitSystemBars()
        name = findViewById(R.id.name)
        host = findViewById(R.id.host)
        port = findViewById(R.id.port)
        user = findViewById(R.id.user)
        status = findViewById(R.id.status)

        index = savedInstanceState?.getInt("index") ?: intent.getIntExtra("index", -1)
        val cfg = Config.load(this)
        if (index in cfg.remotes.indices) {
            val r = cfg.remotes[index]
            name.setText(r.name)
            host.setText(r.host)
            port.setText(r.port.toString())
            user.setText(r.user)
        } else {
            port.setText("22")
        }

        findViewById<Button>(R.id.save).setOnClickListener { if (save() >= 0) finish() }
        findViewById<Button>(R.id.test).setOnClickListener { test() }
        val del = findViewById<Button>(R.id.removeRemote)
        del.visibility = if (index < 0) View.GONE else View.VISIBLE
        del.setOnClickListener { if (remove()) finish() }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        outState.putInt("index", index)
        super.onSaveInstanceState(outState)
    }

    private fun current(): Remote? {
        val n = name.text.toString().trim()
        if (!RemoteRules.nameAllowed(n)) {
            status.text = "Set a name; folders pick their remote by it."
            return null
        }
        val p = port.text.toString().toIntOrNull()
        if (p == null || p !in 1..65535) {
            status.text = "Port must be between 1 and 65535."
            return null
        }
        // Unbracket before validating, or a host of "[]" passes the emptiness
        // check and is then stored empty. An IPv6 literal is kept bare: rsh and
        // rsync each add the brackets their own syntax needs, and a stored
        // bracketed form would be bracketed twice.
        val h = host.text.toString().trim().removeSurrounding("[", "]").trim()
        val u = user.text.toString().trim()
        // Checked here rather than only in test(), so Save and Test reject the
        // same incomplete form.
        if (!RemoteRules.hostAllowed(h)) {
            status.text = "Host must be a hostname or IP address without brackets."
            return null
        }
        if (!RemoteRules.userAllowed(u)) {
            status.text = "User may contain letters, digits, dot, underscore, and hyphen."
            return null
        }
        return Remote(name = n, host = h, port = p, user = u)
    }

    // save validates, takes the pin the caller just accepted or carries the
    // stored one over while host and port are unchanged, renames folder
    // references along with the remote, and persists. Returns the saved entry's
    // index (a new remote is appended), or -1 on a validation failure.
    private fun save(remote: Remote? = current()): Int {
        if (remote == null) return -1
        val cfg = Config.load(this)
        if (cfg.remotes.withIndex().any { (i, r) -> i != index && r.name == remote.name }) {
            status.text = "A remote named \"${remote.name}\" already exists."
            return -1
        }
        val old = cfg.remotes.getOrNull(index)
        val hostKey = when {
            remote.hostKey.isNotEmpty() -> remote.hostKey
            old != null && old.host == remote.host && old.port == remote.port -> old.hostKey
            else -> ""
        }
        val entry = remote.copy(hostKey = hostKey)
        val remotes = cfg.remotes.toMutableList()
        val at = if (index in remotes.indices) {
            remotes[index] = entry
            index
        } else {
            remotes.add(entry)
            remotes.size - 1
        }
        var folders = cfg.folders
        if (old != null && old.name != entry.name) {
            folders = folders.map {
                if (it.remoteName == old.name) it.copy(remoteName = entry.name) else it
            }
        }
        val next = cfg.copy(remotes = remotes, folders = folders)
        val conflict = FolderRules.firstDestinationConflict(next)
        if (conflict != null) {
            val (a, b) = conflict
            status.text =
                "This endpoint would make folder \"${a.name.ifEmpty { a.remotePath }}\" conflict with " +
                    "folder \"${b.name.ifEmpty { b.remotePath }}\"."
            return -1
        }
        Config.save(this, next)
        Scheduler.apply(this)
        return at
    }

    // remove refuses while folders still reference this remote: a dangling
    // name would need special-casing on every other screen.
    private fun remove(): Boolean {
        val cfg = Config.load(this)
        val r = cfg.remotes.getOrNull(index) ?: return true
        val used = cfg.folders.count { it.remoteName == r.name }
        if (used > 0) {
            status.text = "Cannot remove: $used folder(s) sync to this remote."
            return false
        }
        Config.save(this, cfg.copy(remotes = cfg.remotes.filterIndexed { i, _ -> i != index }))
        Scheduler.apply(this)
        return true
    }

    // test scans the form's remote without writing anything. Persisting first
    // would let a failed or cancelled test clear the pin of a working remote
    // the user never saved, which also silently disarms the periodic job.
    private fun test() {
        val r = current() ?: return
        val cfg = Config.load(this)
        if (cfg.remotes.withIndex().any { (i, existing) -> i != index && existing.name == r.name }) {
            status.text = "A remote named \"${r.name}\" already exists."
            return
        }
        if (!Keys.exists(this)) {
            status.text = "No key yet. Generate one on the main screen and add it to the server first."
            return
        }
        // Hand the scan the pin this remote already has, so rsh offers the
        // pinned key type and an untouched server reproduces its stored line.
        // Scanning the bare form instead would negotiate whatever the server
        // prefers and report a key change that never happened.
        val pinned = storedPin(r)
        status.text = "Connecting to ${r.user}@${r.host}:${r.port} ..."
        thread {
            val scan = try {
                Keys.scan(this, r.copy(hostKey = pinned))
            } catch (e: Exception) {
                Keys.Scan(false, "", "", e.message ?: "connection failed")
            }
            runOnUiThread {
                if (isFinishing || isDestroyed) return@runOnUiThread
                if (!scan.ok) {
                    status.text = "Connection failed:\n${scan.error}"
                } else {
                    confirmPin(r, pinned, scan)
                }
            }
        }
    }

    // storedPin is the pin already saved for this remote, and only while host
    // and port still match the form: a pin belongs to one host and port.
    private fun storedPin(r: Remote): String {
        val old = Config.load(this).remotes.getOrNull(index) ?: return ""
        return if (old.host == r.host && old.port == r.port) old.hostKey else ""
    }

    // confirmPin is the one security decision the user makes, so a key that
    // differs from the stored pin must not look like a first-time pin.
    private fun confirmPin(r: Remote, pinned: String, scan: Keys.Scan) {
        if (pinned == scan.line) {
            status.text = "Connection OK. Host key unchanged."
            return
        }
        val b = AlertDialog.Builder(this).setNegativeButton("Cancel", null)
        if (pinned.isEmpty()) {
            b.setTitle("Verify host key")
                .setMessage(
                        "Fingerprint:\n${scan.fingerprint}\n\n" +
                        "Check it on the server with:\n" +
                        "ssh-keygen -lf ${hostKeyFile(scan.fingerprint)}\n\nPin this host?"
                )
                .setPositiveButton("Pin") { _, _ -> savePinned(r, scan.line) }
        } else {
            b.setTitle("HOST KEY CHANGED")
                .setMessage(
                    "This host presents a different key from the one rsend pinned.\n\n" +
                        "Pinned:\n${fingerprintOf(pinned)}\n\n" +
                        "Presented:\n${scan.fingerprint}\n\n" +
                        "This is what an intercepted connection looks like. Only replace " +
                        "the pin if you rebuilt the server or rotated its host key."
                )
                .setPositiveButton("Replace pin") { _, _ -> savePinned(r, scan.line) }
        }
        b.show()
    }

    // savePinned writes the form and the accepted key together: until the user
    // accepts a key, nothing about this remote is persisted.
    private fun savePinned(r: Remote, line: String) {
        val at = save(r.copy(hostKey = line))
        if (at < 0) return
        index = at
        intent.putExtra("index", at)
        status.text = "Host key pinned. Connection OK."
    }

    private fun hostKeyFile(fingerprint: String): String = when (fingerprint.substringBefore(' ')) {
        "ssh-ed25519" -> "/etc/ssh/ssh_host_ed25519_key.pub"
        "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp521" ->
            "/etc/ssh/ssh_host_ecdsa_key.pub"
        "ssh-rsa", "rsa-sha2-256", "rsa-sha2-512" -> "/etc/ssh/ssh_host_rsa_key.pub"
        else -> "/etc/ssh/ssh_host_key.pub"
    }

    // fingerprintOf renders a stored known_hosts line the way rsh -scan and
    // ssh-keygen -lf do, so the two fingerprints in the warning are comparable.
    private fun fingerprintOf(line: String): String {
        val parts = line.trim().split(" ")
        if (parts.size < 3) return "(unreadable pin)"
        return try {
            val raw = Base64.decode(parts[2], Base64.DEFAULT)
            val sha = MessageDigest.getInstance("SHA-256").digest(raw)
            parts[1] + " SHA256:" + Base64.encodeToString(sha, Base64.NO_PADDING or Base64.NO_WRAP)
        } catch (e: Exception) {
            "(unreadable pin)"
        }
    }
}
