import 'dart:async';

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:page_transition/page_transition.dart';
import 'package:provider/provider.dart';

import '/backend/schema/enums/enums.dart';

import '/auth/custom_auth/custom_auth_user_provider.dart';

import '/index.dart';
import '/main.dart';
import '/flutterlib/flutter_theme.dart';
import '/flutterlib/lat_lng.dart';
import '/flutterlib/place.dart';
import '/flutterlib/flutter_util.dart';
import 'serialization_util.dart';

export 'package:go_router/go_router.dart';
export 'serialization_util.dart';

const kTransitionInfoKey = '__transition_info__';

class AppStateNotifier extends ChangeNotifier {
  AppStateNotifier._();

  static AppStateNotifier? _instance;
  static AppStateNotifier get instance => _instance ??= AppStateNotifier._();

  NumstationAuthUser? initialUser;
  NumstationAuthUser? user;
  bool showSplashImage = true;
  String? _redirectLocation;

  /// Determines whether the app will refresh and build again when a sign
  /// in or sign out happens. This is useful when the app is launched or
  /// on an unexpected logout. However, this must be turned off when we
  /// intend to sign in/out and then navigate or perform any actions after.
  /// Otherwise, this will trigger a refresh and interrupt the action(s).
  bool notifyOnAuthChange = true;

  bool get loading => user == null || showSplashImage;
  bool get loggedIn => user?.loggedIn ?? false;
  bool get initiallyLoggedIn => initialUser?.loggedIn ?? false;
  bool get shouldRedirect => loggedIn && _redirectLocation != null;

  String getRedirectLocation() => _redirectLocation!;
  bool hasRedirect() => _redirectLocation != null;
  void setRedirectLocationIfUnset(String loc) => _redirectLocation ??= loc;
  void clearRedirectLocation() => _redirectLocation = null;

  /// Mark as not needing to notify on a sign in / out when we intend
  /// to perform subsequent actions (such as navigation) afterwards.
  void updateNotifyOnAuthChange(bool notify) => notifyOnAuthChange = notify;

  void update(NumstationAuthUser newUser) {
    final shouldUpdate =
        user?.uid == null || newUser.uid == null || user?.uid != newUser.uid;
    initialUser ??= newUser;
    user = newUser;
    // Refresh the app on auth change unless explicitly marked otherwise.
    // No need to update unless the user has changed.
    if (notifyOnAuthChange && shouldUpdate) {
      notifyListeners();
    }
    // Once again mark the notifier as needing to update on auth change
    // (in order to catch sign in / out events).
    updateNotifyOnAuthChange(true);
  }

  void stopShowingSplashImage() {
    showSplashImage = false;
    notifyListeners();
  }
}

GoRouter createRouter(AppStateNotifier appStateNotifier) => GoRouter(
      initialLocation: '/',
      debugLogDiagnostics: true,
      refreshListenable: appStateNotifier,
      errorBuilder: (context, state) =>
          appStateNotifier.loggedIn ? MenuWidget() : LoginWidget(),
      routes: [
        FFRoute(
          name: '_initialize',
          path: '/',
          builder: (context, _) =>
              appStateNotifier.loggedIn ? MenuWidget() : LoginWidget(),
        ),
        FFRoute(
          name: 'login',
          path: '/login',
          builder: (context, params) => LoginWidget(),
        ),
        FFRoute(
          name: 'signup',
          path: '/signup',
          builder: (context, params) => SignupWidget(),
        ),
        FFRoute(
          name: 'login1',
          path: '/login1',
          builder: (context, params) => Login1Widget(),
        ),
        FFRoute(
          name: 'login2',
          path: '/login2',
          builder: (context, params) => Login2Widget(),
        ),
        FFRoute(
          name: 'login3',
          path: '/login3',
          builder: (context, params) => Login3Widget(),
        ),
        FFRoute(
          name: 'login4',
          path: '/login4',
          builder: (context, params) => Login4Widget(),
        ),
        FFRoute(
          name: 'login5',
          path: '/login5',
          builder: (context, params) => Login5Widget(),
        ),
        FFRoute(
          name: 'tnc',
          path: '/tnc',
          builder: (context, params) => TncWidget(),
        ),
        FFRoute(
          name: 'policy',
          path: '/policy',
          builder: (context, params) => PolicyWidget(),
        ),
        FFRoute(
          name: 'menu',
          path: '/menu',
          builder: (context, params) => MenuWidget(),
        ),
        FFRoute(
          name: 'dashboard',
          path: '/dashboard',
          builder: (context, params) => DashboardWidget(),
        ),
        FFRoute(
          name: 'test',
          path: '/test',
          builder: (context, params) => TestWidget(),
        ),
        FFRoute(
          name: 'setting',
          path: '/setting',
          builder: (context, params) => SettingWidget(),
        ),
        FFRoute(
          name: 'user',
          path: '/user',
          builder: (context, params) => UserWidget(),
        ),
        FFRoute(
          name: 'bk1',
          path: '/bk1',
          builder: (context, params) => Bk1Widget(),
        ),
        FFRoute(
          name: 'enquiry',
          path: '/enquiry',
          builder: (context, params) => EnquiryWidget(),
        ),
        FFRoute(
          name: 'myenquiry',
          path: '/myenquiry',
          builder: (context, params) => MyenquiryWidget(),
        ),
        FFRoute(
          name: 'myenquiry1',
          path: '/myenquiry1',
          builder: (context, params) => Myenquiry1Widget(),
        ),
        FFRoute(
          name: 'language',
          path: '/language',
          builder: (context, params) => LanguageWidget(),
        ),
        FFRoute(
          name: 'account1',
          path: '/account1',
          builder: (context, params) => Account1Widget(),
        ),
        FFRoute(
          name: 'chatbox',
          path: '/chatbox',
          builder: (context, params) => ChatboxWidget(),
        ),
        FFRoute(
          name: 'compsec',
          path: '/compsec',
          builder: (context, params) => CompsecWidget(),
        ),
        FFRoute(
          name: 'compsec1',
          path: '/compsec1',
          builder: (context, params) => Compsec1Widget(),
        ),
        FFRoute(
          name: 'compsec2',
          path: '/compsec2',
          builder: (context, params) => Compsec2Widget(),
        ),
        FFRoute(
          name: 'compsec3',
          path: '/compsec3',
          builder: (context, params) => Compsec3Widget(),
        ),
        FFRoute(
          name: 'compsec4',
          path: '/compsec4',
          builder: (context, params) => Compsec4Widget(),
        ),
        FFRoute(
          name: 'compsec5',
          path: '/compsec5',
          builder: (context, params) => Compsec5Widget(),
        ),
        FFRoute(
          name: 'bk2',
          path: '/bk2',
          builder: (context, params) => Bk2Widget(),
        ),
        FFRoute(
          name: 'bk3',
          path: '/bk3',
          builder: (context, params) => Bk3Widget(),
        ),
        FFRoute(
          name: 'bk4',
          path: '/bk4',
          builder: (context, params) => Bk4Widget(),
        ),
        FFRoute(
          name: 'bk4_1',
          path: '/bk41',
          builder: (context, params) => Bk41Widget(),
        ),
        FFRoute(
          name: 'bk4_2',
          path: '/bk42',
          builder: (context, params) => Bk42Widget(),
        ),
        FFRoute(
          name: 'search',
          path: '/search',
          builder: (context, params) => SearchWidget(),
        ),
        FFRoute(
          name: 'bk_checkout',
          path: '/bkCheckout',
          builder: (context, params) => BkCheckoutWidget(),
        ),
        FFRoute(
          name: 'bk_checkout1',
          path: '/bkCheckout1',
          builder: (context, params) => BkCheckout1Widget(),
        ),
        FFRoute(
          name: 'compsec_checkout',
          path: '/compsecCheckout',
          builder: (context, params) => CompsecCheckoutWidget(),
        ),
        FFRoute(
          name: 'compsec_checkout1',
          path: '/compsecCheckout1',
          builder: (context, params) => CompsecCheckout1Widget(),
        ),
        FFRoute(
          name: 'compsec_checkout2',
          path: '/compsecCheckout2',
          builder: (context, params) => CompsecCheckout2Widget(),
        ),
        FFRoute(
          name: 'compsecEnq',
          path: '/compsecEnq',
          builder: (context, params) => CompsecEnqWidget(),
        ),
        FFRoute(
          name: 'compsecEnq1',
          path: '/compsecEnq1',
          builder: (context, params) => CompsecEnq1Widget(),
        ),
        FFRoute(
          name: 'compsec_changename',
          path: '/compsecChangename',
          builder: (context, params) => CompsecChangenameWidget(),
        ),
        FFRoute(
          name: 'compsec_changeaddress',
          path: '/compsecChangeaddress',
          builder: (context, params) => CompsecChangeaddressWidget(),
        ),
        FFRoute(
          name: 'compsec_changedirector',
          path: '/compsecChangedirector',
          builder: (context, params) => CompsecChangedirectorWidget(),
        ),
        FFRoute(
          name: 'compsec_changesecretary',
          path: '/compsecChangesecretary',
          builder: (context, params) => CompsecChangesecretaryWidget(),
        ),
        FFRoute(
          name: 'compsec_changesecrORdir',
          path: '/compsecChangesecrORdir',
          builder: (context, params) => CompsecChangesecrORdirWidget(),
        ),
        FFRoute(
          name: 'compsec_resignation',
          path: '/compsecResignation',
          builder: (context, params) => CompsecResignationWidget(),
        ),
        FFRoute(
          name: 'compsec_transferofShares',
          path: '/compsecTransferofShares',
          builder: (context, params) => CompsecTransferofSharesWidget(),
        ),
        FFRoute(
          name: 'compsec_otherRequest',
          path: '/compsecOtherRequest',
          builder: (context, params) => CompsecOtherRequestWidget(),
        ),
        FFRoute(
          name: 'account',
          path: '/account',
          builder: (context, params) => AccountWidget(),
        ),
        FFRoute(
          name: 'notification',
          path: '/notification',
          builder: (context, params) => NotificationWidget(),
        ),
        FFRoute(
          name: 'compsecQueue',
          path: '/compsecQueue',
          builder: (context, params) => CompsecQueueWidget(),
        ),
        FFRoute(
          name: 'compsecQueue1_1',
          path: '/compsecQueue11',
          builder: (context, params) => CompsecQueue11Widget(),
        ),
        FFRoute(
          name: 'testing',
          path: '/testing',
          builder: (context, params) => TestingWidget(),
        ),
        FFRoute(
          name: 'newuser',
          path: '/newuser',
          builder: (context, params) => NewuserWidget(
            checkPermission23:
                params.getParam('checkPermission23', ParamType.bool),
            checkPremission22:
                params.getParam('checkPremission22', ParamType.bool),
          ),
        ),
        FFRoute(
          name: 'userDetail',
          path: '/userDetail',
          builder: (context, params) => UserDetailWidget(
            firstName: params.getParam('firstName', ParamType.String),
            lastName: params.getParam('lastName', ParamType.String),
            phone: params.getParam('phone', ParamType.int),
            email: params.getParam('email', ParamType.String),
            status: params.getParam('status', ParamType.String),
          ),
        ),
        FFRoute(
          name: 'compsecDigitalTrans_list',
          path: '/compsecDigitalTransList',
          builder: (context, params) => CompsecDigitalTransListWidget(),
        ),
        FFRoute(
          name: 'compsecServicedraft',
          path: '/compsecServicedraft',
          builder: (context, params) => CompsecServicedraftWidget(),
        ),
        FFRoute(
          name: 'setting1',
          path: '/setting1',
          builder: (context, params) => Setting1Widget(),
        ),
        FFRoute(
          name: 'editUser',
          path: '/editUser',
          builder: (context, params) => EditUserWidget(
            firstName: params.getParam('firstName', ParamType.String),
            lastName: params.getParam('lastName', ParamType.String),
            phone: params.getParam('phone', ParamType.int),
            email: params.getParam('email', ParamType.String),
            userID: params.getParam('userID', ParamType.int),
            checkPermission23:
                params.getParam('checkPermission23', ParamType.bool),
            checkPremission22:
                params.getParam('checkPremission22', ParamType.bool),
            roleID: params.getParam('roleID', ParamType.String),
          ),
        ),
        FFRoute(
          name: 'bk_view',
          path: '/bkView',
          builder: (context, params) => BkViewWidget(
            fileUrl: params.getParam('fileUrl', ParamType.JSON),
          ),
        ),
        FFRoute(
          name: 'search1',
          path: '/search1',
          builder: (context, params) => Search1Widget(
            search: params.getParam('search', ParamType.String),
            status: params.getParam('status', ParamType.String),
            category: params.getParam('category', ParamType.String),
            fixDate: params.getParam('fixDate', ParamType.String),
            dateFrom: params.getParam('dateFrom', ParamType.String),
            dateTo: params.getParam('dateTo', ParamType.String),
          ),
        ),
        FFRoute(
          name: 'search_page',
          path: '/searchPage',
          builder: (context, params) => SearchPageWidget(),
        )
      ].map((r) => r.toRoute(appStateNotifier)).toList(),
    );

extension NavParamExtensions on Map<String, String?> {
  Map<String, String> get withoutNulls => Map.fromEntries(
        entries
            .where((e) => e.value != null)
            .map((e) => MapEntry(e.key, e.value!)),
      );
}

extension NavigationExtensions on BuildContext {
  void goNamedAuth(
    String name,
    bool mounted, {
    Map<String, String> pathParameters = const <String, String>{},
    Map<String, String> queryParameters = const <String, String>{},
    Object? extra,
    bool ignoreRedirect = false,
  }) =>
      !mounted || GoRouter.of(this).shouldRedirect(ignoreRedirect)
          ? null
          : goNamed(
              name,
              pathParameters: pathParameters,
              queryParameters: queryParameters,
              extra: extra,
            );

  void pushNamedAuth(
    String name,
    bool mounted, {
    Map<String, String> pathParameters = const <String, String>{},
    Map<String, String> queryParameters = const <String, String>{},
    Object? extra,
    bool ignoreRedirect = false,
  }) =>
      !mounted || GoRouter.of(this).shouldRedirect(ignoreRedirect)
          ? null
          : pushNamed(
              name,
              pathParameters: pathParameters,
              queryParameters: queryParameters,
              extra: extra,
            );

  void safePop() {
    // If there is only one route on the stack, navigate to the initial
    // page instead of popping.
    if (canPop()) {
      pop();
    } else {
      go('/');
    }
  }
}

extension GoRouterExtensions on GoRouter {
  AppStateNotifier get appState => AppStateNotifier.instance;
  void prepareAuthEvent([bool ignoreRedirect = false]) =>
      appState.hasRedirect() && !ignoreRedirect
          ? null
          : appState.updateNotifyOnAuthChange(false);
  bool shouldRedirect(bool ignoreRedirect) =>
      !ignoreRedirect && appState.hasRedirect();
  void clearRedirectLocation() => appState.clearRedirectLocation();
  void setRedirectLocationIfUnset(String location) =>
      appState.updateNotifyOnAuthChange(false);
}

extension _GoRouterStateExtensions on GoRouterState {
  Map<String, dynamic> get extraMap =>
      extra != null ? extra as Map<String, dynamic> : {};
  Map<String, dynamic> get allParams => <String, dynamic>{}
    ..addAll(pathParameters)
    ..addAll(queryParameters)
    ..addAll(extraMap);
  TransitionInfo get transitionInfo => extraMap.containsKey(kTransitionInfoKey)
      ? extraMap[kTransitionInfoKey] as TransitionInfo
      : TransitionInfo.appDefault();
}

class FFParameters {
  FFParameters(this.state, [this.asyncParams = const {}]);

  final GoRouterState state;
  final Map<String, Future<dynamic> Function(String)> asyncParams;

  Map<String, dynamic> futureParamValues = {};

  // Parameters are empty if the params map is empty or if the only parameter
  // present is the special extra parameter reserved for the transition info.
  bool get isEmpty =>
      state.allParams.isEmpty ||
      (state.extraMap.length == 1 &&
          state.extraMap.containsKey(kTransitionInfoKey));
  bool isAsyncParam(MapEntry<String, dynamic> param) =>
      asyncParams.containsKey(param.key) && param.value is String;
  bool get hasFutures => state.allParams.entries.any(isAsyncParam);
  Future<bool> completeFutures() => Future.wait(
        state.allParams.entries.where(isAsyncParam).map(
          (param) async {
            final doc = await asyncParams[param.key]!(param.value)
                .onError((_, __) => null);
            if (doc != null) {
              futureParamValues[param.key] = doc;
              return true;
            }
            return false;
          },
        ),
      ).onError((_, __) => [false]).then((v) => v.every((e) => e));

  dynamic getParam<T>(
    String paramName,
    ParamType type, [
    bool isList = false,
  ]) {
    if (futureParamValues.containsKey(paramName)) {
      return futureParamValues[paramName];
    }
    if (!state.allParams.containsKey(paramName)) {
      return null;
    }
    final param = state.allParams[paramName];
    // Got parameter from `extras`, so just directly return it.
    if (param is! String) {
      return param;
    }
    // Return serialized value.
    return deserializeParam<T>(
      param,
      type,
      isList,
    );
  }
}

class FFRoute {
  const FFRoute({
    required this.name,
    required this.path,
    required this.builder,
    this.requireAuth = false,
    this.asyncParams = const {},
    this.routes = const [],
  });

  final String name;
  final String path;
  final bool requireAuth;
  final Map<String, Future<dynamic> Function(String)> asyncParams;
  final Widget Function(BuildContext, FFParameters) builder;
  final List<GoRoute> routes;

  GoRoute toRoute(AppStateNotifier appStateNotifier) => GoRoute(
        name: name,
        path: path,
        redirect: (context, state) {
          if (appStateNotifier.shouldRedirect) {
            final redirectLocation = appStateNotifier.getRedirectLocation();
            appStateNotifier.clearRedirectLocation();
            return redirectLocation;
          }

          if (requireAuth && !appStateNotifier.loggedIn) {
            appStateNotifier.setRedirectLocationIfUnset(state.location);
            return '/login';
          }
          return null;
        },
        pageBuilder: (context, state) {
          final ffParams = FFParameters(state, asyncParams);
          final page = ffParams.hasFutures
              ? FutureBuilder(
                  future: ffParams.completeFutures(),
                  builder: (context, _) => builder(context, ffParams),
                )
              : builder(context, ffParams);
          final child = appStateNotifier.loading
              ? Center(
                  child: SizedBox(
                    width: 50.0,
                    height: 50.0,
                    child: CircularProgressIndicator(
                      valueColor: AlwaysStoppedAnimation<Color>(
                        FlutterTheme.of(context).info,
                      ),
                    ),
                  ),
                )
              : page;

          final transitionInfo = state.transitionInfo;
          return transitionInfo.hasTransition
              ? CustomTransitionPage(
                  key: state.pageKey,
                  child: child,
                  transitionDuration: transitionInfo.duration,
                  transitionsBuilder:
                      (context, animation, secondaryAnimation, child) =>
                          PageTransition(
                    type: transitionInfo.transitionType,
                    duration: transitionInfo.duration,
                    reverseDuration: transitionInfo.duration,
                    alignment: transitionInfo.alignment,
                    child: child,
                  ).buildTransitions(
                    context,
                    animation,
                    secondaryAnimation,
                    child,
                  ),
                )
              : MaterialPage(key: state.pageKey, child: child);
        },
        routes: routes,
      );
}

class TransitionInfo {
  const TransitionInfo({
    required this.hasTransition,
    this.transitionType = PageTransitionType.fade,
    this.duration = const Duration(milliseconds: 300),
    this.alignment,
  });

  final bool hasTransition;
  final PageTransitionType transitionType;
  final Duration duration;
  final Alignment? alignment;

  static TransitionInfo appDefault() => TransitionInfo(hasTransition: false);
}

class RootPageContext {
  const RootPageContext(this.isRootPage, [this.errorRoute]);
  final bool isRootPage;
  final String? errorRoute;

  static bool isInactiveRootPage(BuildContext context) {
    final rootPageContext = context.read<RootPageContext?>();
    final isRootPage = rootPageContext?.isRootPage ?? false;
    final location = GoRouter.of(context).location;
    return isRootPage &&
        location != '/' &&
        location != rootPageContext?.errorRoute;
  }

  static Widget wrap(Widget child, {String? errorRoute}) => Provider.value(
        value: RootPageContext(true, errorRoute),
        child: child,
      );
}
