package wang.yaojia.webterm.push

import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import androidx.core.app.NotificationCompat
import wang.yaojia.webterm.MainActivity

/**
 * Builds the lock-screen notification LOCALLY (FCM sends data-only; there is no server `notification`
 * block — plan §4.5 / R1). The `cls` → channel/title/actions mapping is a PURE function ([plan]) so
 * it is JVM-unit-testable and so the trust split is decided in one place:
 *
 *  - `needs-input` (a held gate) → high-importance channel + **Allow** and **Deny** actions.
 *  - `done` (finished) → default-importance channel, informational, **no actions**.
 *  - anything else → null (unknown class ⇒ show nothing).
 *
 * ### Content minimization (plan §4.5 / §8)
 * Every visible string ([NotificationPlan.title]/[NotificationPlan.text]) is a static constant derived
 * from `cls` ALONE — never the token, cwd, command, or terminal bytes. The single-use token travels
 * only inside the action [PendingIntent] extras (Deny → receiver, Allow → trampoline), which are
 * `FLAG_IMMUTABLE` (plan §8), and is handed straight to the POST — never rendered, persisted, or logged.
 */
public object NotificationBuilder {

    /** The local action buttons a notification can carry (decided by `cls` only). */
    public enum class Action { ALLOW, DENY }

    /**
     * The Android-free description of a notification, derived purely from `cls`. [highImportance]
     * maps to the channel importance in [build]; keeping it a boolean (not a `NotificationManager`
     * constant) keeps this value pure and JVM-testable.
     */
    public data class NotificationPlan(
        public val channelId: String,
        public val channelName: String,
        public val highImportance: Boolean,
        public val title: String,
        public val text: String,
        public val actions: List<Action>,
    )

    // ── Pure mapping (JVM-tested) ────────────────────────────────────────────────────────────

    /** `cls` → [NotificationPlan], or null for an unknown class. Purely a function of [cls]. */
    public fun plan(cls: String): NotificationPlan? =
        when (cls) {
            PushPayload.CLASS_NEEDS_INPUT -> NotificationPlan(
                channelId = CHANNEL_GATE,
                channelName = CHANNEL_GATE_NAME,
                highImportance = true,
                title = GATE_TITLE,
                text = GATE_TEXT,
                actions = listOf(Action.ALLOW, Action.DENY),
            )
            PushPayload.CLASS_DONE -> NotificationPlan(
                channelId = CHANNEL_DONE,
                channelName = CHANNEL_DONE_NAME,
                highImportance = false,
                title = DONE_TITLE,
                text = DONE_TEXT,
                actions = emptyList(),
            )
            else -> null
        }

    // ── Android assembly ─────────────────────────────────────────────────────────────────────

    /**
     * Turn a [payload] + its [plan] into a real [Notification]. Actions are attached only when the
     * plan has them AND the payload actually carries a token (a gate with no token degrades to a
     * tap-to-open notification). Every [PendingIntent] is `FLAG_IMMUTABLE`.
     */
    public fun build(context: Context, payload: PushPayload, plan: NotificationPlan): Notification {
        ensureChannel(context, plan)
        val notificationId = notificationIdFor(payload.sessionId.toString())

        val builder = NotificationCompat.Builder(context, plan.channelId)
            .setSmallIcon(android.R.drawable.stat_notify_chat)
            .setContentTitle(plan.title)
            .setContentText(plan.text)
            .setAutoCancel(true)
            .setCategory(NotificationCompat.CATEGORY_MESSAGE)
            .setPriority(
                if (plan.highImportance) NotificationCompat.PRIORITY_HIGH else NotificationCompat.PRIORITY_DEFAULT,
            )
            .setContentIntent(openAppIntent(context, notificationId))

        if (plan.actions.isNotEmpty() && payload.isActionableGate) {
            val token = payload.token!!
            for (action in plan.actions) {
                builder.addAction(buildAction(context, action, payload.sessionId.toString(), token, notificationId))
            }
        }
        return builder.build()
    }

    /** Stable per-session id so a newer signal replaces the older card instead of stacking. */
    public fun notificationIdFor(sessionId: String): Int = sessionId.hashCode()

    private fun buildAction(
        context: Context,
        action: Action,
        sessionId: String,
        token: String,
        notificationId: Int,
    ): NotificationCompat.Action {
        val (label, pendingIntent) = when (action) {
            Action.DENY -> DENY_LABEL to denyIntent(context, sessionId, token, notificationId)
            Action.ALLOW -> ALLOW_LABEL to allowIntent(context, sessionId, token, notificationId)
        }
        return NotificationCompat.Action.Builder(0, label, pendingIntent).build()
    }

    /** Deny → the auth-free [DenyBroadcastReceiver] (fail-safe, expedited, no UI). */
    private fun denyIntent(context: Context, sessionId: String, token: String, notificationId: Int): PendingIntent {
        val intent = Intent(context, DenyBroadcastReceiver::class.java).apply {
            putExtra(EXTRA_SESSION_ID, sessionId)
            putExtra(EXTRA_TOKEN, token)
            putExtra(EXTRA_NOTIFICATION_ID, notificationId)
        }
        return PendingIntent.getBroadcast(context, requestCode(notificationId, action = 2), intent, immutableFlags())
    }

    /** Allow → the [AllowTrampolineActivity] (hosts BiometricPrompt — a receiver cannot, R1). */
    private fun allowIntent(context: Context, sessionId: String, token: String, notificationId: Int): PendingIntent {
        val intent = Intent(context, AllowTrampolineActivity::class.java).apply {
            putExtra(EXTRA_SESSION_ID, sessionId)
            putExtra(EXTRA_TOKEN, token)
            putExtra(EXTRA_NOTIFICATION_ID, notificationId)
            addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
        }
        return PendingIntent.getActivity(context, requestCode(notificationId, action = 1), intent, immutableFlags())
    }

    /** Tap-the-body → open the app. Carries NO token (opening the app never needs it). */
    private fun openAppIntent(context: Context, notificationId: Int): PendingIntent {
        val intent = Intent(context, MainActivity::class.java).apply {
            addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
        }
        return PendingIntent.getActivity(context, requestCode(notificationId, action = 0), intent, immutableFlags())
    }

    private fun ensureChannel(context: Context, plan: NotificationPlan) {
        val manager = context.getSystemService(NotificationManager::class.java) ?: return
        val importance =
            if (plan.highImportance) NotificationManager.IMPORTANCE_HIGH else NotificationManager.IMPORTANCE_DEFAULT
        manager.createNotificationChannel(NotificationChannel(plan.channelId, plan.channelName, importance))
    }

    private fun immutableFlags(): Int = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT

    /** Distinct request codes per (notification, action) so Allow/Deny/tap never collide or overwrite. */
    private fun requestCode(notificationId: Int, action: Int): Int = notificationId * REQUEST_CODE_STRIDE + action

    // ── Intent extra keys (shared with the receiver + trampoline; single source of truth) ────────
    public const val EXTRA_SESSION_ID: String = "wang.yaojia.webterm.push.SESSION_ID"
    public const val EXTRA_TOKEN: String = "wang.yaojia.webterm.push.TOKEN"
    public const val EXTRA_NOTIFICATION_ID: String = "wang.yaojia.webterm.push.NOTIFICATION_ID"

    // ── Channels + visible copy (static; token-free by construction) ─────────────────────────────
    public const val CHANNEL_GATE: String = "webterm.gate"
    public const val CHANNEL_DONE: String = "webterm.done"
    private const val CHANNEL_GATE_NAME = "Approval requests"
    private const val CHANNEL_DONE_NAME = "Session updates"
    private const val GATE_TITLE = "Claude needs your approval"
    private const val GATE_TEXT = "A pending action is waiting. Allow or deny it."
    private const val DONE_TITLE = "Claude finished"
    private const val DONE_TEXT = "Your session is done."
    private const val ALLOW_LABEL = "Allow"
    private const val DENY_LABEL = "Deny"

    private const val REQUEST_CODE_STRIDE = 3
}
