package dev.castarr.tv.ui

import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
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.fillMaxHeight
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.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.clipPath
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.focus.onFocusChanged
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.lifecycle.compose.collectAsStateWithLifecycle
import coil.compose.SubcomposeAsyncImage
import androidx.compose.ui.layout.ContentScale
import androidx.tv.material3.Border
import androidx.tv.material3.ClickableSurfaceDefaults
import androidx.tv.material3.Surface
import androidx.tv.material3.Text
import dev.castarr.tv.AppState
import dev.castarr.tv.data.NowNext
import dev.castarr.tv.data.isEpgPlaceholder
import dev.castarr.tv.playlist.Channel
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale

/**
 * Live tab: group rail on the left (focusing a group opens it — exclusive,
 * like a menu), channels of the open group with Now/Next on the right.
 */
@Composable
fun LiveScreen(state: AppState) {
    val genericChannels by state.source.channels.collectAsStateWithLifecycle()
    val dispatcharrChannels by state.dispatcharr.channels.collectAsStateWithLifecycle()
    val favorites by state.dispatcharr.favorites.collectAsStateWithLifecycle()
    // Re-render Now/Next when a new EPG arrives.
    val genericEpg by state.source.epgUpdatedAt.collectAsStateWithLifecycle()
    val dispatcharrEpg by state.dispatcharr.epgUpdatedAt.collectAsStateWithLifecycle()
    val isDispatcharr = state.sourceMode == AppState.SourceMode.DISPATCHARR
    val epgStamp = if (isDispatcharr) dispatcharrEpg else genericEpg
    val allChannels = if (isDispatcharr) dispatcharrChannels else genericChannels
    val groups = remember(allChannels) {
        allChannels.mapNotNull { it.group.ifEmpty { null } }.distinct().sorted()
    }
    val teams = remember(state.enabledTeams) {
        dev.castarr.tv.data.TeamFilters.all.filter { it.key in state.enabledTeams }
    }
    // Recomputed when the EPG or the channel list changes — scanning the
    // sport groups' programmes is cheap, but not per frame.
    val matchesByTeam = remember(teams, allChannels, epgStamp) {
        teams.associate { it.key to state.teamMatches(it.key) }
    }
    val activeTeam = teams.firstOrNull { it.key == state.activeTeam }
    val activeMatches = activeTeam?.let { matchesByTeam[it.key] }.orEmpty()
    val teamHits = remember(activeMatches) { activeMatches.associate { it.first.url to it.second } }
    val channels = when {
        activeTeam != null -> activeMatches.map { it.first }
        state.favoritesOnly -> allChannels.filter { it.backendId in favorites }
        state.groupFilter != null -> allChannels.filter { it.group == state.groupFilter }
        else -> allChannels
    }

    if (allChannels.isEmpty()) {
        Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
            Text(
                "Noch keine Sender — richte unter Einstellungen eine Quelle ein.",
                color = CastarrColors.faint,
                fontFamily = AppFont,
                fontSize = 15.sp,
            )
        }
        return
    }

    // D-pad choreography: left from any channel row lands on the SELECTED
    // group (not the spatially nearest one), right from the rail enters the
    // list at the top — which also gets scrolled up on every group change.
    val railFocus = remember { FocusRequester() }
    val listFocus = remember { FocusRequester() }
    val restoreFocus = remember { FocusRequester() }
    val listState = rememberLazyListState()
    // Index of the channel the viewer last watched, so leaving playback
    // returns them to where they were instead of the top of the list.
    val restoreIndex = state.lastWatched?.let { watched ->
        channels.indexOfFirst { it.url == watched.url }.takeIf { it >= 0 }
    }
    var restored by remember { mutableStateOf(false) }
    LaunchedEffect(state.groupFilter, state.favoritesOnly) {
        // A fresh group starts at the top; a return from playback does not.
        if (restoreIndex == null) listState.scrollToItem(0)
    }
    LaunchedEffect(restoreIndex, channels.size, state.restorePending) {
        if (restoreIndex != null && (state.restorePending || !restored)) {
            listState.scrollToItem(restoreIndex)
            runCatching { restoreFocus.requestFocus() }
            restored = true
            state.restorePending = false
        }
    }
    // A FocusRequester that is not currently attached throws when used, and
    // in a LazyColumn the selected row may well be scrolled out of
    // composition — so jumps are attempted, not declared.
    val intoList = if (channels.isEmpty()) Modifier else Modifier.onPreviewKeyEvent { event ->
        event.type == KeyEventType.KeyDown && event.key == Key.DirectionRight &&
            runCatching { listFocus.requestFocus() }.isSuccess
    }

    val railState = rememberLazyListState()
    Row(Modifier.fillMaxSize()) {
        LazyColumn(
            state = railState,
            modifier = Modifier
                .width(264.dp)
                .fillMaxHeight(),
            contentPadding = androidx.compose.foundation.layout.PaddingValues(
                start = 28.dp, end = 12.dp, top = 4.dp, bottom = 32.dp
            ),
            verticalArrangement = Arrangement.spacedBy(2.dp),
        ) {
            item {
                val selected = !state.favoritesOnly && state.groupFilter == null &&
                    state.activeTeam == null
                GroupItem(
                    label = "Alle Sender",
                    count = allChannels.size,
                    selected = selected,
                    modifier = intoList.then(
                        if (selected) Modifier.focusRequester(railFocus) else Modifier
                    ),
                    suppressAutoSelect = { state.restorePending },
                ) {
                    state.activeTeam = null
                    state.favoritesOnly = false
                    state.groupFilter = null
                }
            }
            if (isDispatcharr) {
                item {
                    GroupItem(
                        label = "★ Favoriten",
                        count = favorites.size,
                        selected = state.favoritesOnly && state.activeTeam == null,
                        modifier = intoList.then(
                            if (state.favoritesOnly) Modifier.focusRequester(railFocus) else Modifier
                        ),
                        suppressAutoSelect = { state.restorePending },
                    ) {
                        state.activeTeam = null
                        state.favoritesOnly = true
                        state.groupFilter = null
                    }
                }
            }
            items(teams, key = { it.key }) { club ->
                GroupItem(
                    label = club.label,
                    count = matchesByTeam[club.key]?.size ?: 0,
                    selected = state.activeTeam == club.key,
                    leading = { Crest(club) },
                    modifier = intoList.then(
                        if (state.activeTeam == club.key) Modifier.focusRequester(railFocus)
                        else Modifier
                    ),
                    suppressAutoSelect = { state.restorePending },
                ) {
                    state.activeTeam = club.key
                    state.favoritesOnly = false
                    state.groupFilter = null
                }
            }
            item {
                Box(
                    Modifier
                        .padding(horizontal = 14.dp, vertical = 8.dp)
                        .fillMaxWidth()
                        .height(1.dp)
                        .background(CastarrColors.line)
                )
            }
            items(groups, key = { it }) { group ->
                GroupItem(
                    label = group,
                    count = remember(allChannels, group) { allChannels.count { it.group == group } },
                    selected = state.groupFilter == group && state.activeTeam == null,
                    modifier = intoList.then(
                        if (state.groupFilter == group) Modifier.focusRequester(railFocus) else Modifier
                    ),
                    suppressAutoSelect = { state.restorePending },
                ) {
                    state.activeTeam = null
                    state.favoritesOnly = false
                    state.groupFilter = group
                }
            }
        }

        LazyColumn(
            state = listState,
            modifier = Modifier
                .weight(1f)
                .fillMaxHeight(),
            contentPadding = androidx.compose.foundation.layout.PaddingValues(
                start = 8.dp, end = 40.dp, top = 4.dp, bottom = 32.dp
            ),
            verticalArrangement = Arrangement.spacedBy(6.dp),
        ) {
            if (channels.isEmpty()) {
                item {
                    Text(
                        when {
                            activeTeam != null ->
                                "In den nächsten 3 Stunden läuft nichts mit ${activeTeam.fullName}."
                            state.favoritesOnly ->
                                "Noch keine Favoriten — halte OK auf einem Sender gedrückt."
                            else -> "Diese Gruppe ist leer."
                        },
                        color = CastarrColors.faint,
                        fontFamily = AppFont,
                        fontSize = 14.sp,
                        modifier = Modifier.padding(24.dp),
                    )
                }
            }
            itemsIndexed(channels, key = { _, c -> c.url + c.name }) { listIndex, channel ->
                ChannelRow(
                    channel = channel,
                    // Stable channel number = position in "Alle Sender",
                    // matching the remote's number pad in every view.
                    number = allChannels.indexOf(channel) + 1,
                    modifier = Modifier
                        .onPreviewKeyEvent { event ->
                            event.type == KeyEventType.KeyDown &&
                                event.key == Key.DirectionLeft &&
                                runCatching { railFocus.requestFocus() }.isSuccess
                        }
                        .then(if (listIndex == 0) Modifier.focusRequester(listFocus) else Modifier)
                        .then(
                            if (listIndex == restoreIndex) Modifier.focusRequester(restoreFocus)
                            else Modifier
                        ),
                    nowNext = state.nowNext(channel),
                    highlight = teamHits[channel.url],
                    playing = state.currentChannel?.url == channel.url,
                    // All rows are favorites in the favorites view — the
                    // star only carries meaning elsewhere.
                    favorite = isDispatcharr && !state.favoritesOnly &&
                        channel.backendId in favorites,
                    epgStamp = epgStamp,
                    onLongClick = if (isDispatcharr) {
                        { state.dispatcharr.toggleFavorite(channel) }
                    } else null,
                ) { state.play(channel) }
            }
        }
    }
}

@Composable
private fun GroupItem(
    label: String,
    count: Int,
    selected: Boolean,
    modifier: Modifier = Modifier,
    leading: (@Composable () -> Unit)? = null,
    suppressAutoSelect: () -> Boolean = { false },
    onSelect: () -> Unit,
) {
    Surface(
        // Focusing a group opens it right away (browse like a menu);
        // clicking is not required, but harmless.
        onClick = onSelect,
        modifier = modifier
            .fillMaxWidth()
            .onFocusChanged { if (it.isFocused && !suppressAutoSelect()) onSelect() },
        shape = ClickableSurfaceDefaults.shape(RoundedCornerShape(10.dp)),
        scale = ClickableSurfaceDefaults.scale(focusedScale = 1f),
        colors = ClickableSurfaceDefaults.colors(
            containerColor = if (selected) CastarrColors.accentDim else Color.Transparent,
            contentColor = if (selected) CastarrColors.accent else CastarrColors.muted,
            focusedContainerColor = CastarrColors.accent,
            focusedContentColor = CastarrColors.onAccent,
        ),
    ) {
        Row(
            modifier = Modifier.padding(horizontal = 14.dp, vertical = 9.dp),
            verticalAlignment = Alignment.CenterVertically,
        ) {
            if (leading != null) {
                leading()
                Spacer(Modifier.width(10.dp))
            }
            Text(
                label,
                fontFamily = AppFont,
                fontSize = 14.sp,
                fontWeight = if (selected) FontWeight.Medium else FontWeight.Normal,
                maxLines = 1,
                overflow = TextOverflow.Ellipsis,
                modifier = Modifier.weight(1f),
            )
            Spacer(Modifier.width(8.dp))
            Text(
                "$count",
                fontFamily = AppFont,
                fontSize = 12.sp,
            )
        }
    }
}

@Composable
private fun ChannelRow(
    channel: Channel,
    number: Int,
    modifier: Modifier = Modifier,
    nowNext: NowNext,
    highlight: dev.castarr.tv.data.Programme? = null,
    playing: Boolean,
    favorite: Boolean,
    epgStamp: Long,
    onLongClick: (() -> Unit)?,
    onClick: () -> Unit,
) {
    Surface(
        onClick = onClick,
        onLongClick = onLongClick,
        modifier = modifier.fillMaxWidth(),
        shape = ClickableSurfaceDefaults.shape(RoundedCornerShape(12.dp)),
        // No focus scale — grown rows get clipped at the pane edges.
        scale = ClickableSurfaceDefaults.scale(focusedScale = 1f),
        colors = ClickableSurfaceDefaults.colors(
            containerColor = if (playing) CastarrColors.accentDim else CastarrColors.surface,
            contentColor = CastarrColors.fg,
            focusedContainerColor = CastarrColors.surfaceFocused,
            focusedContentColor = CastarrColors.fg,
        ),
        border = ClickableSurfaceDefaults.border(
            focusedBorder = Border(
                border = BorderStroke(2.dp, CastarrColors.accent),
                shape = RoundedCornerShape(12.dp),
            ),
        ),
    ) {
        Row(
            modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp),
            verticalAlignment = Alignment.CenterVertically,
        ) {
            Text(
                "$number",
                color = if (playing) CastarrColors.accent else CastarrColors.faint,
                fontFamily = AppFont,
                fontSize = 14.sp,
                fontWeight = FontWeight.Medium,
                modifier = Modifier.width(40.dp),
            )
            LogoTile(channel)
            Spacer(Modifier.width(14.dp))
            Row(
                verticalAlignment = Alignment.CenterVertically,
                modifier = Modifier.width(230.dp),
            ) {
                Text(
                    channel.name,
                    color = if (playing) CastarrColors.accent else CastarrColors.fg,
                    fontFamily = AppFont,
                    fontSize = 16.sp,
                    fontWeight = if (playing) FontWeight.Medium else FontWeight.Normal,
                    maxLines = 1,
                    overflow = TextOverflow.Ellipsis,
                    modifier = Modifier.weight(1f, fill = false),
                )
                if (favorite) {
                    Spacer(Modifier.width(8.dp))
                    Text("★", color = CastarrColors.accent, fontSize = 13.sp)
                }
            }
            Spacer(Modifier.width(24.dp))
            if (highlight != null) {
                // In the club view the matching broadcast is the point, not
                // whatever happens to be running.
                HighlightCell(highlight, Modifier.weight(1f))
            } else {
                EpgCell(nowNext, Modifier.weight(1f))
            }
            if (playing) {
                Spacer(Modifier.width(12.dp))
                Box(
                    Modifier
                        .size(7.dp)
                        .clip(CircleShape)
                        .background(CastarrColors.accent)
                )
            }
        }
    }
}

/**
 * Stand-in for the club badge: real crests are trademarks, so this draws a
 * simple shield in the club colours instead.
 */
@Composable
private fun Crest(team: dev.castarr.tv.data.TeamFilter) {
    Canvas(Modifier.size(18.dp)) {
        val w = size.width
        val h = size.height
        val shield = Path().apply {
            moveTo(w * 0.5f, 0f)
            lineTo(w, h * 0.18f)
            lineTo(w, h * 0.55f)
            cubicTo(w, h * 0.82f, w * 0.75f, h * 0.95f, w * 0.5f, h)
            cubicTo(w * 0.25f, h * 0.95f, 0f, h * 0.82f, 0f, h * 0.55f)
            lineTo(0f, h * 0.18f)
            close()
        }
        drawPath(shield, team.primary)
        clipPath(shield) {
            drawRect(
                team.secondary,
                topLeft = Offset(0f, h * 0.42f),
                size = androidx.compose.ui.geometry.Size(w, h * 0.16f),
            )
        }
    }
}

/** Bare channel logo (they ship transparent); initials as fallback. */
@Composable
private fun LogoTile(channel: Channel) {
    val initials = channel.name.split(" ").filter { it.isNotBlank() }
        .take(2).map { it.first() }.joinToString("").uppercase()
    Box(Modifier.size(width = 52.dp, height = 40.dp), contentAlignment = Alignment.Center) {
        if (channel.logo.isNotEmpty()) {
            SubcomposeAsyncImage(
                model = channel.logo,
                contentDescription = null,
                contentScale = ContentScale.Fit,
                modifier = Modifier.fillMaxSize(),
                loading = { LogoInitials(initials) },
                error = { LogoInitials(initials) },
            )
        } else {
            LogoInitials(initials)
        }
    }
}

@Composable
private fun LogoInitials(initials: String) {
    Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        Text(
            initials,
            color = CastarrColors.faint,
            fontFamily = AppFont,
            fontSize = 12.sp,
            fontWeight = FontWeight.SemiBold,
        )
    }
}

/** The programme that matched the club filter, with its start time. */
@Composable
private fun HighlightCell(programme: dev.castarr.tv.data.Programme, modifier: Modifier = Modifier) {
    val running = System.currentTimeMillis() in programme.start until programme.stop
    Column(modifier) {
        Row(verticalAlignment = Alignment.Bottom) {
            Text(
                programme.title,
                color = CastarrColors.fg,
                fontFamily = AppFont,
                fontSize = 14.sp,
                maxLines = 1,
                overflow = TextOverflow.Ellipsis,
                modifier = Modifier.weight(1f, fill = false),
            )
            Spacer(Modifier.width(12.dp))
            Text(
                if (running) "läuft" else "ab ${formatClock(programme.start)}",
                color = if (running) CastarrColors.accent else CastarrColors.muted,
                fontFamily = AppFont,
                fontSize = 12.sp,
                fontWeight = if (running) FontWeight.SemiBold else FontWeight.Normal,
            )
        }
        Spacer(Modifier.height(6.dp))
        Text(
            "${formatClock(programme.start)}–${formatClock(programme.stop)}",
            color = CastarrColors.faint,
            fontFamily = AppFont,
            fontSize = 12.sp,
        )
    }
}

private fun formatClock(millis: Long): String =
    SimpleDateFormat("HH:mm", Locale.GERMANY).format(Date(millis))

/**
 * Left-aligned EPG column with a fixed leading edge: title + times, progress
 * under the title, next programme only when it differs (design review P1/P2).
 */
@Composable
private fun EpgCell(nowNext: NowNext, modifier: Modifier = Modifier) {
    val now = nowNext.now?.takeUnless { isEpgPlaceholder(it.title) }
    Column(modifier) {
        if (now != null) {
            Row(verticalAlignment = Alignment.Bottom) {
                Text(
                    now.title,
                    color = CastarrColors.fg,
                    fontFamily = AppFont,
                    fontSize = 14.sp,
                    maxLines = 1,
                    overflow = TextOverflow.Ellipsis,
                    modifier = Modifier.weight(1f, fill = false),
                )
                Spacer(Modifier.width(12.dp))
                Text(
                    "${formatClock(now.start)}–${formatClock(now.stop)}",
                    color = CastarrColors.faint,
                    fontFamily = AppFont,
                    fontSize = 12.sp,
                )
            }
            val fraction = ((System.currentTimeMillis() - now.start).toFloat() /
                (now.stop - now.start).coerceAtLeast(1)).coerceIn(0f, 1f)
            Spacer(Modifier.height(6.dp))
            Box(
                Modifier
                    .fillMaxWidth()
                    .height(2.dp)
                    .clip(RoundedCornerShape(1.dp))
                    .background(CastarrColors.line)
            ) {
                Box(
                    Modifier
                        .fillMaxWidth(fraction)
                        .height(2.dp)
                        .background(CastarrColors.accent)
                )
            }
            val next = nowNext.next?.takeUnless {
                isEpgPlaceholder(it.title) || it.title == now.title
            }
            if (next != null) {
                Spacer(Modifier.height(5.dp))
                Text(
                    "danach: ${next.title}",
                    color = CastarrColors.faint,
                    fontFamily = AppFont,
                    fontSize = 12.sp,
                    maxLines = 1,
                    overflow = TextOverflow.Ellipsis,
                )
            }
        }
    }
}
