package com.fabledsword.minstrel.shared.widgets

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.windowInsetsBottomHeight
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
import com.fabledsword.minstrel.cache.mutations.OfflineWriteHintViewModel
import com.fabledsword.minstrel.connectivity.ui.ConnectionErrorBanner
import com.fabledsword.minstrel.player.ui.MiniPlayer
import com.fabledsword.minstrel.player.ui.PlaybackErrorViewModel
import com.fabledsword.minstrel.update.ui.UpdateBanner
import com.fabledsword.minstrel.update.ui.VersionTooOldBanner
import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsViewModel

/**
 * `contentWindowInsets` for a screen's own Scaffold when that screen
 * renders inside [ShellScaffold].
 *
 * The shell owns the window insets for in-shell routes: it consumes the
 * status bar at the top and holds the navigation-bar space at the
 * bottom, below the MiniPlayer. A screen Scaffold's default
 * `contentWindowInsets` is the raw system bars, and Scaffold reads that
 * value directly (`contentWindowInsets.asPaddingValues()`) rather than
 * through the consumption-aware modifier chain — so the shell consuming
 * the inset does NOT reach it. Left at the default, every in-shell
 * screen pads its content up by the nav-bar height a second time: the
 * dead strip between the last list row and the MiniPlayer.
 *
 * Full-screen routes (NowPlaying / Queue / Login / ServerUrl) keep the
 * default — no shell sits above them, so their Scaffold is the only
 * thing that can inset them.
 */
val ShellContentWindowInsets: WindowInsets = WindowInsets(0, 0, 0, 0)

/**
 * Outer shell wrapper for "in-app" routes — banners on top
 * (conditional, none implemented yet), routed screen filling the
 * middle, MiniPlayer pinned at the bottom (auto-hides when no track).
 * A shell-scoped SnackbarHost above the MiniPlayer surfaces transient
 * messages emitted by in-screen TrackActions kebabs (each screen owns
 * its own Scaffold's snackbar host as well; this one is the shell-
 * wide catch-all for events that outlive a single screen).
 *
 * Mirrors the Flutter `_ShellWithPlayerBar` (lib/shared/routing.dart).
 *
 * Top-level routes (Home / Library / Search / Discover / Playlists /
 * Settings / Admin / detail screens) wrap themselves with this.
 * Full-screen routes (NowPlaying / Queue / ServerUrl / Login)
 * intentionally do NOT — they fill the entire screen, often with a
 * back-only AppBar.
 *
 * [onExpandPlayer] is the navigate-to-NowPlaying callback passed
 * through to MiniPlayer's tap-handler. No NavHostController parameter
 * because nothing inside the shell currently navigates - if a banner
 * (UpdateBanner / VersionTooOldBanner / ConnectionErrorBanner) ever
 * grows tap-to-navigate, plumb a NavHostController in at that point.
 */
@Composable
fun ShellScaffold(
    onExpandPlayer: () -> Unit,
    modifier: Modifier = Modifier,
    trackActionsViewModel: TrackActionsViewModel = hiltViewModel(),
    playbackErrorViewModel: PlaybackErrorViewModel = hiltViewModel(),
    offlineWriteHintViewModel: OfflineWriteHintViewModel = hiltViewModel(),
    content: @Composable () -> Unit,
) {
    val snackbarHostState = remember { SnackbarHostState() }
    LaunchedEffect(Unit) {
        trackActionsViewModel.transientMessages.collect { msg ->
            snackbarHostState.showSnackbar(msg)
        }
    }
    LaunchedEffect(Unit) {
        playbackErrorViewModel.messages.collect { msg ->
            snackbarHostState.showSnackbar(msg)
        }
    }
    LaunchedEffect(Unit) {
        offlineWriteHintViewModel.messages.collect { msg ->
            snackbarHostState.showSnackbar(msg)
        }
    }
    // Consume the status-bar inset once here so the banner stack sits
    // below the status bar (mirrors Flutter's SafeArea(bottom:false)).
    // statusBarsPadding consumes the inset for descendants, so the in-
    // shell screens' app bars no longer re-pad for the status bar —
    // avoiding a double inset while fixing banners drawing under it.
    Column(modifier = modifier.fillMaxSize().statusBarsPadding()) {
        // Banner slot. VersionTooOldBanner appears when the server's
        // min_client_version exceeds our build; UpdateBanner softly
        // nudges an available (newer-than-installed) APK; and
        // ConnectionErrorBanner shows when the device loses usable
        // internet.
        VersionTooOldBanner()
        UpdateBanner()
        ConnectionErrorBanner()
        // The window is edge-to-edge, so something has to own the
        // bottom navigation-bar inset, and the shell claims it once
        // here: the content region consumes it so nothing inside a
        // screen pads for it a second time (see
        // [ShellContentWindowInsets] for the Scaffold half of that —
        // Scaffold reads its insets outside the consumption chain), and
        // the Spacer below re-holds the space for the system bar.
        // Un-owned, the inset re-appears inside each screen as a dead
        // strip between the last list row and the MiniPlayer.
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .weight(1f)
                .consumeWindowInsets(WindowInsets.navigationBars),
        ) {
            content()
        }
        SnackbarHost(hostState = snackbarHostState)
        MiniPlayer(
            onExpandClick = onExpandPlayer,
        )
        // Unconditional: the MiniPlayer renders nothing when no track
        // is loaded, and the content region has already given the inset
        // up, so this is what keeps a fresh install's last row off the
        // gesture pill.
        Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars))
    }
}
