import 'dart:async';

import 'package:flutter/foundation.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 'package:url_launcher/url_launcher_string.dart';

import '../api/marianumcloud/talk/room/get_room_response.dart';
import '../api/marianumconnect/marianumconnect_endpoint.dart';
import '../api/marianumconnect/queries/timetable_get_element_week/timetable_element_type.dart';
import '../main.dart';
import '../model/account_data.dart';
import '../notification/notification_tasks.dart';
import '../share_intent/pending_share.dart';
import '../share_intent/remote_file_ref.dart';
import '../state/app/modules/app_modules.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/marianum_message/bloc/marianum_message_state.dart';
import '../view/login/nextcloud_login_flow_page.dart';
import '../view/pages/files/files.dart';
import '../view/pages/files/sharing/sharee_picker_page.dart';
import '../view/pages/foreign_timetable/element_picker_page.dart';
import '../view/pages/marianum_message/marianum_message_view.dart';
import '../view/pages/more/feedback/feedback_dialog.dart';
import '../view/pages/more/roomplan/roomplan.dart';
import '../view/pages/more/share/qr_share_view.dart';
import '../view/pages/settings/chat_background_settings_page.dart';
import '../view/pages/settings/modules_settings_page.dart';
import '../view/pages/settings/settings.dart';
import '../view/pages/share_intent/share_chat_picker.dart';
import '../view/pages/share_intent/share_folder_picker.dart';
import '../view/pages/share_intent/share_target_page.dart';
import '../view/pages/talk/chat_view.dart';
import '../view/pages/talk/details/message_reactions.dart';
import '../view/pages/talk/talk_navigator.dart';
import '../view/pages/ticker/ticker_page_view.dart';
import '../view/pages/timetable/custom_events/custom_events_view.dart';
import '../view/pages/timetable/subject_colors/subject_colors_view.dart';
import '../widget/avatar_crop_page.dart';
import '../widget/debug/cache_view.dart';
import '../widget/file_viewer.dart';
import '../widget/large_profile_picture_view.dart';
import '../widget/user_avatar.dart';

/// Single entry point for full-page navigations. Dialogs and bottom sheets
/// stay at the call sites; only full-page pushes go through here.
class AppRoutes {
  AppRoutes._();

  /// Set by [openChatByToken] (e.g. from a tapped notification) and consumed
  /// by `ChatList` once the matching room is loaded.
  static final ValueNotifier<String?> pendingChatToken = ValueNotifier(null);

  /// Root-navigator observer used by [ChatView] to reclaim the global
  /// [ChatBloc] on `didPopNext` after a stacked chat is popped.
  static final RouteObserver<PageRoute<dynamic>> chatRouteObserver =
      RouteObserver<PageRoute<dynamic>>();

  /// Root navigator key, set on [MaterialApp]. Lets globally-mounted UI (e.g.
  /// the downloads tray, which lives above the navigator in `MaterialApp.builder`
  /// and is therefore never covered by a pushed route) open full-page routes.
  static final GlobalKey<NavigatorState> rootNavigatorKey =
      GlobalKey<NavigatorState>();

  /// A context that is a descendant of the root navigator (its overlay), safe to
  /// pass to `pushScreen`/`showModalBottomSheet` from outside the route tree.
  static BuildContext? get overlayContext =>
      rootNavigatorKey.currentState?.overlay?.context;

  static void openFolder(BuildContext context, List<String> path) {
    pushScreen(context, withNavBar: false, screen: Files(path: path));
  }

  static void openFileViewer(
    BuildContext context,
    String localPath, {
    bool openExternal = false,
    RemoteFileRef? remoteFile,
  }) {
    pushScreen(
      context,
      withNavBar: false,
      screen: FileViewer(
        path: localPath,
        openExternal: openExternal,
        remoteFile: remoteFile,
      ),
    );
  }

  static void openCustomEvents(BuildContext context) {
    pushScreen(context, withNavBar: false, screen: const CustomEventsView());
  }

  static void openSubjectColors(BuildContext context) {
    pushScreen(context, withNavBar: false, screen: const SubjectColorsView());
  }

  /// Opens the full-screen cropper on [imageBytes] and resolves to the cropped
  /// bytes (or null if cancelled). [aspectRatio] defaults to 1:1 for avatars;
  /// pass null for a free-form crop (e.g. chat backgrounds).
  static Future<Uint8List?> openAvatarCrop(
    BuildContext context, {
    required Uint8List imageBytes,
    double? aspectRatio = 1,
  }) {
    return Navigator.of(context).push<Uint8List>(
      MaterialPageRoute(
        fullscreenDialog: true,
        builder: (_) =>
            AvatarCropPage(imageBytes: imageBytes, aspectRatio: aspectRatio),
      ),
    );
  }

  /// Runs the Nextcloud Login Flow v2 (browser login, e.g. for accounts with
  /// two-factor authentication) and resolves to `true` once an app password
  /// was adopted. Used from the login flow and the settings "reconnect"
  /// action.
  static Future<bool> openNextcloudLoginFlow(BuildContext context) async {
    final result = await Navigator.of(context).push<bool>(
      MaterialPageRoute(
        fullscreenDialog: true,
        builder: (_) => const NextcloudLoginFlowPage(),
      ),
    );
    return result ?? false;
  }

  /// Opens the tappable, zoomable profile-picture viewer for [id].
  static void openLargeProfilePicture(BuildContext context, String id) {
    Navigator.of(context).push(
      MaterialPageRoute<void>(builder: (_) => LargeProfilePictureView(id: id)),
    );
  }

  /// Opens the picker for choosing a foreign timetable element and resolves to
  /// the selected element (or null if dismissed). The timetable view renders
  /// the chosen plan inline. Gated behind the `viewForeignTimetables`
  /// capability at the call site.
  static Future<TimetableElementRef?> openElementPicker(
    BuildContext context,
  ) async {
    // pushScreen casts its internal MaterialPageRoute to `Route<T>`, which
    // blows up for a concrete (record) T — so push untyped (T = dynamic, like
    // every other caller) and cast the popped result ourselves.
    final result = await pushScreen(
      context,
      withNavBar: false,
      screen: const ElementPickerPage(),
    );
    return result as TimetableElementRef?;
  }

  /// Opens the user/group picker for sharing a file and resolves to the chosen
  /// recipient (or null if dismissed). Which kinds are offered is gated by the
  /// Nextcloud sharing capabilities at the call site.
  static Future<ShareeRef?> openShareePicker(
    BuildContext context, {
    required bool allowUsers,
    required bool allowGroups,
  }) async {
    // Push untyped (T = dynamic) and cast the popped result ourselves —
    // pushScreen can't carry a concrete non-Widget T (see openElementPicker).
    final result = await pushScreen(
      context,
      withNavBar: false,
      screen: ShareePickerPage(
        allowUsers: allowUsers,
        allowGroups: allowGroups,
      ),
    );
    return result as ShareeRef?;
  }

  static void openMarianumMessage(
    BuildContext context,
    MarianumMessage message,
  ) {
    pushScreen(
      context,
      withNavBar: false,
      screen: MessageView(id: message.id, title: message.name),
    );
  }

  /// Opens a Marianum Message by id — used for push deep links where only the
  /// id (and the notification title) are known.
  static void openNewsletterById(
    BuildContext context, {
    required String id,
    String? title,
  }) {
    pushScreen(
      context,
      withNavBar: false,
      screen: MessageView(
        id: id,
        title: title == null || title.isEmpty ? 'Marianum Message' : title,
      ),
    );
  }

  /// Opens a ticker page (CONTENT or PROXIED_FILE) as a standalone detail
  /// screen. Used for deep links from outside the ticker module — inside the
  /// module pages open in-place instead.
  static void openTickerPage(
    BuildContext context, {
    required String slug,
    String? title,
  }) {
    pushScreen(
      context,
      withNavBar: false,
      screen: TickerPageView(slug: slug, title: title),
    );
  }

  /// Resolves a link tapped inside ticker content. Links that point at another
  /// ticker page (absolute against the active base URL, or site-relative
  /// `/ticker/p/{slug}`) open in-app; everything else goes to the external
  /// launcher.
  static void openTickerLink(BuildContext context, String href) {
    final slug = tickerSlugOf(href);
    if (slug != null && slug.isNotEmpty) {
      openTickerPage(context, slug: slug);
      return;
    }
    unawaited(openExternalUrl(href));
  }

  /// Extracts the ticker page slug from a link that points at another ticker
  /// page (absolute against the active base URL, or site-relative
  /// `/ticker/p/{slug}`), or null for any other link. Exposed so the in-place
  /// ticker navigation can reuse the same detection instead of duplicating it.
  static String? tickerSlugOf(String href) {
    const marker = '/ticker/p/';
    final base = MarianumConnectEndpoint.current();
    String? rest;
    if (href.startsWith('$base$marker')) {
      rest = href.substring('$base$marker'.length);
    } else if (href.startsWith(marker)) {
      rest = href.substring(marker.length);
    }
    if (rest == null) return null;
    rest = rest.split('#').first.split('?').first;
    if (rest.isEmpty) return null;
    return Uri.decodeComponent(rest);
  }

  /// Launches an external URL, restricted to safe schemes (http/https/mailto/tel).
  static Future<void> openExternalUrl(String url) async {
    final uri = Uri.tryParse(url);
    if (uri == null) return;
    const allowed = {'http', 'https', 'mailto', 'tel'};
    if (!allowed.contains(uri.scheme.toLowerCase())) return;
    if (await canLaunchUrlString(url)) {
      await launchUrlString(url, mode: LaunchMode.externalApplication);
    }
  }

  /// Opens a possibly site-relative web URL (e.g. the ticker `webUrl`) by
  /// prefixing the active Marianum-Connect base URL when needed.
  static Future<void> openWebUrl(String relativeOrAbsolute) {
    if (relativeOrAbsolute.startsWith('http')) {
      return openExternalUrl(relativeOrAbsolute);
    }
    return openExternalUrl(
      '${MarianumConnectEndpoint.current()}$relativeOrAbsolute',
    );
  }

  static void openQrShare(BuildContext context) {
    pushScreen(context, withNavBar: false, screen: const QrShareView());
  }

  static void openSettings(BuildContext context) {
    pushScreen(context, withNavBar: false, screen: const Settings());
  }

  static void openModulesSettings(BuildContext context) {
    pushScreen(context, withNavBar: false, screen: const ModulesSettingsPage());
  }

  static void openChatBackgroundSettings(BuildContext context) {
    pushScreen(
      context,
      withNavBar: false,
      screen: const ChatBackgroundSettingsPage(),
    );
  }

  static void openFeedback(BuildContext context) {
    pushScreen(context, withNavBar: false, screen: const FeedbackDialog());
  }

  static void openCacheView(BuildContext context) {
    pushScreen(context, withNavBar: false, screen: const CacheView());
  }

  static void openRoomplan(BuildContext context) {
    pushScreen(context, withNavBar: false, screen: const Roomplan());
  }

  static void openShareTarget(BuildContext context, PendingShare share) {
    pushScreen(
      context,
      withNavBar: false,
      screen: ShareTargetPage(share: share),
    );
  }

  static void openShareChatPicker(BuildContext context, PendingShare share) {
    pushScreen(
      context,
      withNavBar: false,
      screen: ShareChatPicker.forExternalShare(share: share),
    );
  }

  static void openShareFolderPicker(BuildContext context, PendingShare share) {
    pushScreen(
      context,
      withNavBar: false,
      screen: ShareFolderPicker.forExternalShare(share: share),
    );
  }

  static void openInternalShareToChat(
    BuildContext context,
    RemoteFileRef file,
  ) {
    pushScreen(
      context,
      withNavBar: false,
      screen: ShareChatPicker.forInternalShare(file: file),
    );
  }

  static void openForwardMessageToChat(
    BuildContext context, {
    String? text,
    RemoteFileRef? file,
  }) {
    pushScreen(
      context,
      withNavBar: false,
      screen: ShareChatPicker.forMessageForward(text: text, file: file),
    );
  }

  static void openInternalSaveToFolder(
    BuildContext context,
    RemoteFileRef file,
  ) {
    pushScreen(
      context,
      withNavBar: false,
      screen: ShareFolderPicker.forInternalSave(file: file),
    );
  }

  static void openMessageReactions(
    BuildContext context,
    String token,
    int messageId,
  ) {
    pushScreen(
      context,
      withNavBar: false,
      screen: MessageReactions(token: token, messageId: messageId),
    );
  }

  static void openChatView(
    BuildContext context, {
    required GetRoomResponseObject room,
    required String selfId,
    required UserAvatar avatar,
    bool overrideToSingleSubScreen = true,
  }) {
    // Local mark only. Server-side mark is sent later from
    // ChatBloc._loadChat with the freshly-fetched maxId — sending one
    // here too with the chat list's possibly-stale room.lastMessage.id
    // would race the fresh one and could regress the server cursor.
    context.read<ChatListBloc>().markRoomAsRead(room.token, room.lastMessage.id);
    NotificationTasks.clearNotificationsForChat(room.token);
    TalkNavigator.pushSplitView(
      context,
      ChatView(room: room, selfId: selfId, avatar: avatar),
      overrideToSingleSubScreen: overrideToSingleSubScreen,
    );
    context.read<ChatBloc>().setToken(room.token);
  }

  /// Schedules a chat to be opened in the Talk tab once the room is loaded.
  /// Use when only the token is known (e.g. from a tapped notification).
  static void openChatByToken(BuildContext context, String token) {
    pendingChatToken.value = token;
    goToTalkTab(context);
    try {
      context.read<ChatListBloc>().refresh();
    } catch (e) {
      if (kDebugMode) {
        debugPrint('openChatByToken: ChatListBloc refresh failed: $e');
      }
    }
  }

  /// Resolves a pending chat token (set via [openChatByToken]). Returns null
  /// if the room or account is not ready yet — callers should retry when
  /// [pendingChatToken] or the bloc state change.
  static ResolvedPendingChat? resolvePendingChat(BuildContext context) {
    final token = pendingChatToken.value;
    if (token == null) return null;
    if (!AccountData().isPopulated()) return null;

    final rooms = context.read<ChatListBloc>().state.data?.rooms;
    final room = _findRoomByToken(rooms, token);
    if (room == null) return null;

    final isGroup = room.type != GetRoomResponseObjectConversationType.oneToOne;
    final avatar = UserAvatar(
      id: isGroup ? room.token : room.name,
      isGroup: isGroup,
    );
    return ResolvedPendingChat(
      room: room,
      selfId: AccountData().getUsername(),
      avatar: avatar,
    );
  }

  static GetRoomResponseObject? _findRoomByToken(
    GetRoomResponse? rooms,
    String token,
  ) {
    if (rooms == null) return null;
    for (final room in rooms.data) {
      if (room.token == token) return room;
    }
    return null;
  }

  /// Pushes a module from the "Mehr" tab list. Modules already in the bottom
  /// bar are switched to via [goToTab] instead.
  static void openModule(BuildContext context, AppModule module) {
    pushScreen(context, withNavBar: false, screen: module.create());
  }

  /// Switches the bottom navigation to [module]. Returns false when the
  /// module is not currently in the bottom bar.
  static bool goToTab(BuildContext context, Modules module) {
    final index = AppModule.getBottomBarModules(
      context,
    ).indexWhere((e) => e.module == module);
    if (index == -1) return false;
    Main.bottomNavigator.jumpToTab(index);
    return true;
  }

  static void goToTalkTab(BuildContext context) {
    goToTab(context, Modules.talk);
  }
}

class ResolvedPendingChat {
  final GetRoomResponseObject room;
  final String selfId;
  final UserAvatar avatar;

  const ResolvedPendingChat({
    required this.room,
    required this.selfId,
    required this.avatar,
  });
}
