package paris.tourolle.bikecontrol

import android.Manifest
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.os.Bundle
import android.view.WindowManager
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import android.content.pm.PackageManager

/**
 * BikeControl's Android entry point.
 *
 * `tauri android init` generates a bare `class MainActivity : TauriActivity()`.
 * This tracked replacement adds the four things the app cannot start without,
 * and nothing else — everything above the JNI line still lives in Rust.
 *
 *   1. Loading the Rust library and initialising btleplug's Android backend.
 *   2. Asking for the BLE runtime permissions.
 *   3. Reporting whether the Bluetooth radio is actually switched on, which
 *      btleplug has no concept of, and offering to switch it on.
 *   4. Keeping the screen awake while the app is in front (NFR-11).
 */
class MainActivity : TauriActivity() {
    companion object {
        init {
            // btleplug's Android backend registers its native methods against
            // Java classes, so the Rust library has to be in the process before
            // `initBtleplug` can be called. `TauriActivity` loads it too, but
            // later than we need and `System.loadLibrary` is idempotent, so
            // doing it here is free.
            System.loadLibrary("bikecontrol_app_lib")
        }
    }

    /**
     * Implemented in Rust (`src-tauri/src/android.rs`). Hands btleplug a JNIEnv
     * so it can register its native methods and cache its classes.
     *
     * It must be called from a Java thread, because class lookup uses the
     * *calling* thread's class loader: from a Rust-spawned thread the app's
     * classes are simply not visible and btleplug fails with ClassNotFound at
     * the first scan rather than here.
     */
    private external fun initBtleplug()

    /**
     * Hand Rust a reference to this activity, so it can call back the other way
     * — specifically [requestBluetoothEnable], which only the UI thread of a
     * live activity may do.
     */
    private external fun nativeSetActivity(activity: MainActivity)

    /**
     * Tell Rust whether the radio is on.
     *
     * btleplug has no notion of a *disabled* adapter: `getDefaultAdapter()`
     * hands back a perfectly good object whose scans quietly fail, so without
     * this the rider is told "no trainer found" when the truth is "Bluetooth is
     * off". Pushed from here rather than polled from Rust because the state
     * arrives as a broadcast anyway.
     */
    private external fun nativeBluetoothStateChanged(enabled: Boolean)

    /**
     * BLE permissions, split at API 31 exactly as the manifest declares them.
     * Asking for a permission the platform version does not know results in a
     * permanent denial, so the two eras must never be mixed.
     */
    private val blePermissions: Array<String>
        get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            arrayOf(Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT)
        } else {
            arrayOf(
                Manifest.permission.BLUETOOTH,
                Manifest.permission.BLUETOOTH_ADMIN,
                Manifest.permission.ACCESS_FINE_LOCATION,
            )
        }

    private val permissionRequest =
        registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { }

    private fun missingPermissions(): Array<String> =
        blePermissions.filter {
            ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
        }.toTypedArray()

    // -----------------------------------------------------------------------
    // Adapter state.

    private val bluetoothAdapter: BluetoothAdapter?
        get() = getSystemService(BluetoothManager::class.java)?.adapter

    /**
     * The rider can reach the Bluetooth toggle without leaving us — the quick
     * settings shade is an overlay, not a new activity, so `onResume` never
     * fires. Only the broadcast catches that.
     */
    private val bluetoothStateReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            if (intent?.action == BluetoothAdapter.ACTION_STATE_CHANGED) {
                reportBluetoothState()
            }
        }
    }

    private val enableBluetoothRequest =
        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
            // Whatever the rider chose, the answer is in the adapter, not in the
            // result code — declining leaves it off, and some OEM dialogs report
            // success before the radio has finished coming up.
            reportBluetoothState()
        }

    private fun reportBluetoothState() {
        nativeBluetoothStateChanged(bluetoothAdapter?.isEnabled == true)
    }

    /**
     * Called *from Rust*, on a Rust thread, when the rider asks to turn the
     * radio on from the connection screen.
     *
     * The thread hop is done here rather than at the call site because an
     * `ActivityResultLauncher` may only be launched from the main thread, and
     * Rust has no cheap way to get there. Silently does nothing if the radio is
     * already on, or if BLUETOOTH_CONNECT has not been granted — since API 31
     * the enable intent requires it, and firing it without throws.
     */
    @Suppress("unused") // called by name over JNI
    fun requestBluetoothEnable() {
        runOnUiThread {
            if (bluetoothAdapter?.isEnabled != false) return@runOnUiThread
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
                ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT)
                != PackageManager.PERMISSION_GRANTED
            ) {
                return@runOnUiThread
            }
            enableBluetoothRequest.launch(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE))
        }
    }

    override fun onStart() {
        super.onStart()
        ContextCompat.registerReceiver(
            this,
            bluetoothStateReceiver,
            IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED),
            ContextCompat.RECEIVER_NOT_EXPORTED,
        )
        reportBluetoothState()
    }

    override fun onStop() {
        unregisterReceiver(bluetoothStateReceiver)
        super.onStop()
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        // BEFORE super.onCreate, and the order is load-bearing. TauriActivity's
        // super chain registers WryLifecycleObserver on ProcessLifecycleOwner,
        // which synchronously dispatches onCreate -> Rust.create(); tao's
        // ndk_glue::create then spawns a thread and runs our `run()` on it. That
        // thread builds the AppState and starts the scan loop while we are still
        // in onCreate. If it reaches btleplug first, droidplug's global_adapter()
        // is an `expect` and the scan task panics — silently, for the life of the
        // process. Initialising here means the race cannot be lost.
        initBtleplug()

        super.onCreate(savedInstanceState)

        // Both land in Rust statics, so they are safe at any point after the
        // library is loaded. Doing them here means the scan loop knows the state
        // of the radio from close to its first pass rather than from onStart.
        nativeSetActivity(this)
        reportBluetoothState()

        // NFR-11 on Android. A rider mid-interval is not "idle" just because
        // they have not touched the screen, and a screen that blanks takes the
        // numbers they are pacing off with it. This is the window flag rather
        // than a WAKE_LOCK: it needs no permission and it lapses automatically
        // when the app goes to the background, so there is nothing to leak.
        window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)

        val missing = missingPermissions()
        if (missing.isNotEmpty()) {
            permissionRequest.launch(missing)
        }
    }

    /**
     * Ask again on the way back in. A rider who declined, went to Settings and
     * granted it, or simply dismissed the dialog, gets another chance without
     * having to reinstall. Android stops showing the dialog once the user has
     * refused twice, so this cannot become a loop.
     */
    override fun onResume() {
        super.onResume()

        // Retry the backend too. The usual reason it failed in onCreate is that
        // Bluetooth was switched off at launch, and the fix for that happens in
        // Settings — i.e. while we are in the background. The call is idempotent
        // (btleplug guards the native-method registration on a OnceCell and only
        // the adapter is retried), so calling it on every resume costs nothing
        // once it has succeeded.
        initBtleplug()

        val missing = missingPermissions()
        if (missing.isNotEmpty()) {
            permissionRequest.launch(missing)
        }
    }
}
