package invalid.lena.rsend

import android.Manifest
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.os.PowerManager
import android.provider.Settings
import android.view.View
import android.widget.Button
import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkManager
import java.io.ByteArrayOutputStream
import java.util.concurrent.TimeUnit
import kotlin.concurrent.thread

// MainActivity is the dashboard: a status hero, a primary Sync action, grouped
// Setup and Permissions rows, and the list of folders. Editing happens in
// RemoteActivity, ScheduleActivity, and FolderEditActivity.
class MainActivity : AppCompatActivity() {

    private lateinit var lastSync: TextView
    private lateinit var remoteValue: TextView
    private lateinit var keyValue: TextView
    private lateinit var scheduleValue: TextView
    private lateinit var dotAllFiles: View
    private lateinit var valAllFiles: TextView
    private lateinit var dotNotif: View
    private lateinit var valNotif: TextView
    private lateinit var dotBattery: View
    private lateinit var valBattery: TextView
    private lateinit var folders: LinearLayout
    private lateinit var btnSync: Button
    private lateinit var syncBar: ProgressBar
    private lateinit var syncStatus: TextView

    private val notifPerm =
        registerForActivityResult(ActivityResultContracts.RequestPermission()) { refresh() }

    private val openKeyDoc =
        registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
            if (uri != null) importKeyFrom(uri)
        }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        setSupportActionBar(findViewById(R.id.toolbar))
        fitSystemBars()
        lastSync = findViewById(R.id.lastSync)
        remoteValue = findViewById(R.id.remoteValue)
        keyValue = findViewById(R.id.keyValue)
        scheduleValue = findViewById(R.id.scheduleValue)
        dotAllFiles = findViewById(R.id.dotAllFiles)
        valAllFiles = findViewById(R.id.valAllFiles)
        dotNotif = findViewById(R.id.dotNotif)
        valNotif = findViewById(R.id.valNotif)
        dotBattery = findViewById(R.id.dotBattery)
        valBattery = findViewById(R.id.valBattery)
        folders = findViewById(R.id.folders)
        btnSync = findViewById(R.id.btnSync)
        syncBar = findViewById(R.id.syncBar)
        syncStatus = findViewById(R.id.syncStatus)

        findViewById<TextView>(R.id.appVersion).text = "v${BuildConfig.VERSION_NAME}"

        btnSync.setOnClickListener { syncNow() }
        observeSync()
        findViewById<LinearLayout>(R.id.rowRemote).setOnClickListener {
            startActivity(Intent(this, RemoteActivity::class.java))
        }
        findViewById<LinearLayout>(R.id.rowKey).setOnClickListener { showKey() }
        findViewById<LinearLayout>(R.id.rowSchedule).setOnClickListener {
            startActivity(Intent(this, ScheduleActivity::class.java))
        }
        findViewById<LinearLayout>(R.id.rowAllFiles).setOnClickListener {
            startActivity(
                Intent(
                    Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,
                    Uri.parse("package:$packageName"),
                )
            )
        }
        findViewById<LinearLayout>(R.id.rowNotif).setOnClickListener {
            if (Build.VERSION.SDK_INT >= 33 && !notif()) {
                notifPerm.launch(Manifest.permission.POST_NOTIFICATIONS)
            } else {
                // No runtime permission to request (pre-13) or already granted:
                // open the app's notification settings so the row still acts.
                startActivity(
                    Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
                        .putExtra(Settings.EXTRA_APP_PACKAGE, packageName),
                )
            }
        }
        findViewById<LinearLayout>(R.id.rowBattery).setOnClickListener {
            if (batteryUnrestricted()) {
                startActivity(Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS))
            } else {
                startActivity(
                    Intent(
                        Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
                        Uri.parse("package:$packageName"),
                    ),
                )
            }
        }
        findViewById<Button>(R.id.btnAddFolder).setOnClickListener {
            startActivity(Intent(this, FolderEditActivity::class.java).putExtra("index", -1))
        }
        findViewById<Button>(R.id.btnLog).setOnClickListener {
            startActivity(Intent(this, LogActivity::class.java))
        }
    }

    private fun syncNow() {
        val cfg = Config.load(this)
        if (cfg.remote.host.isEmpty() || cfg.remote.user.isEmpty() || cfg.folders.isEmpty() ||
            !Keys.exists(this) || !Keys.pinned(this)) {
            Toast.makeText(this, "Set the remote, key, host pin, and at least one folder first.", Toast.LENGTH_LONG)
                .show()
            return
        }
        val req = OneTimeWorkRequestBuilder<SyncWorker>().build()
        WorkManager.getInstance(this).enqueueUniqueWork("sync-now", ExistingWorkPolicy.KEEP, req)
        Toast.makeText(this, "Sync started.", Toast.LENGTH_SHORT).show()
    }

    // observeSync follows the manual sync's WorkInfo so the dashboard updates
    // live: an indeterminate bar and the current folder while it runs, and a
    // fresh status (last sync) the moment it ends, without leaving the screen.
    private fun observeSync() {
        WorkManager.getInstance(this)
            .getWorkInfosForUniqueWorkLiveData("sync-now")
            .observe(this) { infos ->
                val info = infos.lastOrNull()
                val running = info != null &&
                    (info.state == WorkInfo.State.RUNNING || info.state == WorkInfo.State.ENQUEUED)
                if (running) {
                    syncBar.visibility = View.VISIBLE
                    syncStatus.visibility = View.VISIBLE
                    btnSync.isEnabled = false
                    btnSync.text = "Syncing..."
                    val p = info!!.progress
                    val folder = p.getString(SyncWorker.KEY_FOLDER)
                    val i = p.getInt(SyncWorker.KEY_INDEX, 0)
                    val n = p.getInt(SyncWorker.KEY_TOTAL, 0)
                    syncStatus.text =
                        if (folder != null && n > 0) "Syncing $folder ($i/$n)" else "Starting sync..."
                } else {
                    syncBar.visibility = View.GONE
                    syncStatus.visibility = View.GONE
                    btnSync.isEnabled = true
                    btnSync.text = "Sync now"
                    refresh()
                }
            }
    }

    override fun onResume() {
        super.onResume()
        Scheduler.apply(this)
        refresh()
    }

    private fun allFiles() = Environment.isExternalStorageManager()

    private fun notif() = Build.VERSION.SDK_INT < 33 ||
        checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED

    private fun batteryUnrestricted(): Boolean =
        getSystemService(PowerManager::class.java).isIgnoringBatteryOptimizations(packageName)

    // jobState reports the WorkManager state of the scheduled sync (ENQUEUED,
    // RUNNING, none, ...) so the dashboard shows whether the scheduler is armed.
    private fun jobState(): String = try {
        WorkManager.getInstance(this)
            .getWorkInfosForUniqueWork(Scheduler.NAME)
            .get(500, TimeUnit.MILLISECONDS)
            .firstOrNull()?.state?.name ?: "none"
    } catch (e: Exception) {
        "?"
    }

    private fun refresh() {
        val cfg = Config.load(this)

        lastSync.text = "Last sync: ${LastSync.get(this)}"

        val r = cfg.remote
        remoteValue.text = if (r.host.isEmpty()) {
            "Not set"
        } else {
            "${r.user}@${r.host}:${r.port}, ${if (Keys.pinned(this)) "pinned" else "not pinned"}"
        }

        keyValue.text = if (Keys.exists(this)) "Generated, tap to view" else "Not created, tap to create"

        val s = cfg.schedule
        val sched = if (s.enabled) {
            "Every ${s.intervalMinutes}m" +
                (if (s.wifiOnly) ", unmetered" else "") +
                (if (s.requireCharging) ", charging" else "")
        } else {
            "Off"
        }
        scheduleValue.text = "$sched, job ${jobState()}"

        setPerm(dotAllFiles, valAllFiles, allFiles(), "Granted", "Tap to grant")
        setPerm(dotNotif, valNotif, notif(), "Granted", "Tap to grant")
        setPerm(dotBattery, valBattery, batteryUnrestricted(), "Unrestricted", "Optimized, tap to fix")

        folders.removeAllViews()
        if (cfg.folders.isEmpty()) {
            folders.addView(TextView(this).apply {
                text = "No folders yet. Tap Add folder below."
                setTextAppearance(R.style.TextAppearance_Rsend_Caption)
                setPadding(0, 0, 0, resources.getDimensionPixelSize(R.dimen.space_m))
            })
        } else {
            cfg.folders.forEachIndexed { i, f ->
                val row = layoutInflater.inflate(R.layout.item_folder, folders, false)
                row.findViewById<TextView>(R.id.folderName).text = f.name.ifEmpty { f.local }
                row.findViewById<TextView>(R.id.folderPath).text = "${f.local} -> ${f.remote}"
                row.findViewById<TextView>(R.id.folderChip).text = if (f.delete) "mirror" else "add"
                row.setOnClickListener {
                    startActivity(
                        Intent(this@MainActivity, FolderEditActivity::class.java)
                            .putExtra("index", i)
                    )
                }
                folders.addView(row)
            }
        }
    }

    private fun setPerm(dot: View, value: TextView, ok: Boolean, okText: String, badText: String) {
        dot.setBackgroundResource(if (ok) R.drawable.dot_on else R.drawable.dot_off)
        value.text = if (ok) okText else badText
    }

    // showKey displays the public key (only the key) and offers to copy it,
    // generate a fresh key, or import the user's own private key.
    private fun showKey() {
        val view = layoutInflater.inflate(R.layout.dialog_key, null)
        val keyText = view.findViewById<TextView>(R.id.keyText)
        val pub = Keys.publicKeyText(this)
        keyText.text = pub.ifEmpty { "No key yet. Generate or import one below." }

        val dialog = AlertDialog.Builder(this)
            .setTitle("Identity key")
            .setView(view)
            .setNegativeButton("Close", null)
            .create()

        val copy = view.findViewById<Button>(R.id.btnCopyKey)
        copy.isEnabled = pub.isNotEmpty()
        copy.setOnClickListener {
            val cm = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
            cm.setPrimaryClip(ClipData.newPlainText("rsend pubkey", pub))
            Toast.makeText(this, "Copied", Toast.LENGTH_SHORT).show()
        }
        view.findViewById<Button>(R.id.btnGenKey).setOnClickListener {
            dialog.dismiss()
            confirmGenerate()
        }
        view.findViewById<Button>(R.id.btnImportKey).setOnClickListener {
            dialog.dismiss()
            openKeyDoc.launch(arrayOf("*/*"))
        }
        dialog.show()
    }

    // confirmGenerate warns before replacing an existing key, then generates a
    // new one off the main thread and shows it so the user can copy it.
    private fun confirmGenerate() {
        val msg = if (Keys.exists(this)) {
            "Replace the current key with a new one? You must add the new public key to the server's authorized_keys."
        } else {
            "Generate a new identity key?"
        }
        AlertDialog.Builder(this)
            .setTitle("Generate new key")
            .setMessage(msg)
            .setPositiveButton("Generate") { _, _ ->
                thread {
                    val error = try {
                        Keys.generate(this)
                        null
                    } catch (e: Exception) {
                        e.message ?: "keygen failed"
                    }
                    runOnUiThread {
                        if (isFinishing || isDestroyed) return@runOnUiThread
                        if (error == null) {
                            Scheduler.apply(this)
                            refresh()
                            Toast.makeText(this, "New key generated.", Toast.LENGTH_SHORT).show()
                            showKey()
                        } else {
                            AlertDialog.Builder(this)
                                .setTitle("Keygen failed")
                                .setMessage(error)
                                .setPositiveButton("OK", null)
                                .show()
                        }
                    }
                }
            }
            .setNegativeButton("Cancel", null)
            .show()
    }

    // importKeyFrom reads the chosen private-key file and stores it, replacing
    // the current key. rsh validates the key first; an unusable key (wrong type
    // or passphrase-protected) is reported rather than saved.
    private fun importKeyFrom(uri: Uri) {
        thread {
            val error = try {
                val data = contentResolver.openInputStream(uri)?.use { input ->
                    val out = ByteArrayOutputStream()
                    val buf = ByteArray(4096)
                    while (true) {
                        val n = input.read(buf)
                        if (n < 0) break
                        if (out.size() + n > Keys.MAX_KEY_BYTES) {
                            throw IllegalArgumentException("private key exceeds ${Keys.MAX_KEY_BYTES} bytes")
                        }
                        out.write(buf, 0, n)
                    }
                    out.toByteArray()
                }
                    ?: throw IllegalArgumentException("could not read the file")
                Keys.importKey(this, data)
                null
            } catch (e: Exception) {
                e.message ?: "import failed"
            }
            runOnUiThread {
                if (isFinishing || isDestroyed) return@runOnUiThread
                if (error == null) {
                    Scheduler.apply(this)
                    refresh()
                    Toast.makeText(this, "Key imported.", Toast.LENGTH_SHORT).show()
                    showKey()
                } else {
                    AlertDialog.Builder(this)
                        .setTitle("Import failed")
                        .setMessage(error)
                        .setPositiveButton("OK", null)
                        .show()
                }
            }
        }
    }
}
