import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

import '../../../api/marianumconnect/queries/get_ticker/get_ticker_response.dart';
import '../../../api/marianumconnect/queries/get_ticker_nav/get_ticker_nav_response.dart';
import '../../../api/marianumconnect/queries/get_ticker_page/get_ticker_page_response.dart';
import '../../../routing/app_routes.dart';
import '../../../state/app/infrastructure/loadable_state/loadable_state.dart';
import '../../../state/app/infrastructure/loadable_state/view/loadable_state_consumer.dart';
import '../../../state/app/infrastructure/utility_widgets/bloc_module.dart';
import '../../../state/app/infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc_event.dart';
import '../../../state/app/modules/ticker/bloc/ticker_bloc.dart';
import '../../../state/app/modules/ticker/bloc/ticker_state.dart';
import '../../../theming/app_theme.dart';
import '../../../widget/details_bottom_sheet.dart';
import '../../../widget/placeholder_view.dart';
import '../../../widget/prosemirror/pm_json_view.dart';
import 'widgets/ticker_content_card.dart';
import 'widgets/ticker_nav_list.dart';
import 'widgets/ticker_page_body.dart';
import 'widgets/ticker_updated_bar.dart';

/// Ticker module entry. Wires the [TickerBloc] to the presentation
/// [TickerScaffold]; the "Aktuelles" home surface is driven through the loadable
/// consumer (loading/error/pull-to-refresh), individual pages load themselves.
class TickerView extends StatelessWidget {
  const TickerView({super.key});

  @override
  Widget build(BuildContext context) =>
      BlocModule<TickerBloc, LoadableState<TickerState>>(
        create: (context) => TickerBloc(),
        child: (context, bloc, _) {
          final data = context.watch<TickerBloc>().state.data;
          final sections =
              data?.nav?.sections
                  .where((section) => section.pages.isNotEmpty)
                  .toList() ??
              const <TickerNavSection>[];

          return TickerScaffold(
            sections: sections,
            initialSlug: data?.selectedSlug,
            onSelectionChanged: (slug) => bloc.add(
              Emit<TickerState>((state) => state.copyWith(selectedSlug: slug)),
            ),
            homeBuilder: (context, onLinkTap) =>
                LoadableStateConsumer<TickerBloc, TickerState>(
                  child: (state, loading) => _TickerHome(
                    ticker: state.ticker,
                    hasSections: sections.isNotEmpty,
                    onLinkTap: onLinkTap,
                  ),
                ),
          );
        },
      );
}

/// Bloc-free presentation shell: owns the in-place selection state, renders the
/// phone bottom-sheet nav / tablet sidebar navigation, the app bar (with home
/// action) and swaps the content region between the home surface and a page.
class TickerScaffold extends StatefulWidget {
  final List<TickerNavSection> sections;

  /// Builds the "Aktuelles" home surface. Receives the in-place link handler so
  /// internal ticker links switch the selection instead of pushing.
  final Widget Function(
    BuildContext context,
    void Function(String href) onLinkTap,
  )
  homeBuilder;

  /// Test seam: overrides the per-page content (default renders
  /// [TickerPageBody]).
  final Widget Function(
    BuildContext context,
    String slug,
    void Function(String href) onLinkTap,
  )?
  pageBuilder;

  /// Slug of the page to reopen on mount (persisted from a previous session),
  /// or null for the "Aktuelles" home. Restored only if it still refers to a
  /// selectable page in [sections]; otherwise it falls back to home.
  final String? initialSlug;

  /// Invoked whenever the in-place selection changes so the host can persist
  /// it (null = home). Also fired when a stale [initialSlug] is discarded.
  final void Function(String? slug)? onSelectionChanged;

  const TickerScaffold({
    super.key,
    required this.sections,
    required this.homeBuilder,
    this.pageBuilder,
    this.initialSlug,
    this.onSelectionChanged,
  });

  static const double sidebarBreakpoint = 900;
  static const double sidebarWidth = 300;

  @override
  State<TickerScaffold> createState() => _TickerScaffoldState();
}

class _TickerScaffoldState extends State<TickerScaffold> {
  String? _selectedSlug;
  String? _selectedTitle;

  // Back-gesture handling needs two pieces to cooperate with
  // PersistentTabView: a PopScope (canPop=false on a sub-page) emits the
  // NavigationNotification that makes the tab shell intercept the system back
  // instead of popping the root route, and this LocalHistoryEntry is what the
  // shell then finds poppable on the tab navigator (same mechanism a Drawer
  // uses) — popping it returns to "Aktuelles". Either piece alone fails: the
  // entry emits no notification (back closes the app), the scope alone leaves
  // the tab navigator unpoppable (back switches tabs).
  LocalHistoryEntry? _backEntry;
  bool _disposing = false;

  @override
  void initState() {
    super.initState();
    final initial = widget.initialSlug;
    if (initial != null && _isSelectable(widget.sections, initial)) {
      _selectedSlug = initial;
      _selectedTitle = _titleForSlug(initial);
    } else if (initial != null) {
      // Remembered page is gone (deleted/hidden) → drop the stale slug once
      // mounted and stay on "Aktuelles".
      WidgetsBinding.instance.addPostFrameCallback((_) {
        if (mounted) widget.onSelectionChanged?.call(null);
      });
    }
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    // A restored selection needs the same back interception a tap sets up, but
    // ModalRoute is only reliable here, not in initState.
    if (_selectedSlug != null && _backEntry == null && !_disposing) {
      _ensureBackEntry();
    }
  }

  @override
  void didUpdateWidget(TickerScaffold oldWidget) {
    super.didUpdateWidget(oldWidget);
    // A fresh nav can drop the open page (deleted/hidden/unpublished server
    // side) → fall back to "Aktuelles".
    final slug = _selectedSlug;
    if (slug != null && !_isSelectable(widget.sections, slug)) _selectHome();
  }

  @override
  void dispose() {
    _disposing = true;
    _removeBackEntry();
    super.dispose();
  }

  bool _isSelectable(List<TickerNavSection> sections, String slug) {
    for (final section in sections) {
      for (final page in section.pages) {
        if (page.slug == slug) return page.kind != TickerPageKind.redirect;
      }
    }
    return false;
  }

  void _ensureBackEntry() {
    if (_backEntry != null) return;
    final route = ModalRoute.of(context);
    if (route == null) return;
    final entry = LocalHistoryEntry(
      onRemove: () {
        _backEntry = null;
        if (_disposing || !mounted) return;
        _clearSelection();
      },
    );
    _backEntry = entry;
    route.addLocalHistoryEntry(entry);
  }

  void _removeBackEntry() {
    final entry = _backEntry;
    _backEntry = null;
    entry?.remove();
  }

  void _clearSelection() {
    if (_selectedSlug == null) return;
    setState(() {
      _selectedSlug = null;
      _selectedTitle = null;
    });
    widget.onSelectionChanged?.call(null);
  }

  // On phones the section/page navigation lives in a modal bottom sheet opened
  // from an app bar action (right), matching the app's actions-on-the-right and
  // shared bottom-sheet conventions instead of a left drawer. On tablets the
  // sidebar is permanent and this is unused.
  void _openNavSheet() {
    showDetailsBottomSheet(
      context,
      children: (sheetContext) => tickerNavItems(
        sheetContext,
        sections: widget.sections,
        selectedSlug: _selectedSlug,
        onSelectHome: () {
          Navigator.of(sheetContext).pop();
          _selectHome();
        },
        onSelectPage: (page) {
          Navigator.of(sheetContext).pop();
          _selectPage(page);
        },
        onRedirect: (page) {
          Navigator.of(sheetContext).pop();
          _openRedirect(page);
        },
      ),
    );
  }

  void _selectHome() {
    _clearSelection();
    _removeBackEntry();
  }

  void _selectPage(TickerNavPage page) {
    setState(() {
      _selectedSlug = page.slug;
      _selectedTitle = page.title;
    });
    widget.onSelectionChanged?.call(page.slug);
    _ensureBackEntry();
  }

  void _openRedirect(TickerNavPage page) {
    final url = page.externalUrl;
    if (url != null && url.isNotEmpty) {
      unawaited(AppRoutes.openExternalUrl(url));
    }
  }

  void _onLinkTap(String href) {
    final slug = AppRoutes.tickerSlugOf(href);
    if (slug != null && slug.isNotEmpty) {
      setState(() {
        _selectedSlug = slug;
        _selectedTitle = _titleForSlug(slug);
      });
      widget.onSelectionChanged?.call(slug);
      _ensureBackEntry();
      return;
    }
    unawaited(AppRoutes.openExternalUrl(href));
  }

  String? _titleForSlug(String slug) {
    for (final section in widget.sections) {
      for (final page in section.pages) {
        if (page.slug == slug) return page.title;
      }
    }
    return null;
  }

  @override
  Widget build(BuildContext context) => LayoutBuilder(
    builder: (context, constraints) {
      final wide = constraints.maxWidth >= TickerScaffold.sidebarBreakpoint;
      final slug = _selectedSlug;

      final nav = TickerNavList(
        sections: widget.sections,
        selectedSlug: slug,
        onSelectHome: _selectHome,
        onSelectPage: _selectPage,
        onRedirect: _openRedirect,
      );

      // Keep the "Aktuelles" surface permanently mounted (offstage while a
      // page is open) so returning to it is instant — a re-mount would re-run
      // PmJsonView's image precache and flash its spinner for a frame.
      final content = IndexedStack(
        index: slug == null ? 0 : 1,
        sizing: StackFit.expand,
        children: [
          widget.homeBuilder(context, _onLinkTap),
          if (slug == null)
            const SizedBox.shrink()
          else
            (widget.pageBuilder ?? _defaultPage)(context, slug, _onLinkTap),
        ],
      );

      return PopScope(
        canPop: slug == null,
        // Normally the pop lands on the LocalHistoryEntry, not here; this only
        // fires if the entry is missing (no enclosing route) as a fallback.
        onPopInvokedWithResult: (didPop, _) {
          if (!didPop) _selectHome();
        },
        child: Scaffold(
          appBar: AppBar(
            title: Text(slug == null ? 'Ticker' : (_selectedTitle ?? 'Ticker')),
            actions: [
              // Always present so "Aktuelles" is a fixed anchor; greyed out
              // (disabled) while it is the current surface.
              IconButton(
                icon: const Icon(Icons.home_outlined),
                tooltip: 'Aktuelles',
                onPressed: slug == null ? null : _selectHome,
              ),
            ],
          ),
          // Phones open the page nav via a FAB (bottom sheet); tablets show the
          // permanent sidebar instead, so no FAB there.
          floatingActionButton: wide
              ? null
              : FloatingActionButton(
                  heroTag: 'tickerNav',
                  backgroundColor: Theme.of(context).primaryColor,
                  tooltip: 'Seiten',
                  onPressed: _openNavSheet,
                  child: const Icon(Icons.toc),
                ),
          body: wide
              ? Row(
                  children: [
                    SizedBox(width: TickerScaffold.sidebarWidth, child: nav),
                    const VerticalDivider(width: 1),
                    Expanded(child: content),
                  ],
                )
              : content,
        ),
      );
    },
  );

  Widget _defaultPage(
    BuildContext context,
    String slug,
    void Function(String href) onLinkTap,
  ) => TickerPageBody(
    key: ValueKey(slug),
    slug: slug,
    onLinkTap: onLinkTap,
    onRedirect: _selectHome,
  );
}

/// The "Aktuelles" home surface: the current ticker post, or a hint when there
/// is no post (yet). Scrollable so the loadable consumer's pull-to-refresh
/// works.
class _TickerHome extends StatelessWidget {
  final TickerResponse? ticker;
  final bool hasSections;
  final void Function(String href) onLinkTap;

  const _TickerHome({
    required this.ticker,
    required this.hasSections,
    required this.onLinkTap,
  });

  @override
  Widget build(BuildContext context) {
    final ticker = this.ticker;
    if (ticker == null) {
      return PlaceholderView(
        icon: hasSections ? Icons.campaign_outlined : Icons.feed_outlined,
        text: hasSections
            ? 'Zurzeit gibt es keine aktuelle Meldung.'
            : 'Zurzeit sind keine Ticker-Inhalte verfügbar.',
      );
    }

    return ListView(
      physics: const AlwaysScrollableScrollPhysics(),
      padding: const EdgeInsets.only(bottom: AppSpacing.lg),
      children: [
        TickerUpdatedBar(publishedAt: ticker.publishedAt),
        _CurrentTickerCard(ticker: ticker, onLinkTap: onLinkTap),
      ],
    );
  }
}

class _CurrentTickerCard extends StatelessWidget {
  final TickerResponse ticker;
  final void Function(String href) onLinkTap;

  const _CurrentTickerCard({required this.ticker, required this.onLinkTap});

  @override
  Widget build(BuildContext context) {
    final content = ticker.content;
    final hasContent = ticker.available && content != null;

    return TickerContentCard(
      child: hasContent
          ? PmJsonView(json: content, onLinkTap: onLinkTap)
          : _UnavailableHint(webUrl: ticker.webUrl),
    );
  }
}

class _UnavailableHint extends StatelessWidget {
  final String webUrl;

  const _UnavailableHint({required this.webUrl});

  @override
  Widget build(BuildContext context) => Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Text(
        'Diese Meldung ist in der App nicht verfügbar.',
        style: Theme.of(context).textTheme.bodyMedium,
      ),
      const SizedBox(height: AppSpacing.xs),
      Align(
        alignment: Alignment.centerLeft,
        child: TextButton.icon(
          onPressed: () => AppRoutes.openWebUrl(webUrl),
          icon: const Icon(Icons.open_in_new),
          label: const Text('Im Browser öffnen'),
        ),
      ),
    ],
  );
}
