package invalid.lena.rsend

import android.Manifest
import android.content.ActivityNotFoundException
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager
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.ImageView
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 kotlin.concurrent.thread

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

    companion object {
        private const val MANUAL_WORK_NAME = "sync-now"
    }

    private lateinit var lastSync: 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 remotes: LinearLayout
    private lateinit var folders: LinearLayout
    private lateinit var btnSync: Button
    private lateinit var syncBar: ProgressBar
    private lateinit var syncStatus: TextView

    // Last drawn schedule and the observed state of the periodic job. Observing
    // it keeps the dashboard off the 500ms blocking WorkManager query that used
    // to run on the main thread for every refresh.
    private var schedule: Schedule = Schedule()
    private var jobState: String = "none"
    private var recoveryShown = false

    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)
        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)
        remotes = findViewById(R.id.remotes)
        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()
        WorkManager.getInstance(this)
            .getWorkInfosForUniqueWorkLiveData(Scheduler.NAME)
            .observe(this) { infos ->
                jobState = activeWork(infos)?.state?.name ?: "none"
                renderSchedule(schedule)
            }
        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 {
            openSettings(
                Intent(
                    Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,
                    Uri.parse("package:$packageName"),
                ),
                Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION),
            )
        }
        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.
                openSettings(
                    Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
                        .putExtra(Settings.EXTRA_APP_PACKAGE, packageName),
                    Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse("package:$packageName")),
                )
            }
        }
        findViewById<LinearLayout>(R.id.rowBattery).setOnClickListener {
            if (batteryUnrestricted()) {
                openSettings(Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS))
            } else {
                openSettings(
                    Intent(
                        Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
                        Uri.parse("package:$packageName"),
                    ),
                    Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS),
                )
            }
        }
        findViewById<Button>(R.id.btnAddRemote).setOnClickListener {
            startActivity(Intent(this, RemoteActivity::class.java).putExtra("index", -1))
        }
        findViewById<Button>(R.id.btnAddFolder).setOnClickListener {
            if (Config.load(this).remotes.isEmpty()) {
                Toast.makeText(this, "Add a remote first.", Toast.LENGTH_LONG).show()
            } else {
                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.syncReady(Keys.exists(this))) {
            Toast.makeText(this, "Set up a key, a pinned remote, and at least one folder first.", Toast.LENGTH_LONG)
                .show()
            return
        }
        if (!allFiles()) {
            Toast.makeText(this, "Grant all-files access first, or rsend cannot read your folders.", Toast.LENGTH_LONG)
                .show()
            return
        }
        // A manual sync honours the "unmetered only" setting, but as a check
        // here rather than as a WorkManager constraint. A constrained request
        // would sit ENQUEUED until the phone found wifi, and observeSync counts
        // ENQUEUED as running, so the button would lock at "Syncing..." with no
        // way to cancel it. Refuse now, with a reason the user can act on.
        if (cfg.schedule.wifiOnly && getSystemService(ConnectivityManager::class.java).isActiveNetworkMetered) {
            Toast.makeText(
                this,
                "This network is metered and the schedule is set to unmetered only. Connect to Wi-Fi, or turn that off under Schedule.",
                Toast.LENGTH_LONG,
            ).show()
            return
        }
        val req = OneTimeWorkRequestBuilder<SyncWorker>().build()
        WorkManager.getInstance(this).enqueueUniqueWork(MANUAL_WORK_NAME, 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(MANUAL_WORK_NAME)
            .observe(this) { infos ->
                val active = activeWork(infos)
                if (active != null) {
                    syncBar.visibility = View.VISIBLE
                    syncStatus.visibility = View.VISIBLE
                    btnSync.isEnabled = false
                    btnSync.text = "Syncing..."
                    val p = active.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()
                }
            }
    }

    // WorkManager does not promise query result order. Unique work should have
    // one unfinished record; generation is a deterministic tie-breaker if an
    // old database contains more than one.
    private fun activeWork(infos: List<WorkInfo>): WorkInfo? =
        infos.filterNot { it.state.isFinished }.maxByOrNull { it.generation }

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

    private fun showRecoveryNotice() {
        if (recoveryShown) return
        val message = Config.recoveryMessage(this).trim()
        if (message.isEmpty()) return
        recoveryShown = true
        AlertDialog.Builder(this)
            .setTitle("Configuration recovered")
            .setMessage(message)
            .setPositiveButton("OK") { _, _ -> Config.clearRecoveryMessage(this) }
            .setCancelable(false)
            .show()
    }

    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)

    private fun openSettings(intent: Intent, fallback: Intent? = null) {
        try {
            startActivity(intent)
        } catch (_: ActivityNotFoundException) {
            if (fallback == null) {
                Toast.makeText(this, "This settings screen is not available on this device.", Toast.LENGTH_LONG).show()
            } else {
                try {
                    startActivity(fallback)
                } catch (_: ActivityNotFoundException) {
                    Toast.makeText(this, "This settings screen is not available on this device.", Toast.LENGTH_LONG)
                        .show()
                }
            }
        }
    }

    // renderSchedule draws the schedule row from the saved settings plus the
    // observed WorkManager state, so neither has to block to fetch the other.
    private fun renderSchedule(s: 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"
    }

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

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

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

        schedule = cfg.schedule
        renderSchedule(cfg.schedule)

        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")

        remotes.removeAllViews()
        if (cfg.remotes.isEmpty()) {
            remotes.addView(emptyHint("No remotes yet. Tap Add remote below."))
        } else {
            cfg.remotes.forEachIndexed { i, r ->
                addRow(
                    remotes, R.drawable.ic_remote, r.name,
                    "${r.user}@${r.host}:${r.port}",
                    if (r.pinned()) "pinned" else "no pin",
                ) {
                    startActivity(
                        Intent(this, RemoteActivity::class.java).putExtra("index", i)
                    )
                }
            }
        }

        folders.removeAllViews()
        if (cfg.folders.isEmpty()) {
            folders.addView(emptyHint("No folders yet. Tap Add folder below."))
        } else {
            cfg.folders.forEachIndexed { i, f ->
                addRow(
                    folders, R.drawable.ic_folder, f.name.ifEmpty { f.local },
                    "${f.local} -> ${f.remoteName}:${f.remotePath}",
                    if (f.delete) "mirror" else "add",
                ) {
                    startActivity(
                        Intent(this, FolderEditActivity::class.java).putExtra("index", i)
                    )
                }
            }
        }
    }

    // addRow inflates one tappable list row: icon, title, subtitle, chip.
    private fun addRow(
        list: LinearLayout,
        icon: Int,
        title: String,
        sub: String,
        chip: String,
        onClick: () -> Unit,
    ) {
        val row = layoutInflater.inflate(R.layout.item_row, list, false)
        row.findViewById<ImageView>(R.id.icon).setImageResource(icon)
        row.findViewById<TextView>(R.id.rowTitle).text = title
        row.findViewById<TextView>(R.id.rowSub).text = sub
        row.findViewById<TextView>(R.id.rowChip).text = chip
        row.setOnClickListener { onClick() }
        list.addView(row)
    }

    private fun emptyHint(msg: String): TextView = TextView(this).apply {
        text = msg
        setTextAppearance(R.style.TextAppearance_Rsend_Caption)
        setPadding(0, 0, 0, resources.getDimensionPixelSize(R.dimen.space_m))
    }

    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() {
        if (!Keys.exists(this)) {
            showKeyDialog("")
            return
        }
        thread {
            val result = try {
                Result.success(Keys.publicKeyText(this))
            } catch (e: Exception) {
                Result.failure(e)
            }
            runOnUiThread {
                if (isFinishing || isDestroyed) return@runOnUiThread
                result.fold(
                    onSuccess = { showKeyDialog(it) },
                    onFailure = {
                        AlertDialog.Builder(this)
                            .setTitle("Key unavailable")
                            .setMessage(it.message ?: "could not read public key")
                            .setPositiveButton("OK", null)
                            .show()
                    },
                )
            }
        }
    }

    private fun showKeyDialog(pub: String) {
        val view = layoutInflater.inflate(R.layout.dialog_key, null)
        val keyText = view.findViewById<TextView>(R.id.keyText)
        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()
            chooseKeyToImport()
        }
        dialog.show()
    }

    private fun chooseKeyToImport() {
        if (!Keys.exists(this)) {
            openKeyDoc.launch(arrayOf("*/*"))
            return
        }
        AlertDialog.Builder(this)
            .setTitle("Replace identity key?")
            .setMessage(
                "Importing a key replaces the current one. The imported public key must be present in every " +
                    "server's authorized_keys file."
            )
            .setPositiveButton("Choose key") { _, _ -> openKeyDoc.launch(arrayOf("*/*")) }
            .setNegativeButton("Cancel", null)
            .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 accepts only unencrypted Ed25519 keys, using the
    // same parser as the transport, and reports any other file without saving.
    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()
                }
            }
        }
    }
}
