import 'dart:async';

import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';

import '../focus/focusable_action_bar.dart';
import '../focus/hub_vertical_navigation.dart';
import '../focus/locked_hub_controller.dart';
import '../i18n/strings.g.dart';
import '../media/ids.dart';
import '../media/media_hub.dart';
import '../media/media_item.dart';
import '../mixins/refreshable.dart';
import '../mixins/tab_visibility_aware.dart';
import '../models/catalog/catalog_item.dart';
import '../navigation/main_screen_scope.dart';
import '../providers/catalog_sources_provider.dart';
import '../providers/explore_provider.dart';
import '../services/catalog/catalog_source.dart';
import '../services/settings_service.dart';
import '../utils/platform_detector.dart';
import '../utils/provider_extensions.dart';
import '../widgets/app_icon.dart';
import '../widgets/app_menu.dart';
import '../widgets/catalog_source_logo.dart';
import '../widgets/desktop_app_bar.dart';
import '../widgets/hub_section.dart';
import '../widgets/focusable_popup_menu_button.dart';
import '../widgets/settings_builder.dart';
import '../widgets/toolbar_scrim.dart';
import '../widgets/tv_browse_rail.dart';
import '../widgets/tv_spotlight_scaffold.dart';
import 'catalog_search_screen.dart';
import 'libraries/state_messages.dart';

/// The Explore tab: watchlist + discover rows from the active external
/// catalog source (Trakt). Only mounted when a source is connected (the tab
/// is hidden otherwise, see [NavigationTab.getVisibleTabs]).
class ExploreScreen extends StatefulWidget {
  const ExploreScreen({super.key});

  @override
  State<ExploreScreen> createState() => ExploreScreenState();
}

class ExploreScreenState extends State<ExploreScreen>
    with Refreshable, FullRefreshable, TabVisibilityAware, FocusableTab {
  late ExploreProvider _explore;

  /// Per-row focus keys, keyed by hub id so focus memory survives reloads.
  final Map<String, GlobalKey<HubSectionState>> _hubKeysById = {};
  List<GlobalKey<HubSectionState>> _orderedHubKeys = const [];
  final _actionBarKey = GlobalKey<FocusableActionBarState>();
  final _sourceMenuKey = GlobalKey<AppMenuButtonState<CatalogSourceId>>();

  final _tvBrowseRailKey = GlobalKey<TvBrowseRailState>();
  final _hubFocusMemory = HubFocusMemory();
  final TvSpotlightController _spotlight = TvSpotlightController();

  @override
  void initState() {
    super.initState();
    _explore = context.read<ExploreProvider>();
    _explore.ensureFresh();
  }

  @override
  void refresh() {
    _explore.ensureFresh();
  }

  @override
  void fullRefresh() {
    unawaited(_explore.load());
  }

  @override
  void onTabShown() {
    _explore.ensureFresh();
  }

  @override
  void onTabHidden() {}

  @override
  void dispose() {
    _spotlight.dispose();
    super.dispose();
  }

  @override
  void focusActiveTabIfReady() {
    if (PlatformDetector.isTV()) {
      if (_explore.rowHubs.isNotEmpty) {
        _tvBrowseRailKey.currentState?.requestFocus();
      } else {
        _actionBarKey.currentState?.requestFocusOnFirst();
      }
      return;
    }
    _orderedHubKeys.firstOrNull?.currentState?.requestFocusFromMemory();
  }

  void _setSpotlightItem(MediaItem item) => _spotlight.select(item);

  void _updateHubKeys(List<ExploreRowHub> rowHubs) {
    final liveIds = <String>{for (final rowHub in rowHubs) rowHub.hub.id};
    _hubKeysById.removeWhere((id, _) => !liveIds.contains(id));
    _orderedHubKeys = [
      for (final rowHub in rowHubs) _hubKeysById.putIfAbsent(rowHub.hub.id, GlobalKey<HubSectionState>.new),
    ];
  }

  bool _handleVerticalNavigation(int hubIndex, bool isUp) {
    final keys = _orderedHubKeys;
    return navigateVerticalHubRows(
      hubCount: keys.length,
      hubIndex: hubIndex,
      isUp: isUp,
      onTopBoundary: _actionBarKey.currentState?.requestFocusOnFirst,
      requestFocus: (targetIndex) {
        keys[targetIndex].currentState?.requestFocusFromMemory();
      },
    );
  }

  void _navigateToSidebar() {
    MainScreenFocusScope.focusSidebarOf(context);
  }

  static IconData _rowIcon(CatalogRowId? row) => switch (row) {
    null => Symbols.thumb_up_rounded,
    CatalogRowId.watchlist => Symbols.bookmark_rounded,
    CatalogRowId.recommendedMovies ||
    CatalogRowId.recommendedShows ||
    CatalogRowId.suggestedAnime => Symbols.thumb_up_rounded,
    CatalogRowId.trendingMovies ||
    CatalogRowId.trendingShows ||
    CatalogRowId.trendingAnime ||
    CatalogRowId.airingAnime ||
    CatalogRowId.trending => Symbols.trending_up_rounded,
    CatalogRowId.popularMovies || CatalogRowId.popularShows || CatalogRowId.popularAnime => Symbols.whatshot_rounded,
    CatalogRowId.upcomingMovies || CatalogRowId.upcomingShows => Symbols.event_upcoming_rounded,
  };

  List<AppMenuEntry<CatalogSourceId>> _sourceMenuEntries(CatalogSourcesProvider sources, CatalogSource active) => [
    for (final source in sources.connectedSources)
      AppMenuItem<CatalogSourceId>(
        value: source.id,
        leading: CatalogSourceLogo(source.id),
        label: source.displayName,
        selected: source.id == active.id,
      ),
  ];

  Widget _buildSourceSwitcher(
    CatalogSourcesProvider sources,
    CatalogSource active, {
    TextStyle? textStyle,
    AppMenuAnchorAlignment anchorAlignment = AppMenuAnchorAlignment.start,
    bool parentOwnsFocus = false,
  }) {
    final trigger = Container(
      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
      child: Row(
        mainAxisSize: .min,
        children: [
          CatalogSourceLogo(active.id, size: 22),
          const SizedBox(width: 8),
          Text(active.displayName, style: textStyle ?? Theme.of(context).textTheme.titleLarge),
          const SizedBox(width: 4),
          const AppIcon(Symbols.arrow_drop_down_rounded, fill: 1, size: 24),
        ],
      ),
    );
    if (parentOwnsFocus) {
      return AppMenuButton<CatalogSourceId>(
        key: _sourceMenuKey,
        tooltip: t.explore.selectSource,
        anchorAlignment: anchorAlignment,
        onSelected: (id) => unawaited(sources.setActiveSource(id)),
        entriesBuilder: (context) => _sourceMenuEntries(sources, active),
        child: trigger,
      );
    }
    return FocusablePopupMenuButton<CatalogSourceId>(
      menuKey: _sourceMenuKey,
      tooltip: t.explore.selectSource,
      semanticLabel: t.explore.selectSource,
      semanticValue: active.displayName,
      anchorAlignment: anchorAlignment,
      onSelected: (id) => unawaited(sources.setActiveSource(id)),
      itemBuilder: (context) => _sourceMenuEntries(sources, active),
      child: trigger,
    );
  }

  /// App-bar title: the active source name, as a switcher dropdown when more
  /// than one source is connected (mirrors the libraries dropdown).
  Widget _buildTitle(CatalogSourcesProvider sources) {
    final active = sources.activeSource;
    if (active == null) return Text(t.explore.title);
    if (sources.connectedSources.length < 2) {
      return Text(active.displayName);
    }
    return _buildSourceSwitcher(sources, active);
  }

  @override
  Widget build(BuildContext context) {
    final explore = context.watch<ExploreProvider>();
    final sources = context.watch<CatalogSourcesProvider>();
    final rowHubs = explore.rowHubs;
    _updateHubKeys(rowHubs);

    // The TV toolbar must remain mounted for loading, error, and empty
    // sources so users can always switch away from a source with no rows.
    if (PlatformDetector.isTV()) {
      return SettingsBuilder(
        prefs: const [SettingsService.hideSpoilers, SettingsService.libraryDensity, SettingsService.episodePosterMode],
        builder: (context) => _buildTvContent(rowHubs, sources),
      );
    }

    // One header mode for every state. Flipping floating/pinned between the
    // loading/empty scroll view and the content scroll view swaps the
    // SliverPersistentHeader variant (a different element type), which
    // reparents the GlobalKey'd action bar into a header that builds its
    // children during performLayout — and if a tooltip overlay is showing at
    // that moment (hover on refresh/search), its OverlayPortal re-activation
    // mutates the render tree mid-layout and asserts. Floating behaves
    // identically to pinned over the non-scrolling state widgets, so nothing
    // is lost by unifying.
    Widget appBar() => DesktopSliverAppBar(
      title: _buildTitle(sources),
      pinned: false,
      floating: true,
      snap: true,
      backgroundColor: Theme.of(context).scaffoldBackgroundColor,
      surfaceTintColor: Colors.transparent,
      shadowColor: Colors.transparent,
      scrolledUnderElevation: 0,
      actions: [
        FocusableActionBar(
          key: _actionBarKey,
          onNavigateDown: () => _orderedHubKeys.firstOrNull?.currentState?.requestFocusFromMemory(),
          actions: [
            if (sources.activeSource case final CatalogSource source)
              FocusableAction(
                icon: Symbols.search_rounded,
                tooltip: t.common.search,
                onPressed: () => Navigator.of(
                  context,
                ).push(MaterialPageRoute<void>(builder: (_) => CatalogSearchScreen(source: source))),
              ),
            FocusableAction(
              icon: Symbols.refresh_rounded,
              tooltip: t.common.refresh,
              onPressed: () => unawaited(_explore.load()),
            ),
          ],
        ),
      ],
    );

    Widget buildSimpleScroll({required Widget body}) {
      return CustomScrollView(
        // Android clamping physics won't start a drag on non-filling
        // content, killing pull-to-refresh in the loading/empty/error
        // states without this.
        physics: const AlwaysScrollableScrollPhysics(),
        slivers: [
          appBar(),
          SliverFillRemaining(child: body),
        ],
      );
    }

    Widget content;
    if (rowHubs.isEmpty && explore.isLoading) {
      content = buildSimpleScroll(body: const Center(child: CircularProgressIndicator()));
    } else if (rowHubs.isEmpty && explore.state == ExploreLoadState.error) {
      content = buildSimpleScroll(
        body: ErrorStateWidget(
          message: explore.errorMessage ?? t.explore.emptyTitle,
          icon: Symbols.error_outline_rounded,
          onRetry: () => unawaited(_explore.load()),
        ),
      );
    } else if (rowHubs.isEmpty) {
      content = buildSimpleScroll(
        body: EmptyStateWidget(
          message: t.explore.emptyMessage(source: explore.activeSource?.displayName ?? ''),
          icon: Symbols.explore_rounded,
        ),
      );
    } else {
      content = CustomScrollView(
        physics: const AlwaysScrollableScrollPhysics(),
        slivers: [
          appBar(),
          for (var i = 0; i < rowHubs.length; i++)
            SliverToBoxAdapter(
              child: HubSection(
                key: _orderedHubKeys[i],
                hub: rowHubs[i].hub,
                focusMemory: _hubFocusMemory,
                icon: _rowIcon(rowHubs[i].row),
                loadMoreItems: rowHubs[i].hub.more ? () => _explore.loadAllForHub(rowHubs[i]) : null,
                onVerticalNavigation: (isUp) => _handleVerticalNavigation(i, isUp),
                onNavigateUp: i == 0 ? () => _actionBarKey.currentState?.requestFocusOnFirst() : null,
                onNavigateToSidebar: _navigateToSidebar,
              ),
            ),
          const SliverToBoxAdapter(child: SizedBox(height: 16)),
        ],
      );
    }

    return Scaffold(
      body: RefreshIndicator(onRefresh: _explore.load, child: content),
    );
  }

  ExploreRowHub? _rowForHub(MediaHub hub) {
    for (final rowHub in _explore.rowHubs) {
      if (rowHub.hub.id == hub.id) return rowHub;
    }
    return null;
  }

  Widget _buildTvToolbar(CatalogSourcesProvider sources) {
    final active = sources.activeSource;
    final foregroundColor = Theme.of(context).colorScheme.onSurface;

    return ToolbarScrim(
      child: Row(
        children: [
          const Spacer(),
          FocusableActionBar(
            key: _actionBarKey,
            onNavigateLeft: _navigateToSidebar,
            onNavigateDown: _tvBrowseRailKey.currentState?.requestFocus,
            onBack: _navigateToSidebar,
            spacing: 4,
            actions: [
              if (active != null && sources.connectedSources.length > 1)
                FocusableAction(
                  debugLabel: 'ExploreSourceSwitcher',
                  onPressed: () => _sourceMenuKey.currentState?.showButtonMenu(focusFirstItem: true),
                  child: _buildSourceSwitcher(
                    sources,
                    active,
                    textStyle: Theme.of(
                      context,
                    ).textTheme.titleMedium?.copyWith(color: foregroundColor, fontWeight: .w600),
                    anchorAlignment: AppMenuAnchorAlignment.end,
                    parentOwnsFocus: true,
                  ),
                ),
              if (active != null)
                FocusableAction(
                  icon: Symbols.search_rounded,
                  iconColor: foregroundColor,
                  tooltip: t.common.search,
                  onPressed: () => Navigator.of(
                    context,
                  ).push(MaterialPageRoute<void>(builder: (_) => CatalogSearchScreen(source: active))),
                ),
              FocusableAction(
                icon: Symbols.refresh_rounded,
                iconColor: foregroundColor,
                tooltip: t.common.refresh,
                onPressed: () => unawaited(_explore.load()),
              ),
            ],
          ),
        ],
      ),
    );
  }

  Widget _buildTvContent(List<ExploreRowHub> rowHubs, CatalogSourcesProvider sources) {
    final tvHubs = [for (final rowHub in rowHubs) rowHub.hub];
    return TvSpotlightScaffold(
      hubs: tvHubs,
      spotlightListenable: _spotlight,
      resolveSpotlight: () => _spotlight.resolve(tvHubs),
      resolveClient: (spotlight) => context.tryGetMediaClientForServer(serverIdOrNull(spotlight?.serverId)),
      foreground: Stack(
        fit: StackFit.expand,
        clipBehavior: Clip.none,
        children: [
          if (tvHubs.isEmpty && _explore.isLoading)
            const Center(child: CircularProgressIndicator())
          else if (tvHubs.isEmpty && _explore.state == ExploreLoadState.error)
            Center(
              child: ErrorStateWidget(
                message: _explore.errorMessage ?? t.explore.emptyTitle,
                icon: Symbols.error_outline_rounded,
                onRetry: () => unawaited(_explore.load()),
              ),
            )
          else if (tvHubs.isEmpty)
            Center(
              child: EmptyStateWidget(
                message: t.explore.emptyMessage(source: _explore.activeSource?.displayName ?? ''),
                icon: Symbols.explore_rounded,
              ),
            ),
          if (tvHubs.isNotEmpty)
            Positioned(
              left: 0,
              right: 0,
              bottom: 0,
              child: TvBrowseRail(
                key: _tvBrowseRailKey,
                hubs: tvHubs,
                focusMemory: _hubFocusMemory,
                iconForHub: (hub, _) => _rowIcon(_rowForHub(hub)?.row),
                onFocusedItemChanged: _setSpotlightItem,
                loadMoreItems: (hub) {
                  final rowHub = _rowForHub(hub);
                  return rowHub == null ? Future.value(hub.items) : _explore.loadAllForHub(rowHub);
                },
                onNavigateUp: _actionBarKey.currentState?.requestFocusOnFirst,
                onNavigateToSidebar: _navigateToSidebar,
                onBack: _navigateToSidebar,
                tallPosterScale: TvBrowseRailLayout.compactTallPosterScale,
              ),
            ),
          TvToolbarOverlay(child: _buildTvToolbar(sources)),
        ],
      ),
    );
  }
}
