package com.ponzischeme89.memby.ui

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.Animatable
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.Image
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.fillMaxSize
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.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
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.tv.material3.Text
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.ponzischeme89.memby.R
import com.ponzischeme89.memby.ServiceLocator
import com.ponzischeme89.memby.data.MaintenanceMonitor
import com.ponzischeme89.memby.data.ServiceAlert
import kotlin.math.ceil
import com.ponzischeme89.memby.ui.theme.MembyAccent

private val AlertAccent: Color get() = MembyAccent
private val AlertTitle = Color(0xFFF2F5F7)
private val AlertBody = Color(0xFFC3CBD2)

/**
 * Height of the bar itself, before the rule and fade that blend it into the screen.
 *
 * Kept to a strip rather than a panel because this now appears over playback as well as
 * over the launcher: whatever it says, it is covering somebody's film while it says it.
 */
private val BannerHeight = 48.dp

/**
 * Broadcast-style overscan inset. TVs crop the edges of the picture by a few percent, so
 * nothing meaningful may sit closer than this to the frame.
 */
private val SafeAreaHorizontal = 48.dp

/**
 * A full-width bar that drops in over the top of the screen to say an episode has aired —
 * the shape a broadcaster uses for an in-programme notice, rather than a corner toast.
 *
 * Deliberately not focusable and not dismissable by D-pad: it must never steal focus from
 * a row mid-browse, so it times itself out instead of asking the viewer to act. It spans
 * the navigation rail as well, because for its few seconds it is the top layer of the
 * screen; it draws above the rows but below the update prompt, which is the one thing
 * allowed to own the whole display.
 */
@Composable
fun ServiceAlertBanner(suppressed: Boolean, modifier: Modifier = Modifier) {
    // The alert is collected *here* rather than passed down from the home composable.
    // Read one scope up and every arriving alert would recompose the entire launcher
    // body; read here and it recomposes a bar that is usually not even on screen.
    val current by ServiceLocator.maintenance.alert.collectAsStateWithLifecycle()
    val alert = current?.takeUnless { suppressed }

    // Tell the monitor the moment this is really on screen. Until it hears that, the
    // alert is only offered — it starts no dismissal timer and persists nothing — so an
    // alert arriving behind the screensaver or another app is not silently used up.
    LaunchedEffect(alert?.id) {
        alert?.id?.let { ServiceLocator.maintenance.alertShown(it) }
    }

    // The exit animation still needs something to draw, so hold the last alert until the
    // slide-out has finished with it.
    var lastAlert by remember { mutableStateOf<ServiceAlert?>(null) }
    if (alert != null) lastAlert = alert

    AnimatedVisibility(
        visible = alert != null,
        // In from above the frame, out the same way, both unhurried: over a film the
        // arrival is the intrusion, so it eases in rather than snapping down. Still
        // slower arriving than leaving — showing up should be noticed, going away
        // should not.
        enter = slideInVertically(tween(680, easing = FastOutSlowInEasing)) { -it } +
            fadeIn(tween(520)),
        exit = slideOutVertically(tween(420, easing = FastOutSlowInEasing)) { -it } +
            fadeOut(tween(300)),
        modifier = modifier.zIndex(8f),
    ) {
        lastAlert?.let { AlertBanner(it) }
    }
}

// Internal so the screenshot test can render the bar on its own, without the drop-in
// wrapper around it.
@Composable
internal fun AlertBanner(
    alert: ServiceAlert,
    visibleMillis: Long = MaintenanceMonitor.ALERT_VISIBLE_MS,
) {
    // Keyed on the alert id so a second banner arriving restarts the countdown rather
    // than inheriting whatever was left of the first one's.
    val remaining = remember(alert.id) { Animatable(1f) }
    LaunchedEffect(alert.id) {
        remaining.animateTo(0f, tween(visibleMillis.toInt(), easing = LinearEasing))
    }
    // The ring's fraction is handed down as a lambda and the seconds as derived state, so
    // ten seconds of animation costs ~10 recompositions of one number instead of ~600 of
    // this whole bar. On a weak TV box that difference is the feature's entire cost.
    val secondsLeft = remember(alert.id, visibleMillis) {
        derivedStateOf { ceil(remaining.value * visibleMillis / 1000f).toInt() }
    }

    Column(Modifier.fillMaxWidth()) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .height(BannerHeight)
                .background(
                    // Near-black, and darkest at the left where the text sits. Over a
                    // film almost anything can be behind this, so the bar supplies its
                    // own contrast rather than relying on the picture underneath.
                    Brush.horizontalGradient(
                        0f to Color(0xFF04070A),
                        0.55f to Color(0xF504070A),
                        1f to Color(0xE0070B0F),
                    ),
                )
                .padding(horizontal = SafeAreaHorizontal),
            verticalAlignment = Alignment.CenterVertically,
        ) {
            AlertMark()
            Spacer(Modifier.width(14.dp))
            // The same short green rule used by the ten-minute reminder. It makes the
            // alert feel like part of the player instead of a separate notification UI.
            Box(Modifier.width(3.dp).height(26.dp).background(AlertAccent))
            Spacer(Modifier.width(14.dp))
            Text(
                alertLabel(alert.label),
                color = AlertAccent,
                fontSize = 10.sp,
                fontWeight = FontWeight.Bold,
                letterSpacing = 1.6.sp,
                maxLines = 1,
                overflow = TextOverflow.Ellipsis,
            )
            Spacer(Modifier.width(12.dp))
            Text(
                alert.title,
                color = AlertTitle,
                fontSize = 14.sp,
                fontWeight = FontWeight.SemiBold,
                maxLines = 1,
                overflow = TextOverflow.Ellipsis,
            )
            Spacer(Modifier.width(10.dp))
            Text(
                alert.message,
                color = AlertBody,
                fontSize = 12.sp,
                maxLines = 1,
                overflow = TextOverflow.Ellipsis,
                modifier = Modifier.weight(1f),
            )
            Spacer(Modifier.width(16.dp))
            CountdownRing(fraction = { remaining.value }, secondsLeft = secondsLeft)
        }

        // An accent rule under the bar, then a short fade: without them the banner ends
        // on a hard line across the artwork, which reads as a rendering seam.
        Box(
            Modifier
                .fillMaxWidth()
                .height(1.dp)
                .background(
                    Brush.horizontalGradient(
                        listOf(AlertAccent, AlertAccent.copy(alpha = 0.35f), Color.Transparent),
                    ),
                ),
        )
        Box(
            Modifier
                .fillMaxWidth()
                .height(10.dp)
                .background(
                    Brush.verticalGradient(listOf(Color(0x66000000), Color.Transparent)),
                ),
        )
    }
}

/**
 * The eyebrow above the title. The server picks the wording so it can announce something
 * this build has no name for; a bare alert — or one from a server older than the field —
 * keeps the original episode wording, and an over-long label is cut rather than allowed
 * to push the countdown off the bar.
 */
internal fun alertLabel(label: String): String =
    label.trim().ifEmpty { "JUST AIRED" }.take(MaxAlertLabelChars).uppercase()

private const val MaxAlertLabelChars = 24

/**
 * A ring that empties as the banner's time runs out, with the seconds left inside it.
 *
 * This exists to answer "is it about to go, or did it stick?" — the banner cannot be
 * dismissed by remote, so the one thing the viewer can usefully know is how long they
 * have to read it. Drawn in a single Canvas: an arc and a track are cheaper than any
 * progress component, and this animates every frame for ten seconds.
 *
 * [fraction] is a lambda and [secondsLeft] a [State] on purpose. Read either one in a
 * composable body and every frame recomposes; read them inside the draw lambda and in a
 * derived state, and the sweep costs a redraw only.
 */
@Composable
private fun CountdownRing(fraction: () -> Float, secondsLeft: State<Int>) {
    Box(Modifier.size(26.dp), contentAlignment = Alignment.Center) {
        Canvas(Modifier.fillMaxSize()) {
            val stroke = 2.dp.toPx()
            val inset = stroke / 2f
            val arcSize = Size(size.width - stroke, size.height - stroke)
            drawArc(
                color = Color.White.copy(alpha = 0.12f),
                startAngle = 0f,
                sweepAngle = 360f,
                useCenter = false,
                topLeft = Offset(inset, inset),
                size = arcSize,
                style = Stroke(width = stroke, cap = StrokeCap.Round),
            )
            drawArc(
                color = AlertAccent,
                // Twelve o'clock, unwinding clockwise: the direction a clock hand sweeps,
                // so "less arc left" reads as "less time left" without a legend.
                startAngle = -90f,
                sweepAngle = 360f * fraction().coerceIn(0f, 1f),
                useCenter = false,
                topLeft = Offset(inset, inset),
                size = arcSize,
                style = Stroke(width = stroke, cap = StrokeCap.Round),
            )
        }
        Text(
            secondsLeft.value.coerceAtLeast(0).toString(),
            color = AlertBody,
            fontSize = 9.sp,
            fontWeight = FontWeight.Medium,
        )
    }
}

/**
 * The Emby mark, which is what every one of these banners is really speaking for —
 * a new film, a finished refresh, a server that stopped answering.
 *
 * It replaced the item poster deliberately: artwork made each alert look like a different
 * feature, and half of them (a library refresh, an outage) have no artwork to show. One
 * constant mark says "this is your server talking" in the width of a thumbnail.
 */
@Composable
private fun AlertMark() {
    Image(
        painter = painterResource(R.drawable.emby_logo),
        contentDescription = null,
        contentScale = ContentScale.Fit,
        modifier = Modifier
            .width(30.dp)
            .height(26.dp),
    )
}

// Previews render the bar directly rather than through ServiceAlertBanner: the wrapper's
// whole job is the drop-in, and a still frame of an animation in progress says nothing.
// Posters are left null on purpose — the preview has no network, so this is also the
// fallback tile being checked.

@TvPreview
@Composable
private fun ServiceAlertBannerPreview() {
    PreviewSurface(alignment = Alignment.TopCenter) {
        AlertBanner(
            ServiceAlert(
                id = "sonarr:7:42:aired",
                title = "Northbound",
                message = "S02E04 — The Crossing aired at 9:00 PM and will be in Emby soon.",
            ),
        )
    }
}

/** The other news the bar carries: Radarr finished importing a film. */
@TvPreview
@Composable
private fun ServiceAlertBannerMovieAddedPreview() {
    PreviewSurface(alignment = Alignment.TopCenter) {
        AlertBanner(
            ServiceAlert(
                id = "radarr:412:file:9001",
                title = "Mr. Smith Goes to Washington (1939)",
                message = "Mr. Smith Goes to Washington will be available in Emby shortly.",
                label = "NEW MOVIE ADDED",
            ),
        )
    }
}

/** News about the service rather than the catalogue — the kind seen during a film. */
@TvPreview
@Composable
private fun ServiceAlertBannerServerDownPreview() {
    PreviewSurface(alignment = Alignment.TopCenter) {
        AlertBanner(
            ServiceAlert(
                id = "emby:down:1785012345",
                title = "Emby has stopped communicating",
                message = "Playback may stop until it is back. Memby will say when it returns.",
                label = "SERVER NOT RESPONDING",
            ),
        )
    }
}

/** Long strings are common; the single-line ellipsis is the part worth eyeballing. */
@TvPreview
@Composable
private fun ServiceAlertBannerLongTitlePreview() {
    PreviewSurface(alignment = Alignment.TopCenter) {
        AlertBanner(
            ServiceAlert(
                id = "sonarr:9:88:aired",
                title = "A Very Long Programme Title That Will Not Fit On One Line At All",
                message = "S11E03 — The One Where Absolutely Everything Happens At Once " +
                    "aired at 10:30 PM and is downloading now.",
            ),
        )
    }
}
