import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'dart:ui';

import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:jiffy/jiffy.dart';
import 'package:loader_overlay/loader_overlay.dart';
import 'package:path_provider/path_provider.dart';
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
import 'package:shared_preferences/shared_preferences.dart';

import 'api/marianumcloud/webdav/queries/list_files/list_files_cache.dart';
import 'api/marianumconnect/auth/session_validator.dart';
import 'api/marianumconnect/marianumconnect_endpoint.dart';
import 'api/marianumconnect/queries/get_breakers/get_breakers_response.dart';
import 'api/marianumconnect/queries/report_client_error/client_error_reporter.dart';
import 'api/marianumconnect/queries/telemetry_heartbeat/telemetry_heartbeat.dart';
import 'app.dart';
import 'background/widget_background_task.dart';
import 'firebase_options.dart';
import 'model/account_data.dart';
import 'notification/notification_service.dart';
import 'push/push_message_handler.dart';
import 'push/push_registration.dart';
import 'push/push_registration_store.dart';
import 'push/push_renderer.dart';
import 'routing/app_routes.dart';
import 'share_intent/share_intent_listener.dart';
import 'state/app/modules/account/bloc/account_bloc.dart';
import 'state/app/modules/account/bloc/account_state.dart';
import 'state/app/modules/breaker/bloc/breaker_bloc.dart';
import 'state/app/modules/capabilities/bloc/capabilities_cubit.dart';
import 'state/app/modules/chat/bloc/chat_bloc.dart';
import 'state/app/modules/chat_list/bloc/chat_list_bloc.dart';
import 'state/app/modules/nextcloud_capabilities/bloc/nextcloud_capabilities_cubit.dart';
import 'state/app/modules/settings/bloc/settings_cubit.dart';
import 'state/app/modules/timetable/bloc/timetable_bloc.dart';
import 'storage/settings.dart';
import 'theming/dark_app_theme.dart';
import 'theming/light_app_theme.dart';
import 'utils/app_paths.dart';
import 'utils/downloads/download_manager.dart';
import 'view/login/login.dart';
import 'view/login/post_login_splash.dart';
import 'widget/app_progress_indicator.dart';
import 'widget/avatar_disk_cache.dart';
import 'widget/breaker/breaker.dart';
import 'widget/debug/cache_view.dart';
import 'widget/downloads/download_tray.dart';
import 'widget/emergency/emergency_notice_gate.dart';
import 'widget_data/widget_sync.dart';

Future<void> main() async {
  log('MarianumMobile started');
  WidgetsFlutterBinding.ensureInitialized();

  void addCertificateAsTrusted(ByteData certificate) => SecurityContext
      .defaultContext
      .setTrustedCertificatesBytes(certificate.buffer.asUint8List());

  final initialisationTasks = [
    Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform)
        .then<void>((_) {})
        .onError((error, _) => log('Error initializing Firebase: $error')),
    PlatformAssetBundle()
        .load('assets/ca/lets-encrypt-r3.pem')
        .then(addCertificateAsTrusted),
    PlatformAssetBundle()
        .load('assets/ca/lets-encrypt-r10.pem')
        .then(addCertificateAsTrusted),
    PlatformAssetBundle()
        .load('assets/ca/lets-encrypt-r13.pem')
        .then(addCertificateAsTrusted),
    Future(() async {
      final storage = await HydratedStorage.build(
        storageDirectory: HydratedStorageDirectory(
          (await getTemporaryDirectory()).path,
        ),
      );
      HydratedBloc.storage = storage;
    }),
    Future(() async {
      AppPaths.documentsDir = (await getApplicationDocumentsDirectory()).path;
    }),
    AccountData().waitForPopulation(),
    ShareIntentListener.instance.initialize(),
  ];

  log('starting app initialisation...');
  await Future.wait(initialisationTasks);
  log('app initialisation done!');

  // Local notifications: init the plugin (with tap/action callbacks) and the
  // Android channels, then register the FCM background isolate handler that
  // decrypts and renders Nextcloud pushes while the app is not in foreground.
  await NotificationService().initializeNotifications();
  await PushRenderer.ensureChannels();
  FirebaseMessaging.onBackgroundMessage(pushOnBackgroundMessage);

  // Wire up the native background downloader (progress notifications + tap
  // handling) before the UI so a completion notification tapped during cold
  // start is captured and opened once the downloads tray mounts.
  unawaited(
    DownloadManager.instance.initialize().onError(
      (e, _) => log('DownloadManager init failed: $e'),
    ),
  );

  // Wire up the home-screen widget bridge before runApp so any widget render
  // triggered during startup hits initialised native storage.
  await WidgetSync.ensureInitialized();
  unawaited(
    WidgetBackgroundTask.initialize().onError(
      (e, _) => log('Workmanager init failed: $e'),
    ),
  );

  // Diagnostic log only. getToken() can fail transiently during cold start
  // (Android: "IOException: FCM Registration failed!" from Play Services,
  // iOS: apns-token-not-set until APNS registration completes), so tolerate
  // the failure instead of letting it surface as an uncaught async error that
  // gets reported to the telemetry backend as noise.
  unawaited(
    FirebaseMessaging.instance
        .getToken()
        .then(
          (token) =>
              log('Firebase token: ${token ?? "Error: no Firebase token!"}'),
        )
        .onError((e, _) => log('Firebase token unavailable: $e')),
  );

  // Warm up the Nextcloud root listing in the background while the user is
  // still on the launch screen / other modules — the root endpoint is slow
  // on our instance, so kicking it off early means the Files page already
  // has data ready by the time the user navigates to it. No-op when a
  // cached payload is already present, so this does not undo the day-long
  // root cache TTL.
  if (AccountData().isPopulated()) {
    unawaited(
      ListFilesCache.prefetchRootListing().onError(
        (e, _) => log('Files root prefetch failed: $e'),
      ),
    );
  }

  // Resolve the avatar cache directory ahead of the first avatar render so the
  // synchronous disk read hits and cold-start avatars appear without a blank
  // placeholder flash.
  AvatarDiskCache.instance.warmUp();

  if (kReleaseMode) {
    ErrorWidget.builder = (error) => Material(
      color: Colors.white,
      child: Center(
        child: Padding(
          padding: const EdgeInsets.all(24),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              const Icon(Icons.phonelink_erase_rounded, size: 40),
              const SizedBox(height: 12),
              Text(error.toStringShort(), textAlign: TextAlign.center),
            ],
          ),
        ),
      ),
    );
  }

  FlutterError.onError = (details) {
    log(
      'Uncaught Flutter error: ${details.exception}',
      stackTrace: details.stack,
    );
    ClientErrorReporter.reportFlutterError(details);
    FlutterError.presentError(details);
  };
  PlatformDispatcher.instance.onError = (error, stack) {
    log('Uncaught platform error: $error', stackTrace: stack);
    ClientErrorReporter.reportPlatformError(error, stack);
    return true;
  };

  log('running app...');
  runApp(
    MultiBlocProvider(
      providers: [
        BlocProvider<SettingsCubit>(create: (_) => SettingsCubit()),
        BlocProvider<AccountBloc>(
          create: (_) => AccountBloc(
            initialStatus: AccountData().isPopulated()
                ? AccountStatus.loggedIn
                : AccountStatus.loggedOut,
          ),
        ),
        BlocProvider<BreakerBloc>(create: (_) => BreakerBloc()),
        BlocProvider<CapabilitiesCubit>(create: (_) => CapabilitiesCubit()),
        BlocProvider<NextcloudCapabilitiesCubit>(
          create: (_) => NextcloudCapabilitiesCubit(),
        ),
        BlocProvider<ChatListBloc>(create: (_) => ChatListBloc()),
        BlocProvider<ChatBloc>(
          create: (ctx) => ChatBloc(chatListBloc: ctx.read<ChatListBloc>()),
        ),
        BlocProvider<TimetableBloc>(create: (_) => TimetableBloc()),
      ],
      child: const Main(),
    ),
  );
}

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

  static PersistentTabController bottomNavigator = PersistentTabController(
    initialIndex: 0,
  );

  @override
  State<Main> createState() => _MainState();
}

class _MainState extends State<Main> {
  bool _showPostLoginSplash = false;
  bool _appMounted = true;

  @override
  void initState() {
    super.initState();
    Jiffy.setLocale('de');

    AccountData().waitForPopulation().then((value) {
      if (!mounted) return;
      final accountBloc = context.read<AccountBloc>();
      accountBloc.setStatus(
        value ? AccountStatus.loggedIn : AccountStatus.loggedOut,
      );
      if (value) {
        _scheduleSessionValidation(accountBloc);
        // Cold start while already logged in: the account status doesn't
        // change, so the loggedIn listener below never fires — refresh
        // capabilities here, then self-heal the push registration.
        final settingsCubit = context.read<SettingsCubit>();
        unawaited(
          context.read<CapabilitiesCubit>().load().then((_) {
            if (!mounted) return;
            _syncPush(settingsCubit, context.read<CapabilitiesCubit>());
          }),
        );
        unawaited(context.read<NextcloudCapabilitiesCubit>().load());
      }
    });
  }

  /// Warms the core caches (timetable, chat list, files root) in the
  /// background so the first screen render hits populated data.
  void _prefetchBaseData(BuildContext context) {
    context.read<TimetableBloc>().refresh();
    unawaited(context.read<ChatListBloc>().refresh(silent: true));
    unawaited(ListFilesCache.prefetchRootListing());
  }

  /// Registers/self-heals the push subscription whenever the backend advertises
  /// the capability — independent of the notification toggle, so a user with
  /// notifications off stays registered for silent sync pushes. Fire-and-forget.
  void _syncPush(SettingsCubit settings, CapabilitiesCubit capabilities) {
    final enabled = settings.val().notificationSettings.enabled;
    unawaited(
      PushRegistration.syncSubscription(
        capable: capabilities.canReceivePushNotifications,
      ).then((registered) {
        // The app-start heartbeat runs before this async registration
        // finishes, so it reports the pre-registration state. Re-emit once
        // the identifier is persisted so the new push status shows up this
        // session instead of only after the next launch.
        if (registered) {
          TelemetryHeartbeat.report(notificationsEnabled: enabled);
        }
      }),
    );
  }

  /// Background credential check: a 401 means the password was rotated
  /// server-side, so the validator wipes the local session and flips the
  /// account bloc to `loggedOut` (sending the user to the login screen).
  void _scheduleSessionValidation(AccountBloc accountBloc) {
    unawaited(
      SessionValidator.probeStored(
        onInvalidated: () async {
          if (!mounted) return;
          accountBloc.setStatus(AccountStatus.loggedOut);
        },
      ),
    );
  }

  @override
  Widget build(BuildContext context) => Directionality(
    textDirection: TextDirection.ltr,
    child: BlocBuilder<SettingsCubit, Settings>(
      builder: (context, settings) {
        final devToolsSettings = settings.devToolsSettings;
        // Keep the MC dio singleton aligned with the currently selected
        // endpoint (live / beta / custom). Idempotent when the URL is
        // unchanged so it's safe to call on every rebuild. Mirrored into
        // WidgetSync so the background isolate refreshes against the same
        // endpoint.
        final mcBaseUrl = devToolsSettings.resolveMarianumConnectBaseUrl();
        MarianumConnectEndpoint.update(mcBaseUrl);
        unawaited(WidgetSync.setMarianumConnectBaseUrl(mcBaseUrl));
        // Mirror the notification toggle into group-scoped storage so the FCM
        // background isolate and the iOS NSE can suppress rendering when off.
        unawaited(
          const PushRegistrationStore().setNotificationsEnabled(
            settings.notificationSettings.enabled,
          ),
        );
        return MaterialApp(
          showPerformanceOverlay: devToolsSettings.showPerformanceOverlay,
          checkerboardOffscreenLayers:
              devToolsSettings.checkerboardOffscreenLayers,
          checkerboardRasterCacheImages:
              devToolsSettings.checkerboardRasterCacheImages,
          debugShowCheckedModeBanner: false,
          navigatorKey: AppRoutes.rootNavigatorKey,
          // Used by ChatView.didPopNext to reclaim the global ChatBloc.
          // DownloadRouteObserver tracks full-page navigations so the downloads
          // chip only surfaces once the user leaves the screen they started on.
          navigatorObservers: [
            AppRoutes.chatRouteObserver,
            DownloadRouteObserver(),
          ],
          localizationsDelegates: const [
            ...GlobalMaterialLocalizations.delegates,
            GlobalWidgetsLocalizations.delegate,
          ],
          supportedLocales: const [Locale('de'), Locale('en')],
          locale: const Locale('de'),
          title: 'Marianum Fulda',
          themeMode: settings.appTheme,
          theme: LightAppTheme.theme,
          darkTheme: DarkAppTheme.theme,
          // Brand-colored backdrop behind every route. During the logout
          // home-swap and route pop animations the framework can briefly
          // expose the layer below the topmost Scaffold; without this
          // the dark Material default shows through and the user sees a
          // black flash.
          builder: (context, child) => ColoredBox(
            color: LightAppTheme.marianumRed,
            // Downloads tray mounted ABOVE the navigator so its chip floats over
            // every route (folder views, chat, viewer are full-page pushes that
            // would otherwise cover it).
            child: DownloadTrayHost(child: child ?? const SizedBox.shrink()),
          ),
          home: EmergencyNoticeGate(
            child: LoaderOverlay(
              child: Breaker(
                breaker: BreakerArea.global,
              child: BlocConsumer<AccountBloc, AccountState>(
                listenWhen: (previous, current) =>
                    previous.status != current.status,
                listener: (context, accountState) {
                  // Fresh login (loggedOut -> loggedIn): pull capability flags
                  // for the newly authenticated user, then register push right
                  // away instead of deferring it to the next app start.
                  if (accountState.status == AccountStatus.loggedIn) {
                    final settingsCubit = context.read<SettingsCubit>();
                    final capabilitiesCubit = context.read<CapabilitiesCubit>();
                    unawaited(
                      capabilitiesCubit.load().then((_) {
                        if (!mounted) return;
                        _syncPush(settingsCubit, capabilitiesCubit);
                      }),
                    );
                    unawaited(
                      context.read<NextcloudCapabilitiesCubit>().load(),
                    );
                    _showPostLoginSplash = true;
                    _appMounted = false;
                    WidgetsBinding.instance.addPostFrameCallback((_) {
                      if (mounted) setState(() => _appMounted = true);
                    });
                    _prefetchBaseData(context);
                  }
                  if (accountState.status != AccountStatus.loggedOut) return;
                  // A pending share would otherwise survive logout and be
                  // re-applied after re-login with file paths the OS may
                  // already have evicted from the cache.
                  ShareIntentListener.instance.clear();
                  // Routes pushed via AppRoutes (e.g. Settings) live on the
                  // root navigator and survive the home swap below, so they
                  // would still cover the Login screen after logout. Pop
                  // them here so the user immediately sees Login.
                  final navigator = Navigator.of(context);
                  if (navigator.canPop()) {
                    navigator.popUntil((route) => route.isFirst);
                  }
                  // Capture bloc references before the post-frame callback
                  // — by the time it runs the dialog/Settings context is
                  // gone but this listener context is still valid.
                  final settingsCubit = context.read<SettingsCubit>();
                  final timetableBloc = context.read<TimetableBloc>();
                  final chatListBloc = context.read<ChatListBloc>();
                  final chatBloc = context.read<ChatBloc>();
                  final breakerBloc = context.read<BreakerBloc>();
                  final capabilitiesCubit = context.read<CapabilitiesCubit>();
                  final nextcloudCapabilitiesCubit = context
                      .read<NextcloudCapabilitiesCubit>();
                  // Defer the actual wipe until after this frame so the
                  // App tree (TimetableBloc/ChatListBloc watchers etc.)
                  // is already torn down. Resetting blocs while App is
                  // still in front caused a black-frame race.
                  WidgetsBinding.instance.addPostFrameCallback((_) {
                    unawaited(
                      _wipeUserState(
                        settingsCubit: settingsCubit,
                        timetableBloc: timetableBloc,
                        chatListBloc: chatListBloc,
                        chatBloc: chatBloc,
                        breakerBloc: breakerBloc,
                        capabilitiesCubit: capabilitiesCubit,
                        nextcloudCapabilitiesCubit: nextcloudCapabilitiesCubit,
                      ),
                    );
                  });
                },
                builder: (context, accountState) {
                  switch (accountState.status) {
                    case AccountStatus.loggedIn:
                      return Stack(
                        fit: StackFit.expand,
                        children: [
                          if (_appMounted)
                            const App(key: ValueKey('app-shell')),
                          if (_showPostLoginSplash)
                            PostLoginSplash(
                              key: const ValueKey('post-login-splash'),
                              onComplete: () =>
                                  setState(() => _showPostLoginSplash = false),
                            ),
                        ],
                      );
                    case AccountStatus.loggedOut:
                      return const Login();
                    case AccountStatus.undefined:
                      return Scaffold(
                        backgroundColor: LightAppTheme.marianumRed,
                        body: const Center(
                          child: Column(
                            mainAxisAlignment: MainAxisAlignment.center,
                            children: [
                              AppProgressIndicator.large(color: Colors.white),
                              SizedBox(height: 16),
                              Text(
                                'Konto wird geladen…',
                                style: TextStyle(color: Colors.white),
                              ),
                            ],
                          ),
                        ),
                      );
                  }
                },
                ),
              ),
            ),
          ),
        );
      },
    ),
  );
}

Future<void> _wipeUserState({
  required SettingsCubit settingsCubit,
  required TimetableBloc timetableBloc,
  required ChatListBloc chatListBloc,
  required ChatBloc chatBloc,
  required BreakerBloc breakerBloc,
  required CapabilitiesCubit capabilitiesCubit,
  required NextcloudCapabilitiesCubit nextcloudCapabilitiesCubit,
}) async {
  try {
    // Reset user-data blocs whose tree is no longer mounted after the
    // home swap. We do NOT touch SettingsCubit here — its outer BlocBuilder
    // wraps MaterialApp, so emit'ing a fresh state would tear down the
    // freshly-mounted Login tree and leave the user with a blank screen
    // (the MaterialApp.builder backdrop) until the next interaction.
    await Future.wait([
      timetableBloc.reset(),
      chatListBloc.reset(),
      chatBloc.reset(),
      breakerBloc.reset(),
      capabilitiesCubit.reset(),
      nextcloudCapabilitiesCubit.reset(),
    ]);
    final prefs = await SharedPreferences.getInstance();
    await prefs.clear();
    await HydratedBloc.storage.clear();
    await const CacheView().clear();
    // The chat background image lives outside HydratedStorage, so clear it too
    // (best-effort) to avoid orphaning the previous user's wallpaper.
    final backgroundImage = File(AppPaths.chatBackgroundImage);
    if (backgroundImage.existsSync()) backgroundImage.deleteSync();
    // Stop the periodic widget refresh job so the background isolate doesn't
    // wake up every 30 minutes only to write `loggedIn=false`. Re-registers
    // on the next successful login.
    await WidgetBackgroundTask.cancelAll();
    await WidgetSync.clear();
    await WidgetSync.triggerUpdate();
  } catch (e, s) {
    log('User state wipe failed: $e', stackTrace: s);
  }
}
