package com.ponzischeme89.memby.ui

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.InfiniteRepeatableSpec
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
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.fillMaxWidth
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.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.zIndex
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.tv.material3.Text
import com.ponzischeme89.memby.ServiceLocator
import com.ponzischeme89.memby.data.EmbyOutage
import kotlinx.coroutines.delay
import kotlin.math.ceil

private val OutageRed = Color(0xFFD6403A)
private val OutageTitle = Color(0xFFFFF1F0)
private val OutageBody = Color(0xFFF2C9C6)

/**
 * Slightly taller than the news bar, because unlike that one this does not go away by
 * itself and has a second line to carry. Still a strip rather than a panel: it appears
 * over playback too, and whatever it says it is covering somebody's film.
 */
private val BannerHeight = 60.dp

/** The same broadcast overscan inset the alert bar uses, so the two line up. */
private val SafeAreaHorizontal = 48.dp

/**
 * A red bar across the top of the screen saying Emby is not answering, with the time until
 * the next attempt.
 *
 * It exists because of one specific asymmetry: video direct-plays from Emby while
 * everything else comes from the gateway, so when Emby goes quiet the film stops and
 * nothing on screen explains it — and the gateway is still up and able to say why. The
 * counterpart alert in [ServiceAlertBanner] announces the *moment* it happened; this is
 * the standing state, which is what a television switched on midway through an outage
 * needs instead.
 *
 * Never focusable, and never dismissable. There is nothing for the viewer to do, and a bar
 * that stole D-pad focus mid-browse would be worse than the outage it is reporting. It
 * disappears on its own when Emby answers.
 */
@Composable
fun EmbyOutageBanner(suppressed: Boolean = false, modifier: Modifier = Modifier) {
    // Collected here rather than passed in, for the reason the whole UI-conventions note
    // gives: read one scope up and every poll recomposes the launcher.
    val current by ServiceLocator.maintenance.embyOutage.collectAsStateWithLifecycle()
    val outage = current?.takeUnless { suppressed }

    // Hold the last value so the slide-out has something to draw.
    var lastOutage by remember { mutableStateOf<EmbyOutage?>(null) }
    if (outage != null) lastOutage = outage

    AnimatedVisibility(
        visible = outage != null,
        enter = slideInVertically(tween(520, easing = FastOutSlowInEasing)) { -it } +
            fadeIn(tween(420)),
        // Leaves faster than it arrives, and for a happier reason: Emby came back.
        exit = slideOutVertically(tween(320, easing = FastOutSlowInEasing)) { -it } +
            fadeOut(tween(240)),
        // Above the news bar (8f) — an outage outranks an announcement — and below the
        // mandatory update screen, which owns the whole display.
        modifier = modifier.zIndex(9f),
    ) {
        lastOutage?.let { OutageBanner(it) }
    }
}

/**
 * Internal so previews and the screenshot test can render the bar without the drop-in
 * wrapper, whose entire job is an animation a still frame says nothing about.
 */
@Composable
internal fun OutageBanner(outage: EmbyOutage, nowMillis: () -> Long = { elapsedRealtime() }) {
    // One tick a second, not a frame clock: this is a number that changes once a second
    // and animating it would recompose the bar sixty times for every time it changes.
    val remaining by produceState(
        initialValue = secondsUntil(outage.nextAttemptAtMillis, nowMillis()),
        outage.nextAttemptAtMillis,
    ) {
        while (true) {
            value = secondsUntil(outage.nextAttemptAtMillis, nowMillis())
            delay(1_000L)
        }
    }
    Column(Modifier.fillMaxWidth()) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .height(BannerHeight)
                .background(
                    // Red, and deepest under the text. Over a paused film this bar has to
                    // supply its own contrast rather than borrow the picture's.
                    Brush.horizontalGradient(
                        0f to Color(0xFF7A1512),
                        0.55f to Color(0xF08E1A16),
                        1f to Color(0xD9601210),
                    ),
                )
                .padding(horizontal = SafeAreaHorizontal),
            verticalAlignment = Alignment.CenterVertically,
        ) {
            OutagePulse()
            Spacer(Modifier.width(16.dp))
            Box(Modifier.width(3.dp).height(30.dp).background(OutageRed))
            Spacer(Modifier.width(14.dp))
            Column(Modifier.weight(1f)) {
                Text(
                    "Emby Server failed to respond",
                    color = OutageTitle,
                    fontSize = 10.sp,
                    fontWeight = FontWeight.Bold,
                    letterSpacing = 1.6.sp,
                    maxLines = 1,
                )
                Spacer(Modifier.height(3.dp))
                Text(
                    "Memby failed to connect to the Emby instance. Playback unavailable.",
                    color = OutageTitle,
                    fontSize = 14.sp,
                    fontWeight = FontWeight.SemiBold,
                    maxLines = 1,
                    overflow = TextOverflow.Ellipsis,
                )
            }
            Spacer(Modifier.width(16.dp))
            // Read directly rather than through an animated float: this changes once a
            // second, so the recomposition it costs is one per second of a 60dp bar.
            Text(
                retryLabel(remaining),
                color = OutageBody,
                fontSize = 12.sp,
                fontWeight = FontWeight.Medium,
                maxLines = 1,
            )
        }

        // The same rule-and-fade the news bar ends on, so the two read as one system.
        Box(
            Modifier
                .fillMaxWidth()
                .height(1.dp)
                .background(
                    Brush.horizontalGradient(
                        listOf(OutageRed, OutageRed.copy(alpha = 0.35f), Color.Transparent),
                    ),
                ),
        )
        Box(
            Modifier
                .fillMaxWidth()
                .height(10.dp)
                .background(Brush.verticalGradient(listOf(Color(0x77000000), Color.Transparent))),
        )
    }
}

/**
 * A slow pulse in place of the Emby mark the news bar shows. It says "still trying"
 * without a spinner, which on a television reads as the app being busy rather than the
 * server being away.
 *
 * The animated value is read inside the [Canvas] lambda and never in the composable body —
 * read it here and this whole bar would recompose every frame, permanently, for as long as
 * the outage lasts. That is the one thing this app cannot afford on a weak TV box.
 */
@Composable
private fun OutagePulse() {
    val transition = rememberInfiniteTransition(label = "outage-pulse")
    val pulse = transition.animateFloat(
        initialValue = 0.35f,
        targetValue = 1f,
        animationSpec = InfiniteRepeatableSpec(
            animation = tween(1_400, easing = FastOutSlowInEasing),
            repeatMode = RepeatMode.Reverse,
        ),
        label = "outage-pulse-alpha",
    )
    Canvas(Modifier.size(14.dp)) {
        drawCircle(color = OutageRed.copy(alpha = pulse.value), radius = size.minDimension / 2f)
    }
}

/** Whole seconds until the next attempt, never negative. */
internal fun secondsUntil(deadlineMillis: Long, nowMillis: Long): Int =
    ceil((deadlineMillis - nowMillis).coerceAtLeast(0L) / 1000.0).toInt()

/**
 * The countdown's wording. It says what is about to happen rather than counting for its
 * own sake — the only useful thing a viewer can take from this bar is that something is
 * still trying and roughly when.
 */
internal fun retryLabel(secondsRemaining: Int): String = when {
    secondsRemaining <= 0 -> "Retrying now…"
    secondsRemaining == 1 -> "Retrying in 1s"
    else -> "Retrying in ${secondsRemaining}s"
}

private fun elapsedRealtime(): Long = android.os.SystemClock.elapsedRealtime()

@TvPreview
@Composable
private fun EmbyOutageBannerPreview() {
    PreviewSurface(alignment = Alignment.TopCenter) {
        OutageBanner(
            EmbyOutage(nextAttemptAtMillis = 42_000L, retryIntervalSeconds = 60),
            nowMillis = { 0L },
        )
    }
}

/** The last second before the next attempt, which is a different string. */
@TvPreview
@Composable
private fun EmbyOutageBannerRetryingPreview() {
    PreviewSurface(alignment = Alignment.TopCenter) {
        OutageBanner(
            EmbyOutage(nextAttemptAtMillis = 0L, retryIntervalSeconds = 60),
            nowMillis = { 0L },
        )
    }
}
