import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:cloud_functions/cloud_functions.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:kasy_kit/core/data/api/base_api_exceptions.dart';
import 'package:kasy_kit/features/notifications/api/entities/notifications_entity.dart';
import 'package:logger/logger.dart';
import 'package:permission_handler/permission_handler.dart';

final notificationsApiProvider = Provider<NotificationsApi>(
  (ref) => FirebaseNotificationsApi(
    messaging: FirebaseMessaging.instance,
    logger: Logger(),
    client: FirebaseFirestore.instance,
  ),
);

/// This class is responsible for listening from firebase notifications
/// As I like repositories to not depend on external libraries
/// I wrapped some of the firebase messaging methods
///
/// You could use directly the firebase messaging methods but making a fake implementation
/// of this class would be harder.
abstract class NotificationsApi {
  /// Request permission to receive notifications
  Future<void> requestPermission();

  // Used to listen to notifications when the app is in foreground
  void setForegroundHandler(OnRemoteMessage handler);
  // Used to listen to notifications when the app is in background
  void setBackgroundHandler(OnRemoteMessage handler);
  // Used to listen to notifications when user clicks on the notification
  void setOnOpenNotificationHandler(OnRemoteMessage handler);

  // Used to get the initial message from the app 
  // (notification opened when the app is not ready)
  Future<RemoteMessage?> getInitialMessage();

  // Used to get the past notifications from the server
  Future<List<NotificationEntity>> get(
    String userId, {
    DateTime? startDate,
    required int limit,
    int page = 0, // don't use this in firebase mode
  });

  // Used to mark a notification as read
  Future<void> read(String userId, String notificationId);
  // Used to delete a single notification (per-user only)
  Future<void> delete(String userId, String notificationId);
  // Used to get the unread notifications count
  Stream<int> unreadNotifications(String userId);

  // Used to register to a topic
  // (this allows you to send notifications to a group of users)
  // You can create one general topic for each language
  void registerTopic(String topic);

  // Used to unregister from a topic
  void unregisterTopic(String topic);

  // Used to get the permission status
  Future<PermissionStatus> getPermissionStatus();

  // Admin: find a user's ID by their email address
  Future<String?> findUserByEmail(String email);

  // Admin: create a notification for a specific user (triggers push via Cloud Function / webhook)
  Future<void> sendNotification(
    String userId, {
    required String title,
    required String body,
    String? imageUrl,
    String? route,
  });

  // Admin: send a notification to a list of emails (resolves emails server-side when possible)
  Future<void> sendNotificationToEmails(
    List<String> emails, {
    required String title,
    required String body,
    String? imageUrl,
    String? route,
  });

  // Admin: create a notification for every registered user
  Future<void> sendNotificationToAll({
    required String title,
    required String body,
    String? imageUrl,
    String? route,
  });
}

typedef OnRemoteMessage = Future<void> Function(RemoteMessage message);

class FirebaseNotificationsApi implements NotificationsApi {
  final FirebaseMessaging _messaging;
  final FirebaseFirestore _client;
  final Logger _logger;

  FirebaseNotificationsApi({
    required FirebaseMessaging messaging,
    required FirebaseFirestore client,
    required Logger logger,
  })  : _messaging = messaging,
        _client = client,
        _logger = logger;

  CollectionReference<NotificationEntity?> _collection(String userId) => _client
      .collection('users')
      .doc(userId)
      .collection('notifications')
      .withConverter(
        fromFirestore: (snapshot, _) {
          if (snapshot.exists) {
            return NotificationEntity.fromJson(snapshot.id, snapshot.data()!);
          }
          return null;
        },
        toFirestore: (data, _) => data!.toJson(),
      );

  @override
  Future<void> requestPermission() async {
    try {
      await _messaging.requestPermission();
    } catch (e) {
      _logger.e(e);
    }
  }

  @override
  void setForegroundHandler(OnRemoteMessage handler) {
    FirebaseMessaging.onMessage.listen(handler);
  }

  @override
  void setBackgroundHandler(OnRemoteMessage handler) {
    FirebaseMessaging.onBackgroundMessage(handler);
  }

  @override
  void setOnOpenNotificationHandler(OnRemoteMessage handler) {
    FirebaseMessaging.onMessageOpenedApp.listen(handler);
  }

  @override
  Future<RemoteMessage?> getInitialMessage() {
    return _messaging.getInitialMessage();
  }

  @override
  Future<List<NotificationEntity>> get(
    String userId, {
    DateTime? startDate,
    required int limit,
    int page = 0,
  }) async {
    try {
      _logger.i('get notifications for user $userId');
      var query = _collection(userId) //
          .orderBy('creation_date', descending: true);
      if (startDate != null) {
        query = query.startAfter([startDate.add(const Duration(seconds: -1))]);
      }
      final value = await query.limit(limit).get();
      return value.docs
          .map((e) {
            try {
              return e.data();
            } catch (_) {
              return null;
            }
          })
          .whereType<NotificationEntity>()
          .toList();
    } catch (e, stacktrace) {
      _logger.e('$e: $stacktrace');
      throw ApiError(
        code: 0,
        message: '$e: $stacktrace',
      );
    }
  }

  @override
  Future<void> read(String userId, String notificationId) async {
    try {
      await _collection(userId).doc(notificationId).update({
        'seen_date': DateTime.now(),
      });
    } catch (e, stacktrace) {
      throw ApiError(
        code: 0,
        message: '$e: $stacktrace',
      );
    }
  }

  @override
  Future<void> delete(String userId, String notificationId) async {
    try {
      await _collection(userId).doc(notificationId).delete();
    } catch (e, stacktrace) {
      throw ApiError(
        code: 0,
        message: '$e: $stacktrace',
      );
    }
  }

  @override
  Stream<int> unreadNotifications(String userId) async* {
    // count() aggregation: 1 read per 1000 docs counted (vs N reads for full snapshot).
    // Trade-off: badge refreshes every 30s instead of instantly. FCM push still alerts
    // the user immediately. Ideal fix: maintain a counter field on the user document.
    final query = _client
        .collection('users')
        .doc(userId)
        .collection('notifications')
        .where('seen_date', isNull: true);
    while (true) {
      try {
        final agg = await query.count().get();
        yield agg.count ?? 0;
      } on FirebaseException catch (e) {
        if (e.code == 'permission-denied') return;
        _logger.e('unreadNotifications error: $e');
      } on PlatformException catch (e) {
        if (e.code == 'permission-denied') return;
        _logger.e('unreadNotifications error: $e');
      } catch (e, s) {
        _logger.e('unreadNotifications error: $e: $s');
      }
      await Future.delayed(const Duration(seconds: 30));
    }
  }

  @override
  void registerTopic(String topic) {
    _messaging.subscribeToTopic(topic);
  }

  @override
  void unregisterTopic(String topic) {
    _messaging.unsubscribeFromTopic(topic);
  }

  @override
  Future<PermissionStatus> getPermissionStatus() {
    return Permission.notification.status;
  }

  // Admin send methods use a Callable Function (Admin SDK bypasses Firestore rules).
  HttpsCallable get _adminSendCallable =>
      FirebaseFunctions.instanceFor(region: 'europe-west1')
          .httpsCallable('notificationsFunctions-adminSendNotification');

  @override
  Future<String?> findUserByEmail(String email) async {
    // Email resolution is handled server-side by the callable.
    return null;
  }

  @override
  Future<void> sendNotification(
    String userId, {
    required String title,
    required String body,
    String? imageUrl,
    String? route,
  }) async {
    try {
      await _adminSendCallable.call({
        'title': title,
        'body': body,
        if (imageUrl != null && imageUrl.isNotEmpty) 'imageUrl': imageUrl,
        if (route != null && route.isNotEmpty) 'route': route,
        'targetUserIds': [userId],
      });
    } on FirebaseFunctionsException catch (e) {
      throw ApiError(code: 0, message: e.message ?? e.code);
    }
  }

  @override
  Future<void> sendNotificationToEmails(
    List<String> emails, {
    required String title,
    required String body,
    String? imageUrl,
    String? route,
  }) async {
    try {
      await _adminSendCallable.call({
        'title': title,
        'body': body,
        if (imageUrl != null && imageUrl.isNotEmpty) 'imageUrl': imageUrl,
        if (route != null && route.isNotEmpty) 'route': route,
        'targetEmails': emails,
      });
    } on FirebaseFunctionsException catch (e) {
      final msg = e.message ?? e.code;
      if (e.code == 'not-found') {
        throw UsersNotFoundError(msg);
      }
      throw ApiError(code: 0, message: msg);
    }
  }

  @override
  Future<void> sendNotificationToAll({
    required String title,
    required String body,
    String? imageUrl,
    String? route,
  }) async {
    try {
      await _adminSendCallable.call({
        'title': title,
        'body': body,
        if (imageUrl != null && imageUrl.isNotEmpty) 'imageUrl': imageUrl,
        if (route != null && route.isNotEmpty) 'route': route,
        'sendToAll': true,
      });
    } on FirebaseFunctionsException catch (e) {
      throw ApiError(code: 0, message: e.message ?? e.code);
    }
  }
}

class UsersNotFoundError implements Exception {
  final String emails;
  const UsersNotFoundError(this.emails);

  @override
  String toString() => emails;
}
