package com.ponzischeme89.memby.ui

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
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.IntrinsicSize
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.LazyListState
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.DoneAll
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.FavoriteBorder
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.Tv
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
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.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
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.tv.material3.Icon
import androidx.tv.material3.Text
import com.ponzischeme89.memby.ServiceLocator
import com.ponzischeme89.memby.data.model.BaseItem
import com.ponzischeme89.memby.data.model.MediaRating
import com.ponzischeme89.memby.ui.detail.DetailZone
import com.ponzischeme89.memby.ui.detail.SeasonMarker
import com.ponzischeme89.memby.ui.detail.SeasonProgress
import com.ponzischeme89.memby.ui.detail.episodeAfter
import com.ponzischeme89.memby.ui.detail.episodeEyebrow
import com.ponzischeme89.memby.ui.detail.episodeHeadline
import com.ponzischeme89.memby.ui.detail.episodesForSeason
import com.ponzischeme89.memby.ui.detail.detailPositions
import com.ponzischeme89.memby.ui.detail.heroFacts
import com.ponzischeme89.memby.ui.detail.playbackProgress
import com.ponzischeme89.memby.ui.detail.primaryActionLabel
import com.ponzischeme89.memby.ui.detail.remainingLabel
import com.ponzischeme89.memby.ui.detail.seasonMarkers
import com.ponzischeme89.memby.ui.detail.seasonProgressLabel
import com.ponzischeme89.memby.ui.detail.seriesEpisodeComparator
import com.ponzischeme89.memby.ui.detail.seriesProgressLabel
import kotlinx.coroutines.delay

/**
 * A single episode, opened from Continue Watching or from a show's episode list.
 *
 * It is the same page as a movie's and a series' — [DetailPageScaffold], the same hero, the
 * same fold — with the two substitutions an episode needs. The logo belongs to the *series*,
 * so the season and episode number go directly under it with the episode's own title
 * beneath. And where a movie has tabs, this has the season scroller: the one thing worth
 * navigating from an episode is the rest of the show, and it doubles as the answer to "where
 * am I up to", which is the question somebody resuming a series actually has.
 */
@Composable
fun EpisodeDetailsOverlay(
    item: BaseItem,
    onPlay: (BaseItem) -> Unit,
    onToggleFavorite: (BaseItem, Boolean) -> Unit,
    onTogglePlayed: (BaseItem, Boolean) -> Unit,
    onClose: () -> Unit,
    onOpenItem: (BaseItem) -> Unit = {},
    modifier: Modifier = Modifier,
) {
    val repository = ServiceLocator.repository
    val settings by repository.settingsFlow.collectAsState(initial = repository.currentSettings)
    var episodes by remember(item.seriesId) { mutableStateOf<List<BaseItem>?>(null) }
    var loadFailed by remember(item.seriesId) { mutableStateOf(false) }
    var ratings by remember(item.id) { mutableStateOf<List<MediaRating>>(emptyList()) }

    // Keyed on the series, not the episode: walking from one episode's page to the next
    // within the same show must not re-fetch the whole catalogue of it.
    LaunchedEffect(item.seriesId) {
        val seriesId = item.seriesId
        if (seriesId.isNullOrBlank()) {
            episodes = emptyList()
            return@LaunchedEffect
        }
        runCatching { repository.getSeriesEpisodes(seriesId) }
            .onSuccess { episodes = it.sortedWith(seriesEpisodeComparator) }
            .onFailure {
                loadFailed = true
                episodes = emptyList()
            }
    }
    LaunchedEffect(item.id, settings.showRatingsStrip) {
        ratings = if (settings.showRatingsStrip) repository.getRatings(item) else emptyList()
    }

    EpisodeDetailContent(
        item = item,
        episodes = episodes,
        loadFailed = loadFailed,
        onPlay = onPlay,
        onToggleFavorite = onToggleFavorite,
        onTogglePlayed = onTogglePlayed,
        onOpenItem = onOpenItem,
        ratings = ratings,
        showRatingsStrip = settings.showRatingsStrip,
        modifier = modifier,
    )
}

/** [episodes] is null while the series' episode list is still coming. */
@Composable
internal fun EpisodeDetailContent(
    item: BaseItem,
    episodes: List<BaseItem>?,
    loadFailed: Boolean,
    onPlay: (BaseItem) -> Unit,
    onToggleFavorite: (BaseItem, Boolean) -> Unit,
    onTogglePlayed: (BaseItem, Boolean) -> Unit,
    modifier: Modifier = Modifier,
    ratings: List<MediaRating> = emptyList(),
    showRatingsStrip: Boolean = true,
    onOpenItem: (BaseItem) -> Unit = {},
) {
    val currentSeason = item.parentIndexNumber
    val markers = remember(episodes, currentSeason) {
        seasonMarkers(episodes.orEmpty(), currentSeason)
    }
    // The scroller opens on the season this episode is in and stays wherever the viewer
    // moves it. Selecting a season browses; it never changes which episode the page is about.
    var selectedSeason by remember(item.id) { mutableStateOf(currentSeason) }
    LaunchedEffect(markers) {
        if (markers.none { it.season == selectedSeason }) {
            selectedSeason = currentSeason ?: markers.firstOrNull()?.season
        }
    }
    val seasonEpisodes = remember(episodes, selectedSeason) {
        episodesForSeason(episodes.orEmpty(), selectedSeason)
    }
    val upNext = remember(episodes, item.id) { episodeAfter(episodes.orEmpty(), item) }
    val seriesProgress = remember(markers, currentSeason) {
        seriesProgressLabel(markers, currentSeason)
    }

    val play = remember(item.id) { FocusRequester() }
    val firstEpisode = remember(item.id) { FocusRequester() }
    val emptyPane = remember(item.id) { FocusRequester() }
    val seasonRequesters = remember(markers) { markers.associate { it.season to FocusRequester() } }
    val episodeListState = rememberLazyListState()
    val seasonListState = rememberLazyListState()

    // Every focus target has to be attached on the frame the press lands. While the episode
    // request is in flight there are no cards and no chips, so both directions resolve to
    // whatever is actually placed and the rest falls back to ordinary focus search.
    val hasEpisodeCards = seasonEpisodes.isNotEmpty()
    val contentEntry = if (hasEpisodeCards) firstEpisode else emptyPane
    // The strip's entry point is the selected season's own stop, not a requester of the
    // scaffold's — until the episode list lands there is no stop to land on, and Down from
    // Play has to fall back to ordinary focus search rather than throw.
    val selectedSeasonChip = seasonRequesters[selectedSeason]
    val stripEntry = selectedSeasonChip ?: FocusRequester.Default

    LaunchedEffect(markers, selectedSeason) {
        val index = markers.indexOfFirst { it.season == selectedSeason }
        if (index >= 0) runCatching { seasonListState.scrollToItem(index) }
    }
    // Open the list on the episode the page is about rather than at the top of the season.
    // Episode 9 of a twelve-part season is otherwise nine presses below the fold, on the
    // one screen where the viewer has already said which episode they mean.
    LaunchedEffect(seasonEpisodes, item.id) {
        val index = seasonEpisodes.indexOfFirst { it.id == item.id }
        if (index > 0) runCatching { episodeListState.scrollToItem(index) }
    }

    RestoreDetailFocus(
        itemId = item.id,
        zone = DetailZone.PLAY,
        play = play,
        tabStrip = stripEntry,
        related = firstEpisode,
        relatedReady = hasEpisodeCards,
        content = contentEntry,
        contentReady = true,
    )

    var confirmation by remember(item.id) { mutableStateOf<String?>(null) }
    LaunchedEffect(confirmation) {
        if (confirmation != null) {
            delay(1_800L)
            confirmation = null
        }
    }

    DetailPageScaffold(
        item = item,
        facts = heroFacts(item),
        badges = mediaBadges(item),
        eyebrow = episodeEyebrow(item),
        subtitle = item.name.takeIf(String::isNotBlank),
        title = item.seriesName?.takeIf(String::isNotBlank) ?: item.name,
        playLabel = primaryActionLabel(item),
        onPlay = { onPlay(item) },
        playFocusRequester = play,
        tabFocusRequester = stripEntry,
        contentFocusRequester = contentEntry,
        modifier = modifier,
        progress = playbackProgress(item),
        progressLabel = remainingLabel(item),
        // An episode inherits no explanation of its own; what the page can say about what
        // follows is more use than a taste the engine learned about the show.
        reasons = listOfNotNull(upNext?.let { "Up next  ${episodeHeadline(it)}" }),
        ratings = ratings,
        showRatingsStrip = showRatingsStrip,
        confirmation = confirmation,
        onZoneFocused = { zone -> detailPositions.update(item.id) { it.copy(zone = zone) } },
        strip = { _, _, onFocused ->
            SeasonScroller(
                markers = markers,
                selectedSeason = selectedSeason,
                currentSeason = currentSeason,
                seriesProgress = seriesProgress,
                selectedFocusRequester = selectedSeasonChip,
                seasonFocusRequesters = seasonRequesters,
                listState = seasonListState,
                onSelect = { selectedSeason = it },
                onFocused = onFocused,
            )
        },
        heroActions = buildList {
            add(
                DetailHeroAction(
                    // A heart, not a tick: "Mark watched" beside it is a tick as well. The
                    // heart is what a home card and the screensaver already use.
                    icon = if (item.isFavorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder,
                    description = if (item.isFavorite) "Remove from Favourites" else "Add to Favourites",
                    active = item.isFavorite,
                    onClick = {
                        val desired = !item.isFavorite
                        onToggleFavorite(item, desired)
                        confirmation = if (desired) "Added to Favourites" else "Removed from Favourites"
                    },
                ),
            )
            add(
                DetailHeroAction(
                    icon = Icons.Default.DoneAll,
                    description = if (item.userData?.played == true) "Mark unwatched" else "Mark watched",
                    active = item.userData?.played == true,
                    onClick = { onTogglePlayed(item, item.userData?.played != true) },
                ),
            )
            // The show this episode belongs to, so the full series page is one press away
            // rather than a walk back through Continue Watching.
            item.seriesId?.takeIf(String::isNotBlank)?.let { seriesId ->
                add(
                    DetailHeroAction(
                        icon = Icons.Default.Tv,
                        description = "Open ${item.seriesName ?: "the series"}",
                        onClick = {
                            onOpenItem(
                                BaseItem(
                                    id = seriesId,
                                    name = item.seriesName.orEmpty(),
                                    type = "Series",
                                ),
                            )
                        },
                    ),
                )
            }
        },
    ) {
        EpisodeSeasonPane(
            episodes = episodes,
            seasonEpisodes = seasonEpisodes,
            currentEpisodeId = item.id,
            loadFailed = loadFailed,
            firstEpisodeFocusRequester = firstEpisode,
            emptyFocusRequester = emptyPane,
            aboveEpisodes = stripEntry,
            listState = episodeListState,
            onPlay = onPlay,
        )
    }
}

/**
 * The band under the hero: every season the library holds, with the viewer's place in the
 * show marked on it.
 *
 * A season behind the one being watched is dimmed and ticked. That is the whole point of
 * the component — it answers "where am I up to" at a glance, which is what somebody
 * resuming a long show is actually asking, and it does it without a tab strip's promise
 * that each stop is a different *kind* of content.
 */
@Composable
private fun SeasonScroller(
    markers: List<SeasonMarker>,
    selectedSeason: Int?,
    currentSeason: Int?,
    seriesProgress: String?,
    selectedFocusRequester: FocusRequester?,
    seasonFocusRequesters: Map<Int, FocusRequester>,
    listState: LazyListState,
    onSelect: (Int) -> Unit,
    onFocused: () -> Unit,
) {
    Box(
        Modifier
            .fillMaxWidth()
            .height(DetailStripHeight)
            .background(DetailBackground)
            .padding(horizontal = DetailSideGutter),
    ) {
        Box(Modifier.align(Alignment.BottomStart).fillMaxWidth().height(1.dp).background(DetailHairline))
        Row(
            modifier = Modifier.fillMaxSize(),
            verticalAlignment = Alignment.Bottom,
        ) {
            if (markers.isEmpty()) {
                Text(
                    text = "Loading seasons…",
                    color = DetailQuietText,
                    fontSize = 13.sp,
                    modifier = Modifier.weight(1f).padding(bottom = 16.dp),
                )
            } else {
                LazyRow(
                    state = listState,
                    // Up and Down out of the band belong to the scaffold, which knows
                    // where to fall back to when the exact stop it wants is not composed
                    // — a season chip scrolled out of this row is an ordinary state.
                    modifier = Modifier
                        .weight(1f)
                        .fillMaxSize()
                        .focusGroup(),
                    horizontalArrangement = Arrangement.spacedBy(28.dp),
                    verticalAlignment = Alignment.Bottom,
                ) {
                    items(markers, key = { it.season }) { marker ->
                        SeasonStop(
                            marker = marker,
                            selected = marker.season == selectedSeason,
                            watching = marker.season == currentSeason,
                            focusRequester = if (marker.season == selectedSeason) {
                                selectedFocusRequester
                            } else {
                                seasonFocusRequesters[marker.season]
                            },
                            onSelect = { onSelect(marker.season) },
                            onFocused = onFocused,
                        )
                    }
                }
            }
            seriesProgress?.let {
                Text(
                    text = it,
                    color = DetailQuietText,
                    fontSize = 12.sp,
                    maxLines = 1,
                    overflow = TextOverflow.Ellipsis,
                    modifier = Modifier.padding(start = 18.dp, bottom = 15.dp),
                )
            }
            // Same caption on the Down key the tab strip carries; never focusable.
            Icon(
                Icons.Default.KeyboardArrowDown,
                contentDescription = null,
                tint = DetailQuietText,
                modifier = Modifier.padding(start = 12.dp, bottom = 14.dp).size(18.dp),
            )
        }
    }
}

@Composable
private fun SeasonStop(
    marker: SeasonMarker,
    selected: Boolean,
    watching: Boolean,
    focusRequester: FocusRequester?,
    onSelect: () -> Unit,
    onFocused: () -> Unit,
) {
    var focused by remember(marker.season) { mutableStateOf(false) }
    val done = marker.progress == SeasonProgress.WATCHED
    val labelColour = when {
        selected || focused -> Color.White
        // Behind the viewer: stated, not hidden. The tick beside it is what says why it
        // is quieter than the rest, or a dimmed season reads as one that failed to load.
        done -> DetailQuietText.copy(alpha = 0.55f)
        else -> DetailQuietText
    }
    Column(
        modifier = Modifier
            .width(IntrinsicSize.Max)
            .then(if (focusRequester != null) Modifier.focusRequester(focusRequester) else Modifier)
            .testTag("season-stop-${marker.season}")
            .onFocusChanged {
                focused = it.isFocused
                if (it.isFocused) { onSelect(); onFocused() }
            }
            .clickable(onClick = onSelect),
        horizontalAlignment = Alignment.Start,
    ) {
        Row(
            verticalAlignment = Alignment.CenterVertically,
            modifier = Modifier.padding(start = 4.dp, end = 4.dp),
        ) {
            if (done) {
                Icon(
                    Icons.Default.Check,
                    contentDescription = "Watched",
                    tint = if (selected || focused) DetailAccent else DetailAccent.copy(alpha = 0.6f),
                    modifier = Modifier.size(14.dp).padding(end = 1.dp),
                )
                Spacer(Modifier.width(5.dp))
            }
            Text(
                text = marker.label,
                color = labelColour,
                fontSize = 15.sp,
                fontWeight = if (selected || focused) FontWeight.Bold else FontWeight.Medium,
                maxLines = 1,
            )
            if (watching) {
                Spacer(Modifier.width(7.dp))
                Text(
                    text = "WATCHING",
                    color = DetailAccent,
                    fontSize = 9.sp,
                    fontWeight = FontWeight.Bold,
                    letterSpacing = 0.8.sp,
                    maxLines = 1,
                    modifier = Modifier
                        .background(DetailAccent.copy(alpha = 0.16f), androidx.compose.foundation.shape.RoundedCornerShape(4.dp))
                        .padding(horizontal = 5.dp, vertical = 2.dp),
                )
            }
        }
        seasonProgressLabel(marker)?.let {
            Text(
                text = it,
                color = if (done) DetailQuietText.copy(alpha = 0.5f) else DetailQuietText,
                fontSize = 10.sp,
                maxLines = 1,
                modifier = Modifier.padding(start = 4.dp, end = 4.dp, top = 2.dp, bottom = 11.dp),
            )
        }
        Box(
            Modifier.fillMaxWidth().height(3.dp).background(
                if (selected || focused) DetailAccent else Color.Transparent,
            ),
        )
    }
}

/**
 * The pane under the scroller: the selected season's episodes, this one flagged.
 *
 * Internal so a screenshot test can render it at [detailPaneHeight] on its own — it lives
 * below the fold, where a capture of the whole page shows nothing but its top edge.
 */
@Composable
internal fun EpisodeSeasonPane(
    episodes: List<BaseItem>?,
    seasonEpisodes: List<BaseItem>,
    currentEpisodeId: String,
    loadFailed: Boolean,
    firstEpisodeFocusRequester: FocusRequester,
    emptyFocusRequester: FocusRequester,
    aboveEpisodes: FocusRequester,
    listState: LazyListState,
    onPlay: (BaseItem) -> Unit,
) {
    when {
        episodes == null -> DetailFocusablePane(emptyFocusRequester) {
            Text("Loading episodes…", color = DetailQuietText, fontSize = 15.sp)
        }
        loadFailed -> DetailFocusablePane(emptyFocusRequester) {
            Text("Episodes are temporarily unavailable.", color = DetailQuietText, fontSize = 15.sp)
        }
        seasonEpisodes.isEmpty() -> DetailFocusablePane(emptyFocusRequester) {
            Text("No episodes are available for this season.", color = DetailQuietText, fontSize = 15.sp)
        }
        else -> LazyColumn(
            state = listState,
            verticalArrangement = Arrangement.spacedBy(10.dp),
            contentPadding = PaddingValues(end = 12.dp, bottom = 18.dp),
            modifier = Modifier.fillMaxSize(),
        ) {
            itemsIndexed(seasonEpisodes, key = { _, episode -> episode.id }) { index, episode ->
                EpisodeCard(
                    episode = episode,
                    onClick = { onPlay(episode) },
                    seasonFocusRequester = aboveEpisodes,
                    isFirst = index == 0,
                    isCurrent = episode.id == currentEpisodeId,
                    modifier = if (index == 0) {
                        Modifier.focusRequester(firstEpisodeFocusRequester)
                    } else {
                        Modifier
                    },
                )
            }
        }
    }
}
