package com.ponzischeme89.memby.ui

import android.os.Build
import androidx.activity.compose.BackHandler
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDownward
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.tv.material3.Icon
import androidx.tv.material3.Text
import com.ponzischeme89.memby.data.model.GatewayUpdate
import com.ponzischeme89.memby.update.AppInstall
import com.ponzischeme89.memby.update.InstallPermissionRequiredException
import com.ponzischeme89.memby.update.UpdateChecker
import kotlinx.coroutines.launch
import com.ponzischeme89.memby.ui.theme.MembyAccent
import com.ponzischeme89.memby.ui.theme.MembySurface

private val UpdateAccent: Color get() = MembyAccent
private val UpdateTitle = Color(0xFFF2F5F7)
private val UpdateBody = Color(0xFFAEB7BF)
private val UpdateFaint = Color(0xFFA2ADB5)

/**
 * The app-level update gate. AppRoot composes this instead of login, profiles, or Home,
 * so no focused media card or playback action exists behind its buttons.
 *
 * A **mandatory** update covers everything and cannot be dismissed: Back is swallowed and
 * there is one button. The operator has decided this build may no longer be used, so
 * leaving a way past it would defeat the point. An **optional** one is the same panel with
 * a "Later" button.
 *
 * The instructions matter more than the visuals here: on a TV, the system installer looks
 * like a permission prompt the viewer has never seen, so the copy says what will happen
 * before it happens.
 */
@Composable
fun UpdateScreen(
    update: GatewayUpdate,
    onDismiss: () -> Unit,
    modifier: Modifier = Modifier,
) {
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    val scope = rememberCoroutineScope()
    val checker = remember { UpdateChecker(context) }

    var installing by remember { mutableStateOf(false) }
    var message by remember { mutableStateOf<String?>(null) }
    var waitingForInstallPermission by remember { mutableStateOf(false) }

    fun beginInstall() {
        if (installing) return
        installing = true
        message = null
        scope.launch {
            val result = checker.downloadAndInstall(
                apkUrl = update.downloadUrl,
                token = "",
                expectedVersion = update.version,
                expectedSHA256 = update.sha256,
                expectedSizeBytes = update.sizeBytes,
            )
            installing = false
            waitingForInstallPermission =
                result.exceptionOrNull() is InstallPermissionRequiredException
            message = result.exceptionOrNull()?.message ?: "Opening the installer…"
        }
    }

    // The install happens in the system installer, so its outcome arrives here rather than
    // as the result of the call above. On a mandatory update this is the only thing that
    // can tell a viewer why the screen they cannot leave is still there.
    LaunchedEffect(Unit) { AppInstall.messages.collect { message = it } }

    // Android's permission screen pauses Memby. Once the viewer grants permission and
    // returns, continue automatically instead of making them discover they must press
    // Update now for a second time.
    DisposableEffect(lifecycleOwner, waitingForInstallPermission, update.version) {
        val observer = LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_RESUME &&
                waitingForInstallPermission &&
                (Build.VERSION.SDK_INT < Build.VERSION_CODES.O ||
                    context.packageManager.canRequestPackageInstalls())
            ) {
                waitingForInstallPermission = false
                beginInstall()
            }
        }
        lifecycleOwner.lifecycle.addObserver(observer)
        onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
    }

    val primaryFocus = remember { FocusRequester() }
    val laterFocus = remember { FocusRequester() }
    LaunchedEffect(update.version) { runCatching { primaryFocus.requestFocus() } }

    // Swallow Back entirely while an update is required. For an optional prompt, Back is
    // the natural "later".
    BackHandler(enabled = true) { if (!update.isMandatory) onDismiss() }

    val transition = rememberInfiniteTransition(label = "update")
    val drift by transition.animateFloat(
        initialValue = 0f,
        targetValue = 1f,
        animationSpec = infiniteRepeatable(tween(7_000, easing = LinearEasing), RepeatMode.Reverse),
        label = "drift",
    )
    val bob by transition.animateFloat(
        initialValue = 0f,
        targetValue = 1f,
        animationSpec = infiniteRepeatable(tween(1_800, easing = LinearEasing), RepeatMode.Reverse),
        label = "bob",
    )

    var entered by remember { mutableStateOf(false) }
    LaunchedEffect(Unit) { entered = true }
    val entrance by animateFloatAsState(
        targetValue = if (entered) 1f else 0f,
        animationSpec = tween(360),
        label = "update-entrance",
    )

    Box(
        modifier = modifier
            .fillMaxSize()
            // Opaque, not a scrim: a required update is not a dialog over usable content.
            .background(MembySurface),
    ) {
        Canvas(Modifier.fillMaxSize()) {
            val centre = Offset(size.width * (0.5f + 0.06f * drift), size.height * 0.34f)
            val radius = size.minDimension * 0.62f
            drawCircle(
                brush = Brush.radialGradient(
                    colors = listOf(UpdateAccent.copy(alpha = 0.14f), Color.Transparent),
                    center = centre,
                    radius = radius,
                ),
                radius = radius,
                center = centre,
            )
        }

        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(horizontal = 60.dp)
                .graphicsLayer {
                    alpha = entrance
                    translationY = (1f - entrance) * 22.dp.toPx()
                },
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center,
        ) {
            Box(
                modifier = Modifier
                    .size(92.dp)
                    .graphicsLayer { translationY = -6.dp.toPx() * bob }
                    .clip(CircleShape)
                    .background(Color(0xFF16202A))
                    .border(1.dp, UpdateAccent.copy(alpha = 0.4f), CircleShape),
                contentAlignment = Alignment.Center,
            ) {
                Icon(
                    imageVector = Icons.Default.ArrowDownward,
                    contentDescription = null,
                    tint = UpdateAccent,
                    modifier = Modifier.size(40.dp),
                )
            }

            Spacer(Modifier.height(26.dp))
            Text(
                if (update.isMandatory) "Time to update Memby" else "A new version of Memby is ready",
                color = UpdateTitle,
                fontSize = 32.sp,
                fontWeight = FontWeight.SemiBold,
                textAlign = TextAlign.Center,
            )

            Spacer(Modifier.height(10.dp))
            Text(
                if (update.isMandatory) {
                    "Version ${update.version} is required to keep watching. It only takes a moment."
                } else {
                    "Version ${update.version} is available."
                },
                color = UpdateBody,
                fontSize = 17.sp,
                textAlign = TextAlign.Center,
                modifier = Modifier.widthIn(max = 640.dp),
            )

            if (update.notes.isNotBlank()) {
                Spacer(Modifier.height(14.dp))
                Text(
                    update.notes,
                    color = UpdateFaint,
                    fontSize = 15.sp,
                    textAlign = TextAlign.Center,
                    modifier = Modifier.widthIn(max = 560.dp),
                )
            }

            Spacer(Modifier.height(22.dp))
            Text(
                // Said before it happens, because the system installer is unfamiliar and
                // easy to back out of by accident.
                "Choose Update now — Memby downloads the new version, then your TV asks you " +
                    "to confirm the install. If it asks permission to install apps, allow it and " +
                    "the update continues. Your profiles and sign-in stay on this TV.",
                color = UpdateFaint,
                fontSize = 14.sp,
                textAlign = TextAlign.Center,
                modifier = Modifier.widthIn(max = 600.dp),
            )

            Spacer(Modifier.height(30.dp))
            Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
                UpdateButton(
                    label = if (installing) "Downloading…" else "Update now",
                    primary = true,
                    enabled = !installing,
                    onClick = ::beginInstall,
                    modifier = Modifier
                        .focusRequester(primaryFocus)
                        .focusProperties {
                            if (!update.isMandatory) right = laterFocus
                        },
                )
                if (!update.isMandatory) {
                    UpdateButton(
                        label = "Not now",
                        primary = false,
                        enabled = true,
                        onClick = onDismiss,
                        modifier = Modifier
                            .focusRequester(laterFocus)
                            .focusProperties { left = primaryFocus },
                    )
                }
            }

            message?.let {
                Spacer(Modifier.height(18.dp))
                Text(it, color = UpdateFaint, fontSize = 14.sp, textAlign = TextAlign.Center)
            }

            if (update.isMandatory) {
                Spacer(Modifier.height(22.dp))
                Text(
                    "Stuck? Ask whoever set up Memby for you.",
                    color = UpdateFaint.copy(alpha = 0.75f),
                    fontSize = 13.sp,
                )
            } else {
                Spacer(Modifier.height(18.dp))
                Text(
                    "Press Back or choose Not now to dismiss this update.",
                    color = UpdateFaint.copy(alpha = 0.82f),
                    fontSize = 13.sp,
                )
            }
        }
    }
}

@Composable
internal fun UpdateButton(
    label: String,
    primary: Boolean,
    enabled: Boolean,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
) {
    var focused by remember { mutableStateOf(false) }
    val scale by animateFloatAsState(
        targetValue = if (focused) 1.06f else 1f,
        animationSpec = tween(120),
        label = "update-button-scale",
    )

    Box(
        modifier = modifier
            .graphicsLayer { scaleX = scale; scaleY = scale; alpha = if (enabled) 1f else 0.6f }
            .clip(RoundedCornerShape(10.dp))
            .background(
                when {
                    focused && primary -> UpdateAccent
                    focused -> Color(0xFF2A343F)
                    primary -> Color(0xFF2C6A29)
                    else -> Color(0xFF1E2833)
                },
            )
            .border(
                width = if (focused) 0.dp else 1.dp,
                color = Color.White.copy(alpha = 0.12f),
                shape = RoundedCornerShape(10.dp),
            )
            .onFocusChanged { focused = it.isFocused }
            // Some Android TV launchers/remotes do not turn a Foundation click target's
            // centre key into a click consistently. Consume the activation keys here and
            // invoke once on key-up, while retaining clickable for accessibility/pointer
            // input.
            .onPreviewKeyEvent { event ->
                val native = event.nativeKeyEvent
                val activationKey =
                    native.keyCode == android.view.KeyEvent.KEYCODE_DPAD_CENTER ||
                        native.keyCode == android.view.KeyEvent.KEYCODE_ENTER ||
                        native.keyCode == android.view.KeyEvent.KEYCODE_NUMPAD_ENTER
                if (!activationKey) {
                    false
                } else {
                    if (enabled && native.action == android.view.KeyEvent.ACTION_UP) {
                        onClick()
                    }
                    true
                }
            }
            .focusable(interactionSource = remember { MutableInteractionSource() })
            .clickable(enabled = enabled, onClick = onClick)
            .padding(horizontal = 30.dp, vertical = 13.dp),
    ) {
        Text(
            label,
            color = if (focused && primary) Color(0xFF06240A) else UpdateTitle,
            fontSize = 16.sp,
            fontWeight = FontWeight.SemiBold,
        )
    }
}
