package io.unom.punktfunk

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import io.unom.punktfunk.models.PendingTrust

// The touch UI's prompts, each described once — a title, a list of [DialogAction]s (primary
// first) and a body — and drawn as a Material `AlertDialog`.
//
// History: these used to be drawn a second way, as the Compose console's glass card, and the two
// renderers drifted by hand until the descriptions were shared here. The Compose console is gone
// (the console is the Skia shell now — design/android-skia-console-port.md — and it draws its own
// pairing/trust screens), so only the touch renderer remains; the shared-description shape stays
// because it is the right shape regardless.

/** One button of a prompt. [primary] lifts it into the confirm slot; the rest lay out beside. */
class DialogAction(
    val label: String,
    val primary: Boolean = false,
    val enabled: Boolean = true,
    val onClick: () -> Unit,
)

/**
 * One prompt. [actions] is ordered PRIMARY FIRST — the first (or the one flagged primary) becomes
 * `confirmButton`, the rest lay out beside it.
 */
@Composable
fun PunktfunkDialog(
    title: String,
    onDismiss: () -> Unit,
    actions: List<DialogAction>,
    /**
     * False pins the prompt open against a stray tap outside it — for a dialog sitting over work
     * in flight, where a mis-tap would abandon it. Console-side there is no outside to tap, so
     * this only reaches the touch renderer.
     */
    dismissOnOutsideTap: Boolean = true,
    body: @Composable ColumnScope.() -> Unit,
) {
    val primary = actions.firstOrNull { it.primary } ?: actions.firstOrNull()
    val rest = actions.filter { it !== primary }
    AlertDialog(
        onDismissRequest = onDismiss,
        properties = DialogProperties(dismissOnClickOutside = dismissOnOutsideTap),
        title = { Text(title) },
        text = { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { body() } },
        confirmButton = {
            primary?.let { a ->
                TextButton(onClick = a.onClick, enabled = a.enabled) { Text(a.label) }
            }
        },
        dismissButton = {
            if (rest.isNotEmpty()) {
                Row {
                    rest.forEach { a ->
                        TextButton(onClick = a.onClick, enabled = a.enabled) { Text(a.label) }
                    }
                }
            }
        },
    )
}

/** A prompt's body paragraph, dimmed to sit under the title. */
@Composable
private fun PromptText(text: String) {
    Text(
        text,
        style = MaterialTheme.typography.bodyMedium,
        color = MaterialTheme.colorScheme.onSurfaceVariant,
    )
}

/** First connection to a host that advertised pair=optional: offer TOFU, but pitch PIN pairing. */
@Composable
fun TrustNewHostPrompt(
    pt: PendingTrust,
    onTrust: () -> Unit,
    onPairInstead: () -> Unit,
    onDismiss: () -> Unit,
) {
    PunktfunkDialog(
        title = "Trust this host?",
        onDismiss = onDismiss,
        actions = listOf(
            DialogAction("Trust (TOFU)", primary = true, onClick = onTrust),
            DialogAction("Pair with PIN…", onClick = onPairInstead),
            DialogAction("Cancel", onClick = onDismiss),
        ),
    ) {
        PromptText("First connection to ${pt.host}:${pt.port}.")
        pt.advertisedFp?.let { PromptText("Fingerprint ${it.take(16)}…") }
        PromptText(
            "This host allows trust-on-first-use, but that can't tell an impostor from the real " +
                "host. Pairing with a PIN is stronger — it proves both sides.",
        )
    }
}

/** The pinned fingerprint no longer matches — force re-pairing (never a silent re-trust). */
@Composable
fun FingerprintChangedPrompt(
    pt: PendingTrust,
    onRepair: () -> Unit,
    onDismiss: () -> Unit,
) {
    PunktfunkDialog(
        title = "Host identity changed",
        onDismiss = onDismiss,
        actions = listOf(
            DialogAction("Re-pair", primary = true, onClick = onRepair),
            DialogAction("Cancel", onClick = onDismiss),
        ),
    ) {
        PromptText(
            "The pinned fingerprint for ${pt.host} no longer matches what it now advertises. " +
                "This can mean a host reinstall — or an impostor. Re-pair with the host's PIN to " +
                "continue.",
        )
    }
}

/**
 * A fresh pair=required (or manual/unknown-policy) host: offer the two ways in. "Request access" is
 * the no-PIN path — connect and wait for the operator to click Approve in the host's console;
 * "Use a PIN…" switches to the SPAKE2 ceremony.
 */
@Composable
fun RequestAccessPrompt(
    pt: PendingTrust,
    onRequestAccess: () -> Unit,
    onUsePin: () -> Unit,
    onDismiss: () -> Unit,
) {
    PunktfunkDialog(
        title = "Pairing required",
        onDismiss = onDismiss,
        actions = listOf(
            DialogAction("Request access", primary = true, onClick = onRequestAccess),
            DialogAction("Use a PIN…", onClick = onUsePin),
            DialogAction("Cancel", onClick = onDismiss),
        ),
    ) {
        PromptText("${pt.host}:${pt.port} requires pairing before it will stream.")
        PromptText(
            "Request access and approve this device in the host's console (or web UI) — no PIN " +
                "needed. Or pair with the 4-digit PIN the host displays.",
        )
    }
}

/**
 * The no-PIN "request access" wait: the connect is parked on the host until the operator approves
 * this device. Cancel returns the UI immediately — the caller trips the per-attempt flag so a late
 * approval is torn down silently (see ConnectScreen.requestAccess) and resumes discovery.
 *
 * Outside taps are ignored: a connect is parked on the host, and a stray tap beside the card is not
 * a decision to abandon it.
 */
@Composable
fun AwaitingApprovalPrompt(hostLabel: String, onCancel: () -> Unit) {
    PunktfunkDialog(
        title = "Waiting for approval",
        onDismiss = onCancel,
        actions = listOf(DialogAction("Cancel", primary = true, onClick = onCancel)),
        dismissOnOutsideTap = false,
    ) {
        // MUST be the name the connect actually knocked with (`HostConnect`), or this sends the
        // user looking for a row the console does not show.
        val label = deviceName(LocalContext.current)
        Row(
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.spacedBy(12.dp),
        ) {
            CircularProgressIndicator(
                modifier = Modifier.size(20.dp),
                strokeWidth = 2.dp,
                color = MaterialTheme.colorScheme.primary,
            )
            Text(
                "Approve this device on $hostLabel.",
                color = MaterialTheme.colorScheme.onSurface,
            )
        }
        PromptText(
            "Open the host's console (or web UI) and approve “$label”. It connects " +
                "automatically once you approve — no PIN needed.",
        )
    }
}

/**
 * Android 17+ Local Network Protection rationale: ACCESS_LOCAL_NETWORK was denied, so discovery and
 * every connect are dead — offer the system prompt again and a settings deep link (a permanently-
 * denied request returns instantly without ever showing the prompt, so "Allow" alone isn't enough).
 */
@Composable
fun LocalNetworkPrompt(
    onAllow: () -> Unit,
    onSettings: () -> Unit,
    onDismiss: () -> Unit,
) {
    PunktfunkDialog(
        title = "Allow local network access",
        onDismiss = onDismiss,
        actions = listOf(
            DialogAction("Allow", primary = true, onClick = onAllow),
            DialogAction("Open settings", onClick = onSettings),
            DialogAction("Not now", onClick = onDismiss),
        ),
    ) {
        PromptText(
            "Android blocks Punktfunk from talking to devices on your network, so it can't find " +
                "or reach any host until you allow it.",
        )
        PromptText(
            "If no prompt appears after you allow it, enable “Nearby devices” for Punktfunk in " +
                "system settings.",
        )
    }
}

/**
 * The link measurement and what to do with the result. A TV box on a powerline adapter is exactly
 * the machine whose link is worth measuring, so this belongs on the couch surface too — and so
 * does [speedTestTargetNote], which the console used to omit, leaving a console user to guess
 * which layer Apply would write to.
 */
@Composable
fun SpeedTestPrompt(
    hostName: String,
    target: SpeedTestTarget,
    phase: SpeedTestPhase,
    onApply: (toProfile: Boolean) -> Unit,
    onDismiss: () -> Unit,
) {
    val done = phase as? SpeedTestPhase.Done
    PunktfunkDialog(
        title = "Network speed test",
        onDismiss = onDismiss,
        // Measuring bursts traffic for two seconds; a tap outside must not abandon it midway.
        dismissOnOutsideTap = phase !is SpeedTestPhase.Measuring,
        actions = buildList {
            if (done != null) {
                add(
                    DialogAction(
                        when (target) {
                            SpeedTestTarget.Global -> "Apply"
                            is SpeedTestTarget.Profile -> "Apply to “${target.profile.name}”"
                            is SpeedTestTarget.Ask -> "Set in “${target.profile.name}”"
                        },
                        primary = true,
                    ) { onApply(true) },
                )
                if (target is SpeedTestTarget.Ask) {
                    add(DialogAction("Set as default") { onApply(false) })
                }
            }
            add(DialogAction("Close", primary = done == null, onClick = onDismiss))
        },
    ) {
        PromptText(hostName)
        when (phase) {
            SpeedTestPhase.Connecting -> PromptText("Connecting…")
            SpeedTestPhase.Measuring ->
                PromptText(
                    "Measuring — the host is bursting test traffic for two seconds.",
                )
            is SpeedTestPhase.Failed -> Text(
                phase.message,
                style = MaterialTheme.typography.bodyMedium,
                color = MaterialTheme.colorScheme.error,
            )
            is SpeedTestPhase.Done -> {
                Text(
                    "%.0f Mbit/s measured · %.1f %% loss".format(phase.measuredMbps, phase.lossPct),
                    style = MaterialTheme.typography.bodyLarge,
                    fontWeight = FontWeight.SemiBold,
                    color = MaterialTheme.colorScheme.onSurface,
                )
                PromptText(
                    "Recommended bitrate: %.0f Mbit/s".format(phase.recommendedMbps),
                )
                PromptText(speedTestTargetNote(target))
            }
        }
    }
}

/** One line saying which layer an Apply will write to, and why that one. */
private fun speedTestTargetNote(target: SpeedTestTarget): String = when (target) {
    SpeedTestTarget.Global ->
        "This host uses the default settings, so the bitrate goes there."
    is SpeedTestTarget.Profile ->
        "This host streams with “${target.profile.name}”, which sets its own bitrate — " +
            "that override is what it actually reads."
    is SpeedTestTarget.Ask ->
        "This host streams with “${target.profile.name}”, which currently inherits the default " +
            "bitrate. Setting it in the profile affects only this host's profile; setting it as " +
            "the default affects everything that inherits it."
}
