package app.chanora.chanora_flutter

import android.Manifest
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.IBinder
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat

/**
 * Android foreground service that hosts the lifecycle of an active
 * Chanora voice session.
 *
 * Trace:
 * - SDD-107 `AndroidVoiceForegroundService` (foreground service class,
 *   notification channel id `chanora.voice.session`,
 *   `foregroundServiceType=microphone` on API 30+, POST_NOTIFICATIONS
 *   on API 33+, START_NOT_STICKY, ongoing-notification re-post on
 *   dismissal, lifecycle bound to voice_join / voice_leave /
 *   shutdown_if_idle).
 * - SDD-105 (`AndroidJniBootstrap` — `JavaVM*` capture; the Rust side
 *   invokes [start] / [stop] via JNI per SDD-107 §10).
 * - SDD-106 (`AndroidPermissionRequester` — service may start in
 *   listen-only mode; type=microphone is still declared so capture can
 *   resume on grant without a service restart, per SDD-107 §8).
 * - SDD-108 (`AndroidAudioModeController` — owns
 *   `AudioManager.setMode`; this service does NOT touch the audio
 *   mode, per SDD-107 §8 and SDD-108 §1).
 * - SAD-086 (source SAD).
 *
 * ## Naming
 *
 * The SDD-107 implementation text references the simple name
 * `ChanoraVoiceForegroundService` in a `voice/` subpackage. This file
 * uses the FQN `app.chanora.chanora_flutter.AndroidVoiceForegroundService`
 * as coordinated for Wave 2B (the AndroidManifest entry declared by
 * Wave 2B-1 matches this FQN). The behaviour and lifecycle contract
 * are unchanged.
 *
 * ## Responsibilities
 *
 * This service is purely the foreground-lifecycle host: it keeps the
 * process foregrounded so the Rust audio engine (`crates/chanora_audio`)
 * can keep capture / playback streams open while the UI is backgrounded.
 *
 * It does NOT:
 *   - call `AudioManager.setMode` (owned by SDD-108 /
 *     `AndroidAudioModeController`),
 *   - open or drive audio streams (owned by the Rust audio engine
 *     via existing JNI on `crates/chanora_audio`),
 *   - perform any networking.
 */
class AndroidVoiceForegroundService : Service() {

    companion object {
        private const val TAG = "ChanoraVoiceFGS"

        /**
         * Notification channel id.
         *
         * Trace: SDD-107 §4.
         */
        private const val CHANNEL_ID = "chanora.voice.session"

        // SDD-107 §4: notification strings are sourced from
        // res/values/strings.xml and accessed via the service context
        // in instance methods below (ensureChannel, buildNotification).

        /**
         * Stable notification id ("CHAN") per SDD-107 §5. Must remain
         * stable so that re-posts after user dismissal land on the
         * same notification slot.
         */
        private const val NOTIFICATION_ID = 0x4348414E

        /** Intent action: begin / refresh the foreground session. */
        const val ACTION_START_VOICE_SESSION: String =
            "app.chanora.action.START_VOICE_SESSION"

        /** Intent action: terminate the foreground session. */
        const val ACTION_STOP_VOICE_SESSION: String =
            "app.chanora.action.STOP_VOICE_SESSION"

        /**
         * Start the service in voice-session mode.
         *
         * Intended call sites:
         *   - Kotlin: [MainActivity] or platform glue.
         *   - Rust: invoked from the `voice_join` bridge handler via
         *     JNI per SDD-107 §10 (the `JavaVM*` captured by SDD-105
         *     is used to call this static method).
         *
         * Trace: SDD-107 §7 (lifecycle — start).
         */
        @JvmStatic
        fun start(context: Context) {
            val intent = Intent(context, AndroidVoiceForegroundService::class.java).apply {
                action = ACTION_START_VOICE_SESSION
            }
            ContextCompat.startForegroundService(context, intent)
        }

        /**
         * Stop the service. Idempotent per SDD-107 §7.
         *
         * Trace: SDD-107 §7 (lifecycle — stop).
         */
        @JvmStatic
        fun stop(context: Context) {
            val intent = Intent(context, AndroidVoiceForegroundService::class.java).apply {
                action = ACTION_STOP_VOICE_SESSION
            }
            // We deliberately route through startService so the running
            // service receives ACTION_STOP_VOICE_SESSION via
            // onStartCommand and can perform an orderly stopForeground +
            // stopSelf. If the service is already stopped this is a
            // no-op aside from a brief onCreate/onDestroy cycle, which
            // satisfies the idempotent-stop contract.
            try {
                context.startService(intent)
            } catch (e: IllegalStateException) {
                // Background-start restrictions: if the app is in a
                // state that disallows starting services (e.g.,
                // process being torn down), there is nothing left to
                // stop. Log and continue.
                Log.w(TAG, "stop() could not deliver intent: ${e.message}")
            }
        }
    }

    /**
     * Service is start-only — no clients bind. Per SDD-107 §1.
     *
     * Trace: SDD-107 §1.
     */
    override fun onBind(intent: Intent?): IBinder? = null

    /**
     * Lifecycle entry. Promotes the service to foreground within the
     * 5-second platform deadline per SDD-107 §5, dispatches on the
     * incoming action, and returns [START_NOT_STICKY] per SDD-107 §7
     * (process death must NOT auto-restart; state is rebuilt from the
     * audio engine on the next `voice_join`).
     *
     * Trace: SDD-107 §5, §7.
     */
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        when (intent?.action) {
            ACTION_STOP_VOICE_SESSION -> {
                stopForegroundCompat()
                stopSelf()
            }
            // Treat null action (e.g., service re-creation by the
            // system before we return START_NOT_STICKY takes effect)
            // and any unknown action the same as START — we must call
            // startForeground within 5 seconds of onStartCommand or the
            // platform will kill us with a ForegroundServiceDidNotStart
            // exception (Android 12+).
            ACTION_START_VOICE_SESSION, null -> promoteToForeground()
            else -> {
                Log.w(TAG, "Unknown action: ${intent.action}; treating as START")
                promoteToForeground()
            }
        }
        // SDD-107 §7: do not auto-restart on process death; the next
        // voice_join re-starts the service explicitly.
        return START_NOT_STICKY
    }

    /**
     * Tear-down. Removes the ongoing notification and clears the
     * channel slot per SDD-107 §7 (stop path).
     *
     * If the user manually dismissed the notification while the
     * service was still running (API 34+ allows this per SDD-107 §7),
     * the dismissal does NOT stop the session — the audio engine is
     * the sole authority for when the service goes away. The service
     * re-posts the notification on the next state transition; in
     * practice "next state transition" means the next [start] call
     * arriving with ACTION_START_VOICE_SESSION, which calls
     * [startForeground] again. We do NOT schedule a JobScheduler /
     * AlarmManager re-post (per the task scope) — re-posting is
     * driven by the Rust audio engine emitting a state tick.
     *
     * Trace: SDD-107 §7.
     */
    override fun onDestroy() {
        stopForegroundCompat()
        try {
            val nm = getSystemService(NOTIFICATION_SERVICE) as? NotificationManager
            nm?.cancel(NOTIFICATION_ID)
        } catch (e: SecurityException) {
            // Unlikely on cancel(), but POST_NOTIFICATIONS-related
            // SecurityException surfaces have been reported on some
            // OEM builds. Do not crash teardown.
            Log.w(TAG, "cancel() raised SecurityException: ${e.message}")
        }
        super.onDestroy()
    }

    /**
     * Build (or refresh) the channel and call [startForeground].
     *
     * Trace: SDD-107 §4 (channel), §5 (notification), §6
     * (POST_NOTIFICATIONS handling).
     */
    private fun promoteToForeground() {
        ensureChannel()
        val notification = buildNotification()

        // POST_NOTIFICATIONS (API 33+): per SDD-107 §6, denial must NOT
        // block the service. We still call startForeground; the
        // platform will silently suppress the notification if the
        // permission is missing. We log a warning so this case is
        // observable.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            val granted = ContextCompat.checkSelfPermission(
                this,
                Manifest.permission.POST_NOTIFICATIONS,
            ) == PackageManager.PERMISSION_GRANTED
            if (!granted) {
                Log.w(
                    TAG,
                    "POST_NOTIFICATIONS not granted; service will run without a " +
                        "visible notification (SDD-107 §6).",
                )
            }
        }

        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                // Typed overload required on API 29+ when the manifest
                // declares foregroundServiceType=microphone (SDD-107
                // §2). On API 34+ this is enforced; declaring it on
                // 29+ is forward-safe.
                startForeground(
                    NOTIFICATION_ID,
                    notification,
                    ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE,
                )
            } else {
                // API 28 (minSdk per build.gradle.kts): un-typed
                // startForeground is the only available overload.
                @Suppress("DEPRECATION")
                startForeground(NOTIFICATION_ID, notification)
            }
        } catch (e: SecurityException) {
            // FOREGROUND_SERVICE_MICROPHONE missing on API 34+, or
            // RECORD_AUDIO missing while type=microphone is declared.
            // Per SDD-107 §6 / §7 we do not crash; the audio engine
            // will observe the absence via the BridgeEvent stream and
            // clamp to listen-only.
            Log.e(
                TAG,
                "startForeground(type=MICROPHONE) failed: ${e.message}. " +
                    "Service may not have promoted; SDD-107 §6 listen-only path applies.",
                e,
            )
        }
    }

    /**
     * Create the notification channel on first use. Repeated creates
     * are no-ops per Android contract (SDD-107 §4).
     */
    private fun ensureChannel() {
        // minSdk = 28 (Android 9), so NotificationChannel APIs (O+) are
        // unconditionally available.
        val nm = getSystemService(NOTIFICATION_SERVICE) as? NotificationManager ?: run {
            Log.e(TAG, "NotificationManager unavailable; cannot create channel")
            return
        }
        val existing = nm.getNotificationChannel(CHANNEL_ID)
        if (existing != null) return
        val channel = NotificationChannel(
            CHANNEL_ID,
            getString(R.string.voice_session_channel_name),
            NotificationManager.IMPORTANCE_LOW,
        ).apply {
            description = getString(R.string.voice_session_channel_description)
            setShowBadge(false)
        }
        nm.createNotificationChannel(channel)
    }

    /**
     * Build the ongoing notification.
     *
     * Privacy: per SDD-107 §5, the notification carries ONLY product
     * copy — no server-supplied channel names, user names, or message
     * content.
     *
     * Trace: SDD-107 §5.
     */
    private fun buildNotification(): Notification {
        val contentIntent: PendingIntent? = packageManager
            .getLaunchIntentForPackage(packageName)
            ?.let { launch ->
                PendingIntent.getActivity(
                    this,
                    0,
                    launch,
                    PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
                )
            }

        val builder = NotificationCompat.Builder(this, CHANNEL_ID)
            .setSmallIcon(R.mipmap.ic_launcher)
            .setContentTitle(getString(R.string.voice_session_notification_title))
            .setContentText(getString(R.string.voice_session_notification_text))
            .setOngoing(true)
            .setCategory(NotificationCompat.CATEGORY_CALL)
            .setPriority(NotificationCompat.PRIORITY_LOW)
            .setShowWhen(false)

        if (contentIntent != null) {
            builder.setContentIntent(contentIntent)
        }
        return builder.build()
    }

    /**
     * Version-portable `stopForeground` that removes the notification.
     */
    private fun stopForegroundCompat() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            stopForeground(STOP_FOREGROUND_REMOVE)
        } else {
            @Suppress("DEPRECATION")
            stopForeground(true)
        }
    }
}
