package com.ponzischeme89.memby.ui

import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
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.FirstPage
import androidx.compose.material.icons.filled.Movie
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.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import com.ponzischeme89.memby.ServiceLocator
import com.ponzischeme89.memby.data.RelatedContent
import com.ponzischeme89.memby.data.model.BaseItem
import com.ponzischeme89.memby.data.model.MediaRating
import com.ponzischeme89.memby.ui.detail.DetailTab
import com.ponzischeme89.memby.ui.detail.DetailZone
import com.ponzischeme89.memby.ui.detail.creditRows
import com.ponzischeme89.memby.ui.detail.detailPositions
import com.ponzischeme89.memby.ui.detail.detailTab
import com.ponzischeme89.memby.ui.detail.detailTabs
import com.ponzischeme89.memby.ui.detail.franchiseStart
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.technicalSpecs
import kotlinx.coroutines.delay

/**
 * A movie, or any single playable item that is not a series.
 *
 * Everything structural lives in [DetailPageScaffold]; this file only decides which tabs a
 * movie has and what goes in each one.
 */
@Composable
fun MediaDetailsOverlay(
    item: BaseItem,
    onPlay: (BaseItem) -> Unit,
    onToggleFavorite: (BaseItem, Boolean) -> Unit,
    onTogglePlayed: (BaseItem, Boolean) -> Unit,
    onClose: () -> Unit,
    onOpenItem: (BaseItem) -> Unit = {},
    modifier: Modifier = Modifier,
) {
    val settings by ServiceLocator.repository.settingsFlow
        .collectAsState(initial = ServiceLocator.repository.currentSettings)
    var related by remember(item.id) { mutableStateOf<RelatedContent?>(null) }
    var trailer by remember(item.id) { mutableStateOf<BaseItem?>(null) }
    var ratings by remember(item.id) { mutableStateOf<List<MediaRating>>(emptyList()) }
    LaunchedEffect(item.id) {
        related = ServiceLocator.repository.getRelated(item)
    }
    LaunchedEffect(item.id) {
        trailer = ServiceLocator.repository.getLocalTrailer(item.id)
    }
    LaunchedEffect(item.id, settings.showRatingsStrip) {
        ratings = if (settings.showRatingsStrip) ServiceLocator.repository.getRatings(item) else emptyList()
    }
    MediaDetailContent(
        item = item,
        onPlay = onPlay,
        onToggleFavorite = onToggleFavorite,
        onTogglePlayed = onTogglePlayed,
        onOpenItem = onOpenItem,
        related = related,
        trailer = trailer,
        ratings = ratings,
        showRatingsStrip = settings.showRatingsStrip,
        hideWatchedMovies = settings.hideWatchedMovies,
        modifier = modifier,
    )
}

/**
 * The layout, with everything it renders as a parameter — so it can be screenshotted and
 * previewed without a repository behind it. [related] is null while it is still coming.
 */
@Composable
internal fun MediaDetailContent(
    item: BaseItem,
    onPlay: (BaseItem) -> Unit,
    onToggleFavorite: (BaseItem, Boolean) -> Unit,
    onTogglePlayed: (BaseItem, Boolean) -> Unit,
    modifier: Modifier = Modifier,
    related: RelatedContent? = null,
    trailer: BaseItem? = null,
    ratings: List<MediaRating> = emptyList(),
    showRatingsStrip: Boolean = true,
    hideWatchedMovies: Boolean = false,
    onOpenItem: (BaseItem) -> Unit = {},
) {
    val specs = remember(item.id, item.mediaStreams) { technicalSpecs(item) }
    val credits = remember(item.id, item.people, item.genres) { creditRows(item) }
    val visibleRelated = remember(related, hideWatchedMovies) {
        visibleWithWatchedPreference(related?.items.orEmpty(), hideWatchedMovies)
    }
    val badges = remember(item.id, item.mediaStreams) { mediaBadges(item) }
    val franchise = remember(item.id, item.collectionName, related?.items) {
        franchiseStart(item, related?.items.orEmpty())
    }
    val tabs = remember(item.id) { detailTabs(isSeries = false) }
    // The item arrives with whatever a home row asked for and is replaced by the full
    // record moments later. Panes say "loading" rather than "nothing here" until then.
    val detailsLoaded = item.people.isNotEmpty() || item.mediaStreams.isNotEmpty()

    val remembered = remember(item.id) { detailPositions.get(item.id) }
    // Every newly opened title starts as a complete hero frame. Remembering a content
    // tab also restored its focus and caused the page to reopen below the artwork.
    var tabKey by remember(item.id) { mutableStateOf(DetailTab.OVERVIEW.key) }
    val selectedTab = detailTab(tabKey, tabs)

    val play = remember(item.id) { FocusRequester() }
    val tabStrip = remember(item.id) { FocusRequester() }
    // One requester per pane. Sharing a single "information pane" requester between the
    // Overview and Cast & Details panes attached it to two nodes at once for the 80ms
    // AnimatedContent spends fading the outgoing pane out, and a Down press landing in that
    // window could request focus on the pane that is disappearing.
    val overviewPane = remember(item.id) { FocusRequester() }
    val castPane = remember(item.id) { FocusRequester() }
    val firstRelated = remember(item.id) { FocusRequester() }
    val relatedListState = rememberLazyListState(remembered.relatedIndex)
    val contentEntry = when (selectedTab) {
        DetailTab.MORE_LIKE_THIS -> firstRelated
        DetailTab.CAST_DETAILS -> castPane
        else -> overviewPane
    }

    DetailPositionMemory(
        itemId = item.id,
        tabKey = tabKey,
        relatedIndex = { relatedListState.firstVisibleItemIndex },
    )
    RestoreDetailFocus(
        itemId = item.id,
        zone = DetailZone.PLAY,
        play = play,
        tabStrip = tabStrip,
        related = firstRelated,
        relatedReady = visibleRelated.isNotEmpty(),
        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 = badges + listOfNotNull(airingBadgeLabel(item)),
        tabs = tabs,
        selectedTab = selectedTab,
        onSelectTab = { tabKey = it.key },
        playLabel = primaryActionLabel(item),
        onPlay = { onPlay(item) },
        playFocusRequester = play,
        tabFocusRequester = tabStrip,
        contentFocusRequester = contentEntry,
        modifier = modifier,
        progress = playbackProgress(item),
        progressLabel = remainingLabel(item),
        reasons = related?.reasons?.takeIf(List<String>::isNotEmpty)
            ?: listOfNotNull(item.membyRecommendationReason?.takeIf(String::isNotBlank)),
        ratings = ratings,
        showRatingsStrip = showRatingsStrip,
        confirmation = confirmation,
        onZoneFocused = { zone -> detailPositions.update(item.id) { it.copy(zone = zone) } },
        heroActions = buildList {
            add(
                DetailHeroAction(
                    // A heart, not a tick: "Mark watched" two buttons along 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"
                    },
                ),
            )
            trailer?.let {
                add(DetailHeroAction(Icons.Default.Movie, "Play trailer", onClick = { onPlay(it) }))
            }
            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) },
                ),
            )
            // Appended because related content arrives asynchronously. Existing action
            // indices (and therefore their attached FocusRequesters) must not move.
            franchise?.takeIf { it.firstMovie.id != item.id }?.let { start ->
                add(
                    DetailHeroAction(
                        icon = Icons.Default.FirstPage,
                        description = "Open the first ${start.name} movie, ${start.firstMovie.name}",
                        label = "Start with ${start.firstMovie.name}",
                        onClick = { onOpenItem(start.firstMovie) },
                    ),
                )
            }
        },
    ) { visibleTab ->
        when (visibleTab) {
            DetailTab.OVERVIEW -> DetailOverviewPane(item, credits, overviewPane)
            DetailTab.MORE_LIKE_THIS -> DetailMoreLikeThisPane(
                items = visibleRelated,
                loading = related == null,
                onSelect = onOpenItem,
                firstFocusRequester = firstRelated,
                listState = relatedListState,
            )
            DetailTab.CAST_DETAILS -> DetailCastAndDetailsPane(
                item = item,
                credits = credits,
                specs = specs,
                detailsLoaded = detailsLoaded,
                focusRequester = castPane,
            )
            DetailTab.EPISODES -> Unit
        }
    }
}

/**
 * Writes the page's position back to [detailPositions] as it changes.
 *
 * Kept out of the pages themselves because both need exactly this and getting it slightly
 * different in two places is how "it remembered last time" bugs start. The zone is written
 * by the scaffold's focus callbacks; this covers the two things focus does not report.
 */
@Composable
internal fun DetailPositionMemory(
    itemId: String,
    tabKey: String,
    season: Int? = null,
    episodeIndex: () -> Int = { 0 },
    relatedIndex: () -> Int = { 0 },
) {
    LaunchedEffect(itemId, tabKey, season) {
        detailPositions.update(itemId) { it.copy(tabKey = tabKey, season = season ?: it.season) }
    }
    LaunchedEffect(itemId) {
        snapshotFlow { episodeIndex() to relatedIndex() }.collect { (episode, relatedCard) ->
            detailPositions.update(itemId) {
                it.copy(episodeIndex = episode, relatedIndex = relatedCard)
            }
        }
    }
}

/**
 * Puts focus back where the viewer left it.
 *
 * Play is focused first regardless: it exists on the first frame, so the remote is live
 * while the rest of the page is still arriving. Only then does focus move on to the band
 * the viewer was actually in — and only if that band has something to land on, because a
 * request against an unplaced [FocusRequester] throws.
 */
@Composable
internal fun RestoreDetailFocus(
    itemId: String,
    zone: DetailZone,
    play: FocusRequester,
    tabStrip: FocusRequester,
    related: FocusRequester,
    relatedReady: Boolean,
    content: FocusRequester? = null,
    contentReady: Boolean = false,
) {
    LaunchedEffect(itemId) {
        delay(32L)
        runCatching { play.requestFocus() }
    }
    // One shot. The readiness flags flip when the network lands, and re-running then would
    // haul focus out from under a viewer who has already started moving around the page.
    var restored by remember(itemId) { mutableStateOf(zone == DetailZone.PLAY) }
    LaunchedEffect(itemId, relatedReady, contentReady) {
        if (restored) return@LaunchedEffect
        val target = when (zone) {
            DetailZone.PLAY -> null
            DetailZone.TABS -> tabStrip
            DetailZone.CONTENT -> content?.takeIf { contentReady }
            DetailZone.RELATED -> related.takeIf { relatedReady }
        } ?: return@LaunchedEffect
        // After Play, so the restore lands second and wins; a frame either way is
        // invisible, but the order is not.
        delay(96L)
        if (runCatching { target.requestFocus() }.isSuccess) restored = true
    }
}
