import 'dart:async';
import 'dart:developer';

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

import 'api/marianumconnect/queries/get_breakers/get_breakers_response.dart';
import 'api/marianumconnect/queries/telemetry_heartbeat/telemetry_heartbeat.dart';
import 'main.dart';
import 'model/data_cleaner.dart';
import 'notification/notification_controller.dart';
import 'notification/notification_tasks.dart';
import 'push/push_registration.dart';
import 'push/push_tap_router.dart';
import 'routing/app_routes.dart';
import 'share_intent/share_intent_listener.dart';
import 'state/app/modules/app_modules.dart';
import 'state/app/modules/breaker/bloc/breaker_bloc.dart';
import 'state/app/modules/capabilities/bloc/capabilities_cubit.dart';
import 'state/app/modules/chat_list/bloc/chat_list_bloc.dart';
import 'state/app/modules/settings/bloc/settings_cubit.dart';
import 'state/app/modules/timetable/bloc/timetable_bloc.dart';
import 'state/app/modules/timetable/bloc/timetable_state.dart';
import 'storage/settings.dart' as model;
import 'utils/debouncer.dart';
import 'utils/haptics.dart';
import 'view/pages/overhang.dart';
import 'widget/breaker/breaker.dart';
import 'widget_data/widget_navigation.dart';
import 'widget_data/widget_publisher.dart';

class App extends StatefulWidget {
  const App({super.key});

  @override
  State<App> createState() => _AppState();
}

class _AppState extends State<App> with WidgetsBindingObserver {
  late Timer _updateTimings;
  StreamSubscription<dynamic>? _timetableWidgetSync;
  StreamSubscription<RemoteMessage>? _onMessageSub;
  StreamSubscription<RemoteMessage>? _onMessageOpenedAppSub;
  StreamSubscription<String>? _fcmTokenRefreshSub;
  int _knownTotalTabs = 1;
  int _lastTabIndex = 0;
  bool _userOnLastTab = false;
  DateTime? _lastTelemetryAt;

  static const Duration _chatListActiveInterval = Duration(seconds: 15);
  static const Duration _chatListIdleInterval = Duration(seconds: 60);
  static const Duration _telemetryInterval = Duration(minutes: 15);

  void _onTabControllerChanged() {
    final newIndex = Main.bottomNavigator.index;
    if (newIndex != _lastTabIndex) {
      Haptics.selection();
      _lastTabIndex = newIndex;
    }
    _userOnLastTab = newIndex == _knownTotalTabs - 1;
    _syncChatListPolling();
  }

  void _syncChatListPolling({bool refresh = true}) {
    if (!mounted) return;
    final modules = AppModule.getBottomBarModules(context);
    final talkSlot = modules.indexWhere((m) => m.module == Modules.talk);
    final talkIsActive =
        talkSlot >= 0 && Main.bottomNavigator.index == talkSlot;
    final bloc = context.read<ChatListBloc>();
    bloc.setAutoRefreshInterval(
      talkIsActive ? _chatListActiveInterval : _chatListIdleInterval,
    );
    if (talkIsActive && refresh) bloc.refresh();
  }

  // Wall-clock throttle rather than Debouncer.throttle: a Timer does not tick
  // reliably while the app is suspended, so the window would still be open on
  // the resume it is supposed to let through.
  void _reportTelemetry() {
    if (!mounted) return;
    final now = DateTime.now();
    final last = _lastTelemetryAt;
    if (last != null && now.difference(last) < _telemetryInterval) return;
    _lastTelemetryAt = now;
    TelemetryHeartbeat.report(
      notificationsEnabled: context
          .read<SettingsCubit>()
          .val()
          .notificationSettings
          .enabled,
    );
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    log('AppLifecycle: $state');
    if (state == AppLifecycleState.resumed) {
      _reportTelemetry();
      Debouncer.throttle('appLifecycleState', const Duration(seconds: 10), () {
        if (!mounted) return;
        log('Refreshing due to LifecycleChange');
        NotificationTasks.updateProviders(context);
      });
      // updateProviders already refreshes the chat list; only re-arm the poll.
      _syncChatListPolling(refresh: false);
      _handlePendingWidgetNavigation();
    } else if (mounted) {
      // Stop polling while backgrounded: a silent refresh failing in the
      // background would otherwise leave an error that flashes on the next
      // resume before the foreground refetch replaces it.
      context.read<ChatListBloc>().setAutoRefreshInterval(null);
    }
  }

  void _onPushTapPending() {
    final token = PushTapRouter.pendingChatToken.value;
    if (token == null || !mounted) return;
    PushTapRouter.pendingChatToken.value = null;
    NotificationTasks.navigateToTalk(context, chatToken: token);
  }

  void _onNewsletterTapPending() {
    final id = PushTapRouter.pendingNewsletterId.value;
    if (id == null || !mounted) return;
    PushTapRouter.pendingNewsletterId.value = null;
    AppRoutes.openNewsletterById(context, id: id);
  }

  Future<void> _handlePendingWidgetNavigation() async {
    final pending = await WidgetNavigation.consumePendingTimetableTap();
    if (!pending || !mounted) return;
    // `withNavBar: false` routes sit on the root navigator above the
    // bottom-nav; pop them so jumpToTab is actually visible. Stop at
    // popups so open dialogs/sheets stay alive.
    final navigator = Navigator.of(context);
    if (navigator.canPop()) {
      navigator.popUntil((route) => route.isFirst || route is PopupRoute);
    }
    AppRoutes.goToTab(context, Modules.timetable);
  }

  void _handlePendingShare() {
    if (!mounted) return;
    final share = ShareIntentListener.pending.value;
    if (share == null) return;
    // A second share would otherwise leave the previous share-flow page
    // on top with stale (already-cleared) file paths.
    final navigator = Navigator.of(context);
    if (navigator.canPop()) {
      navigator.popUntil((route) => route.isFirst || route is PopupRoute);
    }
    AppRoutes.openShareTarget(context, share);
  }

  @override
  void initState() {
    super.initState();
    Haptics.bind(context.read<SettingsCubit>());
    Main.bottomNavigator = PersistentTabController(initialIndex: 0);
    _lastTabIndex = Main.bottomNavigator.index;
    Main.bottomNavigator.addListener(_onTabControllerChanged);
    WidgetsBinding.instance.addObserver(this);

    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (!mounted) return;
      context.read<BreakerBloc>().refresh();
      context.read<ChatListBloc>().refresh();
      // Re-mounts on every login, so this also covers post-logout state reset.
      final timetable = context.read<TimetableBloc>();
      timetable.refresh();
      // Mirror BLoC updates into the home-screen widget without waiting
      // for the periodic background refresh.
      final settingsCubit = context.read<SettingsCubit>();
      final capabilitiesCubit = context.read<CapabilitiesCubit>();
      _timetableWidgetSync?.cancel();
      _timetableWidgetSync = timetable.stream.listen((state) {
        final data = state.data;
        if (data is TimetableState && !state.isLoading) {
          unawaited(
            WidgetPublisher.publishFromBlocState(
              data,
              settings: settingsCubit.val(),
              isTeacher: capabilitiesCubit.isTeacher,
            ),
          );
        }
      });
      // Initial publish in case hydrated storage already has data.
      final initialData = timetable.state.data;
      if (initialData is TimetableState) {
        unawaited(
          WidgetPublisher.publishFromBlocState(
            initialData,
            settings: settingsCubit.val(),
            isTeacher: capabilitiesCubit.isTeacher,
          ),
        );
      }
      unawaited(_handlePendingWidgetNavigation());
      ShareIntentListener.instance.attach();
      ShareIntentListener.pending.addListener(_handlePendingShare);
      _handlePendingShare();
      _syncChatListPolling();
    });

    _updateTimings = Timer.periodic(const Duration(seconds: 30), (_) {
      if (mounted) setState(() {});
    });

    _reportTelemetry();

    // A refreshed FCM token invalidates the existing push subscription — the
    // NC device identifier stays stable, so we simply re-register (NC first,
    // then the proxy). Debounced so a burst of refreshes triggers one call.
    // Not gated on the notification toggle: registration is kept alive even
    // when notifications are off so silent sync pushes keep flowing.
    _fcmTokenRefreshSub = FirebaseMessaging.instance.onTokenRefresh.listen((_) {
      Debouncer.debounce(
        'pushTokenRefresh',
        const Duration(seconds: 3),
        () => unawaited(PushRegistration().onTokenRefresh()),
      );
    });

    // Android renders pushes locally, so a tap arrives via the local
    // notifications callback (PushTapRouter) rather than onMessageOpenedApp.
    PushTapRouter.pendingChatToken.addListener(_onPushTapPending);
    PushTapRouter.pendingNewsletterId.addListener(_onNewsletterTapPending);

    _onMessageSub = FirebaseMessaging.onMessage.listen((message) {
      if (!mounted) return;
      NotificationController.onForegroundMessageHandler(message, context);
    });

    // iOS delivers alert pushes (Connect direct pushes, and NC pushes rendered
    // by the NSE) natively; a tap surfaces here.
    _onMessageOpenedAppSub = FirebaseMessaging.onMessageOpenedApp.listen((
      message,
    ) {
      if (!mounted) return;
      NotificationController.onAppOpenedByNotification(message, context);
    });
    FirebaseMessaging.instance.getInitialMessage().then((message) {
      if (message == null || !mounted) return;
      NotificationController.onAppOpenedByNotification(message, context);
    });

    DataCleaner.cleanOldCache();
  }

  @override
  void dispose() {
    _updateTimings.cancel();
    _timetableWidgetSync?.cancel();
    _onMessageSub?.cancel();
    _onMessageOpenedAppSub?.cancel();
    _fcmTokenRefreshSub?.cancel();
    PushTapRouter.pendingChatToken.removeListener(_onPushTapPending);
    PushTapRouter.pendingNewsletterId.removeListener(_onNewsletterTapPending);
    ShareIntentListener.pending.removeListener(_handlePendingShare);
    ShareIntentListener.instance.detach();
    Main.bottomNavigator.removeListener(_onTabControllerChanged);
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  Widget build(
    BuildContext context,
  ) => BlocBuilder<SettingsCubit, model.Settings>(
    builder: (context, _) {
      final bottomBarModules = AppModule.getBottomBarModules(context);
      final totalTabs = bottomBarModules.length + 1;
      final currentIndex = Main.bottomNavigator.index;

      // PersistentTabView caches per-tab navigators by index and only
      // appends/trims at the end, so reordering/hiding leaves stale
      // route stacks under the wrong tabs. Re-key on layout to remount.
      final layoutKey = ValueKey(
        '${bottomBarModules.map((m) => m.module.name).join('|')}|more',
      );

      if (totalTabs != _knownTotalTabs) {
        var targetIndex = currentIndex;
        if (_userOnLastTab || currentIndex >= totalTabs) {
          targetIndex = totalTabs - 1;
        }
        // Replace the controller atomically: a stale index past the new
        // tab list crashes Style6BottomNavBar's initState.
        if (targetIndex != currentIndex) {
          Main.bottomNavigator.removeListener(_onTabControllerChanged);
          Main.bottomNavigator = PersistentTabController(
            initialIndex: targetIndex,
          );
          _lastTabIndex = targetIndex;
          Main.bottomNavigator.addListener(_onTabControllerChanged);
          _userOnLastTab = targetIndex == totalTabs - 1;
        }
      }

      _knownTotalTabs = totalTabs;

      return PersistentTabView(
        key: layoutKey,
        controller: Main.bottomNavigator,
        navBarOverlap: const NavBarOverlap.none(),
        backgroundColor: Theme.of(context).colorScheme.primary,
        handleAndroidBackButtonPress: true,
        screenTransitionAnimation: const ScreenTransitionAnimation(
          curve: Curves.easeOutQuad,
          duration: Duration(milliseconds: 200),
        ),
        tabs: [
          ...bottomBarModules.map((e) => e.toBottomTab(context)),
          PersistentTabConfig(
            screen: const Breaker(breaker: BreakerArea.more, child: Overhang()),
            item: ItemConfig(
              activeForegroundColor: Theme.of(context).primaryColor,
              inactiveForegroundColor: Theme.of(context).colorScheme.secondary,
              icon: const Icon(Icons.apps),
              title: 'Mehr',
            ),
          ),
        ],
        navBarBuilder: (config) => Style6BottomNavBar(
          // Animation controllers are built once in initState and never
          // grown — re-key on item count to avoid RangeError on growth.
          key: ValueKey(config.items.length),
          navBarConfig: config,
          navBarDecoration: NavBarDecoration(
            border: Border(
              top: BorderSide(
                width: 1,
                color: Theme.of(context).colorScheme.outlineVariant,
              ),
            ),
            color: Theme.of(context).colorScheme.surface,
          ),
        ),
      );
    },
  );
}
