import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:kasy_kit/core/config/features.dart';
import 'package:kasy_kit/core/data/models/subscription.dart';
import 'package:kasy_kit/core/data/models/user.dart';
import 'package:kasy_kit/core/data/repositories/user_repository.dart';
import 'package:kasy_kit/core/initializer/onstart_service.dart';
import 'package:kasy_kit/core/shared_preferences/shared_preferences.dart';
import 'package:kasy_kit/core/states/models/user_state.dart';
import 'package:kasy_kit/environnements.dart';
import 'package:kasy_kit/features/authentication/repositories/authentication_repository.dart';
import 'package:kasy_kit/features/notifications/providers/models/device.dart';
import 'package:kasy_kit/features/notifications/repositories/device_repository.dart';
import 'package:logger/logger.dart';
import 'package:purchases_flutter/models/entitlement_info_wrapper.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'user_state_notifier.g.dart';

/// Use this provider to access the user state (legacy name)
final userStateNotifierProvider = userStateProvider;

/// This enum is used to parameter the list of the authentication mode
///
/// Most of apps try to not force user to create an account to access the app
/// But you may want to force the user to be authenticated to access the app
enum AuthenticationMode {
  /// By default the user will be authenticated anonymously
  /// This means that the user can access your app without login
  /// He will be able to link his account later to an email or social login
  anonymous,

  /// The user requires to be authenticated to access the app
  /// By default the user won't have any identity
  authRequired,
}

/// This class is responsible for managing the state of the user over the app.
/// It will be used to know if the user is connected or not and to get the user
@Riverpod(keepAlive: true)
class UserStateNotifier extends _$UserStateNotifier implements OnStartService {
  Logger get _logger => Logger();

  AuthenticationRepository get _authenticationRepository =>
      ref.read(authRepositoryProvider);

  DeviceRepository get _deviceRepository => ref.read(deviceRepositoryProvider);

  UserRepository get _userRepository => ref.read(userRepositoryProvider);

  /// The authentication mode of the app.
  /// On web (when withWeb is true), always require explicit auth —
  /// anonymous accounts are not used on web.
  /// see [AuthenticationMode]
  AuthenticationMode get mode {
    if (kIsWeb && withWeb) return AuthenticationMode.authRequired;
    return ref.read(environmentProvider).authenticationMode;
  }

  @override
  UserState build() {
    return const UserState(user: User.loading());
  }

  @override
  Future<void> init() async {
    try {
      await _loadState();
    } catch (e, stacktrace) {
      _logger.e(e, stackTrace: stacktrace);
      if (kDebugMode) {
        // we automatically logout the user if an error occurs in debug mode
        // customize this behavior to fit your needs
        _authenticationRepository.logout();
      }
      rethrow;
    }
    assert(state.user is! LoadingUserData, 'UserStateNotifier is not ready');
    await _initDeviceRegistration();
    _deviceRepository.onTokenUpdate(_onUpdateToken);
  }

  /// This function is called when the user click on the signin button
  /// It will load the user state and register the device to the notification service
  /// It will also load the subscription state
  Future<void> onSignin() async {
    state = const UserState(user: User.loading());
    await _loadState();
    await _initDeviceRegistration();
  }

  /// Set the user as onboarded in the database
  /// This function is called when the user has completed the onboarding
  Future<void> onOnboarded() async {
    if (state.user.idOrNull == null) {
      // Guest with no account: persist onboarding in local state only.
      // No Firestore document exists yet — no write needed.
      state = state.copyWith(user: const User.anonymous(onboarded: true));
      return;
    }
    final newUser = await _userRepository.setOnboarded(state.user);
    state = state.copyWith(user: newUser);
  }

  /// Mark the user as onboarded immediately (optimistic) and write to the
  /// backend in background. Used by the skip-onboarding flow so navigation
  /// to home is instant — no spinner while waiting for a network round-trip.
  void onSkippedOnboarding() {
    state = state.copyWith(
      user: switch (state.user) {
        final AuthenticatedUserData u => u.copyWith(onboarded: true),
        final AnonymousUserData u => u.copyWith(onboarded: true),
        final LoadingUserData _ => const User.anonymous(onboarded: true),
      },
    );
    final id = state.user.idOrNull;
    if (id != null) {
      unawaited(_userRepository.setOnboarded(state.user));
    }
  }

  /// This function is called when the user click on the logout button
  /// It will unregister the device from the notification service
  /// and logout the user
  Future<void> onLogout() async {
    final userId = state.user.idOrThrow;
    _deviceRepository.removeTokenUpdateListener();
    // Best-effort: if the network call fails we still proceed with logout so
    // the user is never stuck on the previous account. A stale device doc on
    // the old user is cleaned up server-side by the cross-user token dedup
    // trigger when the same install registers under a new account.
    try {
      await _deviceRepository.unregister(userId);
    } catch (e) {
      _logger.w('Failed to unregister device during logout: $e');
    }
    await _authenticationRepository.logout();
    // Biometric lock is a per-account preference, not a device-wide one.
    // The next user signing in on this install should start without it set.
    await ref.read(sharedPreferencesProvider).setBiometricEnabled(false);
    state = const UserState(user: User.anonymous());
    if (mode == AuthenticationMode.anonymous) {
      await _loadAnonymousState();
    }
  }

  /// Refresh the user
  Future<void> onUpdateAvatar() async {
    await refresh();
  }

  /// Refresh the user
  Future<void> refresh() async {
    final id = state.user.idOrNull;
    if (id == null) return; // Guest: no Firestore document, nothing to refresh
    final user = await _userRepository.get(id);
    // Preserve the id if the Firestore document is not found (e.g. backend not
    // yet deployed), so the user never loses their session identity.
    state = state.copyWith(user: user ?? User.anonymous(id: id));
  }

  /// This function is called after a user successfuly purchased a subscription
  /// It will refresh the subscription state without waiting for the webhook
  /// (which can take some time and could show a wrong state to the user)
  /// On next app start, the subscription will be refreshed from the webhook result
  Future<void> refreshSubscription({
    SubscriptionProduct? product,
    List<EntitlementInfo>? entitlements,
  }) async {
    if (product != null) {
      final newUser = switch (state.user) {
        final AuthenticatedUserData user => user.copyWith(
          subscription: Subscription.active(
            activeOffer: product,
            entitlements: entitlements,
          ),
        ),
        final AnonymousUserData user => user.copyWith(
          subscription: Subscription.active(
            activeOffer: product,
            entitlements: entitlements,
          ),
        ),
        final LoadingUserData _ => throw 'User is not connected',
      };
      state = state.copyWith(user: newUser);
      return;
    }
    await Future.delayed(const Duration(seconds: 2));
    await _loadState();
  }

  /// Apple store and Google play stores requires you to be able to delete a user account on demand
  /// Here is the function to do it.
  /// It will delete the user account and logout the user
  Future<void> deleteAccount() async {
    final userId = state.user.idOrThrow;
    _deviceRepository.removeTokenUpdateListener();
    try {
      await _deviceRepository.unregister(userId);
    } catch (e) {
      _logger.w('Failed to unregister device during account deletion: $e');
    }
    await _userRepository.delete();
    await _authenticationRepository.logout();
    state = const UserState(user: User.anonymous());

    if (mode == AuthenticationMode.anonymous) {
      await _loadAnonymousState();
    }
  }

  // -------------------------------
  // PRIVATES
  // -------------------------------

  /// load anonymous state for the user
  Future<void> _loadAnonymousState() async {
    await _authenticationRepository.signupAnonymously();
    await Future.delayed(const Duration(seconds: 1));
    final credentials = await _authenticationRepository.get();
    var user = await _userRepository.get(credentials!.id);
    // We retry to get the user 3 times (sometimes the user creation trigger is not fast enough)
    var retry = 0;
    while (user == null && retry < 3) {
      await Future.delayed(const Duration(seconds: 1));
      user = await _userRepository.get(credentials.id);
      retry++;
    }
    if (user == null) {
      // Backend not deployed yet: the Cloud Function onUserRegistration has not
      // created the Firestore document. Log the issue for the developer and fall
      // back to a local anonymous state so the app still opens.
      _logger.e(
        '⚠️  User profile not found in database.\n'
        '    The backend may not be deployed yet.\n'
        '    Run: kasy deploy\n'
        '    Then restart the app.',
      );
      state = state.copyWith(user: User.anonymous(id: credentials.id));
      return;
    }
    state = state.copyWith(user: user);
  }

  /// Load the state of the user
  Future<void> _loadState() async {
    final credentials = await _authenticationRepository.get();

    if (credentials == null && mode == AuthenticationMode.anonymous) {
      _logger.i('Anonymous user mode activated, signup anonymously');
      await _loadAnonymousState();
    } else if (credentials == null && mode == AuthenticationMode.authRequired) {
      _logger.i('Authentification required, user is not connected');
      state = state.copyWith(user: const User.anonymous());
    } else if (credentials != null) {
      _logger.i('User is connected with id ${credentials.id}');
      var user = await _userRepository.get(credentials.id);
      // Retry a few times: the Cloud Function onUserRegistration may not have
      // finished creating the Firestore document yet (race condition on first login).
      var retry = 0;
      while (user == null && retry < 3) {
        await Future.delayed(const Duration(seconds: 1));
        user = await _userRepository.get(credentials.id);
        retry++;
      }
      if (user == null) {
        // User document not found after login. Possible causes:
        // - Firebase: onUserRegistration Cloud Function not deployed or failed
        // - Supabase: handle_new_user trigger not run yet (race) or failed
        // - User was deleted but session was still cached
        // Logout to clear invalid session, then create new anonymous user if applicable.
        _logger.e(
          '⚠️  User profile not found in database after sign-in.\n'
          '    Logging out and creating new anonymous user.',
        );
        await _authenticationRepository.logout();
        state = state.copyWith(user: const User.anonymous());
        if (mode == AuthenticationMode.anonymous) {
          await _loadAnonymousState();
        }
        return;
      }
      // If the Firestore document is anonymous (no email) but Firebase Auth
      // already has an email (happens after linkWithProvider — Google, email, etc.),
      // sync the email and name so the user is properly recognised as authenticated.
      if (user is AnonymousUserData) {
        final authEmail = await _authenticationRepository.getCurrentUserEmail();
        if (authEmail != null && authEmail.isNotEmpty) {
          final authName = await _authenticationRepository
              .getCurrentUserDisplayName();
          await _userRepository.updateEmailAndName(
            userId: credentials.id,
            email: authEmail,
            name: authName,
          );
          final syncedUser = await _userRepository.get(credentials.id);
          state = state.copyWith(user: syncedUser ?? user);
          return;
        }
      }
      state = state.copyWith(user: user);
    }
  }

  /// If user has an ID we will register his device to send notifications from
  /// the server to the device (only if user has accepted them)
  /// Maybe save your device in UserState if you need it in your app
  Future<void> _initDeviceRegistration() async {
    if (kIsWeb) return;
    final userId = state.user.idOrNull;
    if (userId == null) {
      return;
    }
    try {
      final _ = await _deviceRepository.register(userId);
    } catch (err, stacktrace) {
      _logger.e(err, stackTrace: stacktrace);
      _logger.e('''
          ❌ Your device seems not to be registered.
          Check that you correctly setup a device registration API
          see: `lib/features/notifications/api/device_api.dart`
        ''');
    }
  }

  /// This function is called when the device token is updated
  /// It will update the token in the database
  Future<void> _onUpdateToken(Device device) async {
    await _deviceRepository.updateToken(device.token);
  }
}
