package app.chanora.chanora_flutter

import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.provider.Settings
import android.util.Log
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat

/**
 * Android runtime-permission requester for the audio subsystem.
 *
 * Trace:
 * - SDD-106 `AndroidPermissionRequester` (RECORD_AUDIO runtime flow with
 *   listen-only fallback, settings deep-link for permanent denial,
 *   revocation handling, bridge event surface).
 * - SRS-209 (Android runtime permission UX).
 * - SAD-085 (source SAD for this SDD unit).
 *
 * ## Why Activity-bound, not a global singleton
 *
 * Android runtime permission requests are inherently tied to a
 * concrete [Activity] (the system dialog is hosted by the Activity and
 * the result is delivered through `onRequestPermissionsResult`). A
 * process-wide singleton would have to track which Activity is
 * currently in the foreground and would race with configuration
 * changes. Binding the requester to the host Activity ([MainActivity],
 * owned by Wave 2B-4) keeps the lifecycle simple and audit-clear: one
 * requester per Activity instance, no static state to leak across
 * Activity recreation.
 *
 * ## Contract with the host Activity
 *
 * The host Activity MUST:
 *   1. Construct one [AndroidPermissionRequester] in `onCreate`.
 *   2. Forward `onRequestPermissionsResult` to
 *      [handleRequestPermissionsResult].
 *   3. Call [onResume] from its `Activity.onResume` so mid-session
 *      revocation (per SDD-106 §4) is observed.
 *
 * The Dart-side wiring (publishing `BridgeEvent::PermissionState` over
 * the bridge event stream, per SDD-106 §5) is delivered through the
 * [MethodChannels.ANDROID_PERMISSIONS] channel. The actual MethodChannel
 * handler is wired by [MainActivity] in a follow-up task; for now this
 * class exposes [stateChangeListener] and the channel name constant.
 *
 * ## Thread model
 *
 * All public methods are expected to be called from the Android main
 * thread (Activity lifecycle thread). The Rust audio engine queries
 * cached state via a separate JNI helper (SDD-106 §7, out of scope for
 * this Kotlin class) — this class does not directly expose state to
 * other threads.
 */
class AndroidPermissionRequester {

    /**
     * Discrete permission-state values surfaced to the host Activity
     * and onward to the Rust bridge.
     *
     * Trace: SDD-106 §5 (state machine), SRS-209.
     *
     * Note: SDD-106 §5 also lists `Undetermined` as a fourth state.
     * That state is internal to the Rust-side cache (initial value
     * before the first query); the Kotlin requester never emits it
     * because every emission corresponds to a resolved
     * `checkSelfPermission` result.
     */
    sealed class PermissionState {
        /** Permission granted by the user. */
        object Granted : PermissionState()
        /** Permission denied, but the user may still be re-prompted. */
        object Denied : PermissionState()
        /**
         * Permission denied with "do not ask again" — Android will no
         * longer show the system dialog. The UI must deep-link to app
         * settings via [openAppSettings].
         *
         * Trace: SDD-106 §3.
         */
        object PermanentlyDenied : PermissionState()
    }

    private companion object {
        private const val TAG = "ChanoraPerm"

        /**
         * Stable request code for `RECORD_AUDIO`. Must remain stable
         * across releases so that result dispatch in
         * [handleRequestPermissionsResult] matches the request.
         */
        private const val REQ_RECORD_AUDIO = 0x52454341 // "RECA"
        private const val REQ_STARTUP_PERMISSIONS = 0x53544152 // "STAR"

        /**
         * SharedPreferences file backing the cross-process / cross-launch
         * "has the user ever been asked for this permission?" flag.
         *
         * Trace: SDD-106 §3, §4 (M-1 strict-review fix).
         */
        private const val PREFS_FILE = "chanora_permissions"

        /**
         * Boolean key set to `true` the first time we invoke
         * [ActivityCompat.requestPermissions] for `RECORD_AUDIO`.
         *
         * Contract (M-1 strict-review fix):
         *   - `false` (default) — the user has never been prompted in
         *     any prior process for `RECORD_AUDIO`. In this state,
         *     `shouldShowRequestPermissionRationale == false` means
         *     "fresh / never asked", NOT "permanently denied".
         *   - `true` — the user has been prompted at least once in
         *     some prior (or current) process. In this state,
         *     `shouldShowRequestPermissionRationale == false` after a
         *     non-granted result indicates "Do not ask again" /
         *     permanent denial.
         *
         * Persisting this across process death is what lets
         * [onResume] and [handleRequestPermissionsResult] faithfully
         * observe revocation per SDD-106 §4 even when Android killed
         * and restarted the process during a settings round-trip.
         */
        private const val KEY_RECORD_AUDIO_HAS_REQUESTED = "record_audio_has_requested"
        private const val KEY_BLUETOOTH_CONNECT_HAS_REQUESTED = "bluetooth_connect_has_requested"
        private const val KEY_POST_NOTIFICATIONS_HAS_REQUESTED = "post_notifications_has_requested"
    }

    /**
     * In-flight callback for the active permission request, if any.
     * Cleared in [handleRequestPermissionsResult]. Single-flight is
     * enforced by overwriting (the most recent caller wins); concurrent
     * `voice_join` coalescing (SDD-106 §8) is owned by the Rust side.
     */
    private var pendingCallback: ((PermissionState) -> Unit)? = null
    private var pendingStartupCallback: ((Map<String, PermissionState>) -> Unit)? = null

    /**
     * Optional listener invoked on every resolved state change. The
     * MainActivity wires this to a [io.flutter.plugin.common.MethodChannel]
     * on [MethodChannels.ANDROID_PERMISSIONS] in a follow-up task.
     *
     * Trace: SDD-106 §5.
     */
    var stateChangeListener: ((permission: String, state: PermissionState) -> Unit)? = null

    fun ensureStartupPermissions(
        activity: Activity,
        callback: (Map<String, PermissionState>) -> Unit,
    ) {
        val required = mutableListOf<String>()
        val resolved = mutableMapOf<String, PermissionState>()

        fun resolveNow(permission: String) {
            resolved[permission] = currentState(activity, permission)
        }

        val startupPermissions = buildList {
            add(Manifest.permission.RECORD_AUDIO)
            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
                add(Manifest.permission.BLUETOOTH_CONNECT)
            }
            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
                add(Manifest.permission.POST_NOTIFICATIONS)
            }
        }

        for (permission in startupPermissions) {
            val granted = ContextCompat.checkSelfPermission(activity, permission) ==
                PackageManager.PERMISSION_GRANTED
            if (granted) {
                resolveNow(permission)
            } else {
                markRequested(activity, permission)
                required.add(permission)
            }
        }

        if (required.isEmpty()) {
            for (permission in startupPermissions) {
                emit(permission, resolved[permission] ?: currentState(activity, permission), null)
            }
            callback(
                startupPermissions.associateWith { resolved[it] ?: currentState(activity, it) }
            )
            return
        }

        pendingStartupCallback = callback
        ActivityCompat.requestPermissions(
            activity,
            required.toTypedArray(),
            REQ_STARTUP_PERMISSIONS,
        )
    }

    /**
     * Ensure `RECORD_AUDIO` is granted, prompting the user if not.
     *
     * Behaviour matrix (SDD-106 §1–§3):
     *   - Already granted → [callback] invoked synchronously with
     *     [PermissionState.Granted].
     *   - Not granted, may prompt → system dialog shown; result
     *     delivered asynchronously via
     *     [handleRequestPermissionsResult].
     *   - Permanently denied → caller is expected to surface
     *     "Open settings" affordance and call [openAppSettings].
     *
     * Trace: SDD-106 §1, §2, §3.
     */
    fun ensureRecordAudioPermission(
        activity: Activity,
        callback: (PermissionState) -> Unit,
    ) {
        val permission = Manifest.permission.RECORD_AUDIO
        val granted = ContextCompat.checkSelfPermission(activity, permission) ==
            PackageManager.PERMISSION_GRANTED
        if (granted) {
            emit(permission, PermissionState.Granted, callback)
            return
        }

        // Stash the callback; result is dispatched in
        // handleRequestPermissionsResult.
        pendingCallback = callback
        // SDD-106 §3, §4 (M-1 strict-review fix): record that the user
        // has now been prompted at least once. This persists across
        // process death so subsequent shouldShowRequestPermissionRationale
        // == false readings can be classified as permanent denial rather
        // than "never asked".
        markRecordAudioRequested(activity)
        ActivityCompat.requestPermissions(
            activity,
            arrayOf(permission),
            REQ_RECORD_AUDIO,
        )
    }

    /**
     * Forwarded from `Activity.onRequestPermissionsResult`. Returns
     * `true` if the result was consumed by this requester, `false`
     * otherwise (so the caller can chain other requesters).
     *
     * Trace: SDD-106 §2, §3.
     */
    fun handleRequestPermissionsResult(
        activity: Activity,
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray,
    ): Boolean {
        if (requestCode == REQ_STARTUP_PERMISSIONS) {
            val cb = pendingStartupCallback
            pendingStartupCallback = null
            val resolved = mutableMapOf<String, PermissionState>()
            permissions.forEachIndexed { index, permission ->
                val state = if (index < grantResults.size &&
                    grantResults[index] == PackageManager.PERMISSION_GRANTED
                ) {
                    PermissionState.Granted
                } else {
                    currentDeniedState(activity, permission)
                }
                resolved[permission] = state
                emit(permission, state, null)
            }
            cb?.invoke(resolved)
            return true
        }
        if (requestCode != REQ_RECORD_AUDIO) return false
        val cb = pendingCallback
        pendingCallback = null

        val idx = permissions.indexOf(Manifest.permission.RECORD_AUDIO)
        if (idx < 0 || idx >= grantResults.size) {
            // Edge case: user dismissed dialog without a result (e.g.
            // tap outside on some OEMs). Treat as Denied (re-promptable).
            Log.w(TAG, "RECORD_AUDIO result missing from callback; treating as Denied")
            emit(Manifest.permission.RECORD_AUDIO, PermissionState.Denied, cb)
            return true
        }

        val state: PermissionState = if (grantResults[idx] == PackageManager.PERMISSION_GRANTED) {
            PermissionState.Granted
        } else {
            // Per SDD-106 §3 (M-1 strict-review fix): distinguish
            // "permanently denied" via shouldShowRequestPermissionRationale
            // == false AFTER a denial. Because we just returned from a
            // system dialog, the persistent has-ever-requested flag is
            // guaranteed true at this point; we still consult it for
            // symmetry with onResume and to make the contract explicit.
            val shouldRationale = ActivityCompat.shouldShowRequestPermissionRationale(
                activity,
                Manifest.permission.RECORD_AUDIO,
            )
            val hasEverRequested = hasEverRequestedRecordAudio(activity)
            if (shouldRationale) {
                PermissionState.Denied
            } else if (hasEverRequested) {
                PermissionState.PermanentlyDenied
            } else {
                // Defensive: should be unreachable because we set the
                // flag immediately before requestPermissions, but if a
                // host bypasses ensureRecordAudioPermission and forwards
                // a result, treat the absence of prior ask as Denied
                // rather than over-classifying as permanent.
                PermissionState.Denied
            }
        }
        emit(Manifest.permission.RECORD_AUDIO, state, cb)
        return true
    }

    /**
     * Re-check `RECORD_AUDIO` state on Activity resume.
     *
     * Contract: [MainActivity] (Wave 2B-4) MUST invoke this from its
     * `onResume` so mid-session revocation (SDD-106 §4) — which Android
     * may apply by killing/restarting the process — is observed and
     * propagated to the Rust audio engine via [stateChangeListener].
     *
     * The active voice session (per SDD-094) is not torn down here;
     * only the cached permission state is refreshed. Clamping
     * `capture_active = false` in the listen-only path is the Rust
     * audio engine's responsibility (SDD-106 §4, §6).
     *
     * Trace: SDD-106 §4.
     */
    fun onResume(activity: Activity, callback: (PermissionState) -> Unit) {
        val permission = Manifest.permission.RECORD_AUDIO
        val granted = ContextCompat.checkSelfPermission(activity, permission) ==
            PackageManager.PERMISSION_GRANTED
        val state: PermissionState = if (granted) {
            PermissionState.Granted
        } else {
            // SDD-106 §3, §4 (M-1 strict-review fix): distinguish three
            // cases that all present as "not granted" on cold launch:
            //
            //   (a) Permission was never asked in any prior process —
            //       persistent flag is false. Surface Denied (re-promptable);
            //       UI will trigger the system dialog on first use.
            //   (b) Permission was asked before and is currently denied
            //       but still re-promptable — shouldShowRequestPermissionRationale
            //       returns true. Surface Denied.
            //   (c) Permission was asked before and the user selected
            //       "Don't ask again" or revoked from Settings — flag is
            //       true AND shouldShowRequestPermissionRationale is false.
            //       Surface PermanentlyDenied so the UI can deep-link to
            //       app settings (SDD-106 §3).
            //
            // Without the persistent flag, cases (a) and (c) are
            // indistinguishable after a process restart, which is what
            // the original implementation conservatively folded into
            // Denied at the cost of misreporting revocation. The
            // SharedPreferences-backed flag closes that gap.
            val hasEverRequested = hasEverRequestedRecordAudio(activity)
            if (!hasEverRequested) {
                PermissionState.Denied
            } else {
                val shouldRationale = ActivityCompat.shouldShowRequestPermissionRationale(
                    activity,
                    Manifest.permission.RECORD_AUDIO,
                )
                if (shouldRationale) PermissionState.Denied else PermissionState.PermanentlyDenied
            }
        }
        emit(permission, state, callback)
    }

    fun currentState(activity: Activity, permission: String): PermissionState {
        val granted = ContextCompat.checkSelfPermission(activity, permission) ==
            PackageManager.PERMISSION_GRANTED
        if (granted) return PermissionState.Granted
        return currentDeniedState(activity, permission)
    }

    /**
     * Deep-link to the application's Settings → App info page so the
     * user can re-grant a permanently-denied permission.
     *
     * Trace: SDD-106 §3.
     */
    fun openAppSettings(activity: Activity) {
        val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
            data = Uri.fromParts("package", activity.packageName, null)
            addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
        }
        try {
            activity.startActivity(intent)
        } catch (e: android.content.ActivityNotFoundException) {
            // Surface for diagnostics; we do not silently swallow.
            Log.e(TAG, "Failed to launch app settings: ${e.message}", e)
        }
    }

    /** Emit a resolved state to both the per-request callback and the listener. */
    private fun emit(
        permission: String,
        state: PermissionState,
        callback: ((PermissionState) -> Unit)?,
    ) {
        callback?.invoke(state)
        // The stateChangeListener is wired by MainActivity to invoke
        // METHOD_PERMISSION_STATE_CHANGED on ANDROID_PERMISSIONS, which
        // publishes BridgeEvent::PermissionState to the bridge event
        // stream. See MainActivity.configureFlutterEngine (SDD-106 §5).
        stateChangeListener?.invoke(permission, state)
    }

    /**
     * Read the persistent "has the user ever been asked for RECORD_AUDIO?"
     * flag. See [KEY_RECORD_AUDIO_HAS_REQUESTED] for the contract.
     *
     * Trace: SDD-106 §3, §4 (M-1 strict-review fix).
     */
    private fun hasEverRequestedRecordAudio(activity: Activity): Boolean {
        val prefs = activity.applicationContext.getSharedPreferences(
            PREFS_FILE,
            Context.MODE_PRIVATE,
        )
        return prefs.getBoolean(KEY_RECORD_AUDIO_HAS_REQUESTED, false)
    }

    private fun hasEverRequested(activity: Activity, permission: String): Boolean {
        val prefs = activity.applicationContext.getSharedPreferences(
            PREFS_FILE,
            Context.MODE_PRIVATE,
        )
        return prefs.getBoolean(requestedKey(permission), false)
    }

    /**
     * Persist that the user has now been prompted for RECORD_AUDIO at
     * least once. Idempotent. Uses `apply` (async, lossless across
     * process death once committed) since the flag is consulted on
     * subsequent launches, not in the same critical section.
     *
     * Trace: SDD-106 §3, §4 (M-1 strict-review fix).
     */
    private fun markRecordAudioRequested(activity: Activity) {
        val prefs = activity.applicationContext.getSharedPreferences(
            PREFS_FILE,
            Context.MODE_PRIVATE,
        )
        if (!prefs.getBoolean(KEY_RECORD_AUDIO_HAS_REQUESTED, false)) {
            prefs.edit().putBoolean(KEY_RECORD_AUDIO_HAS_REQUESTED, true).apply()
        }
    }

    private fun markRequested(activity: Activity, permission: String) {
        val prefs = activity.applicationContext.getSharedPreferences(
            PREFS_FILE,
            Context.MODE_PRIVATE,
        )
        val key = requestedKey(permission)
        if (!prefs.getBoolean(key, false)) {
            prefs.edit().putBoolean(key, true).apply()
        }
        if (permission == Manifest.permission.RECORD_AUDIO) {
            markRecordAudioRequested(activity)
        }
    }

    private fun requestedKey(permission: String): String = when (permission) {
        Manifest.permission.RECORD_AUDIO -> KEY_RECORD_AUDIO_HAS_REQUESTED
        Manifest.permission.BLUETOOTH_CONNECT -> KEY_BLUETOOTH_CONNECT_HAS_REQUESTED
        Manifest.permission.POST_NOTIFICATIONS -> KEY_POST_NOTIFICATIONS_HAS_REQUESTED
        else -> "requested_${permission.replace('.', '_')}"
    }

    private fun currentDeniedState(activity: Activity, permission: String): PermissionState {
        val hasEverRequested = hasEverRequested(activity, permission)
        if (!hasEverRequested) return PermissionState.Denied
        val shouldRationale = ActivityCompat.shouldShowRequestPermissionRationale(
            activity,
            permission,
        )
        return if (shouldRationale) {
            PermissionState.Denied
        } else {
            PermissionState.PermanentlyDenied
        }
    }
}
