import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '../utils/device_id.dart';

import '../models/notification_item.dart';
import 'profile_provider.dart';
import 'supabase_provider.dart';
import 'stream_recovery.dart';
import 'realtime_controller.dart';
import '../utils/app_time.dart';

final notificationsProvider = StreamProvider<List<NotificationItem>>((ref) {
  final userId = ref.watch(currentUserIdProvider);
  if (userId == null) {
    return const Stream.empty();
  }
  final client = ref.watch(supabaseClientProvider);

  final wrapper = StreamRecoveryWrapper<NotificationItem>(
    stream: client
        .from('notifications')
        .stream(primaryKey: ['id'])
        .eq('user_id', userId)
        .order('created_at', ascending: false),
    onPollData: () async {
      final data = await client
          .from('notifications')
          .select()
          .eq('user_id', userId)
          .order('created_at', ascending: false);
      return data.map(NotificationItem.fromMap).toList();
    },
    fromMap: NotificationItem.fromMap,
    channelName: 'notifications',
    onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus,
  );

  ref.onDispose(wrapper.dispose);
  return wrapper.stream.map((result) => result.data);
});

final unreadNotificationsCountProvider = Provider<int>((ref) {
  final notificationsAsync = ref.watch(notificationsProvider);
  return notificationsAsync.maybeWhen(
    data: (items) => items.where((item) => item.isUnread).length,
    orElse: () => 0,
  );
});

final notificationsControllerProvider = Provider<NotificationsController>((
  ref,
) {
  final client = ref.watch(supabaseClientProvider);
  return NotificationsController(client);
});

class NotificationsController {
  NotificationsController(this._client);

  final SupabaseClient _client;

  /// Internal helper that inserts notification rows and sends pushes if
  /// [targetUserIds] is provided.
  /// Internal helper that inserts notification rows and optionally sends
  /// FCM pushes.  Callers should use [createNotification] instead.
  Future<void> _createAndPush(
    List<Map<String, dynamic>> rows, {
    List<String>? targetUserIds,
    String? pushTitle,
    String? pushBody,
    Map<String, dynamic>? pushData,
  }) async {
    if (rows.isEmpty) return;
    debugPrint(
      'notifications_provider: inserting ${rows.length} rows; pushTitle=$pushTitle',
    );
    await _client.from('notifications').insert(rows);

    // If target user IDs are provided, invoke client-side push
    // flow by calling `sendPush`. We no longer rely on the DB trigger
    // to call the edge function.
    if (targetUserIds == null || targetUserIds.isEmpty) return;

    debugPrint('notification rows inserted; invoking client push');
    try {
      await sendPush(
        userIds: targetUserIds,
        title: pushTitle ?? '',
        body: pushBody ?? '',
        data: pushData,
      );
    } catch (e) {
      debugPrint('sendPush failed: $e');
    }
  }

  /// Create a typed notification in the database.  This method handles
  /// inserting the row(s); a PostgreSQL trigger will forward the new row to
  /// the `send_fcm` edge function, so clients do **not** directly invoke it
  /// (avoids CORS/auth problems on web).  The [pushTitle]/[pushBody] values
  /// are still stored for the trigger payload.
  Future<void> createNotification({
    required List<String> userIds,
    required String type,
    required String actorId,
    Map<String, dynamic>? fields,
    String? pushTitle,
    String? pushBody,
    Map<String, dynamic>? pushData,
  }) async {
    debugPrint(
      'createNotification called type=$type users=${userIds.length} pushTitle=$pushTitle pushBody=$pushBody',
    );
    if (userIds.isEmpty) return;

    final rows = userIds.map((userId) {
      return <String, dynamic>{
        'user_id': userId,
        'actor_id': actorId,
        'type': type,
        ...?fields,
      };
    }).toList();

    await _createAndPush(
      rows,
      targetUserIds: userIds,
      pushTitle: pushTitle,
      pushBody: pushBody,
      pushData: pushData,
    );
  }

  /// Convenience for mention-specific case; left for compatibility.
  Future<void> createMentionNotifications({
    required List<String> userIds,
    required String actorId,
    required int messageId,
    String? ticketId,
    String? taskId,
  }) async {
    return createNotification(
      userIds: userIds,
      type: 'mention',
      actorId: actorId,
      fields: {
        'message_id': messageId,
        ...?(ticketId != null ? {'ticket_id': ticketId} : null),
        ...?(taskId != null ? {'task_id': taskId} : null),
      },
      pushTitle: 'New mention',
      pushBody: 'You were mentioned in a message',
      pushData: {
        ...?(ticketId != null ? {'ticket_id': ticketId} : null),
        ...?(taskId != null ? {'task_id': taskId} : null),
      },
    );
  }

  Future<void> markRead(String id) async {
    await _client
        .from('notifications')
        .update({'read_at': AppTime.nowUtc().toIso8601String()})
        .eq('id', id);
  }

  Future<void> markReadForTicket(String ticketId) async {
    final userId = _client.auth.currentUser?.id;
    if (userId == null) return;
    await _client
        .from('notifications')
        .update({'read_at': AppTime.nowUtc().toIso8601String()})
        .eq('ticket_id', ticketId)
        .eq('user_id', userId)
        .filter('read_at', 'is', null);
  }

  Future<void> markReadForTask(String taskId) async {
    final userId = _client.auth.currentUser?.id;
    if (userId == null) return;
    await _client
        .from('notifications')
        .update({'read_at': AppTime.nowUtc().toIso8601String()})
        .eq('task_id', taskId)
        .eq('user_id', userId)
        .filter('read_at', 'is', null);
  }

  /// Store or update an FCM token for the current user.
  Future<void> registerFcmToken(String token) async {
    final userId = _client.auth.currentUser?.id;
    if (userId == null) return;
    final deviceId = await DeviceId.getId();
    if (deviceId.isEmpty) {
      debugPrint('registerFcmToken: device id missing; skipping');
      return;
    }
    // upsert using a unique constraint on (user_id, device_id) so a single
    // device keeps its token updated without overwriting other devices.
    final payload = {
      'user_id': userId,
      'device_id': deviceId,
      'token': token,
      'created_at': DateTime.now().toUtc().toIso8601String(),
    };
    final res = await _client.from('fcm_tokens').upsert(payload);
    final dyn = res as dynamic;
    if (dyn.error != null) {
      // duplicate key or RLS issue - just log it
      debugPrint('registerFcmToken error: ${dyn.error?.message ?? dyn.error}');
    } else {
      debugPrint('registerFcmToken success for user=$userId token=$token');
    }
  }

  /// Remove an FCM token (e.g. when the user logs out or uninstalls).
  Future<void> unregisterFcmToken(String token) async {
    final userId = _client.auth.currentUser?.id;
    if (userId == null) return;
    final deviceId = await DeviceId.getId();
    // Prefer to delete by device_id to avoid removing other devices' tokens.
    final res = await _client
        .from('fcm_tokens')
        .delete()
        .eq('user_id', userId)
        .eq('device_id', deviceId)
        .or('token.eq.$token');
    final uDyn = res as dynamic;
    if (uDyn.error != null) {
      debugPrint(
        'unregisterFcmToken error: ${uDyn.error?.message ?? uDyn.error}',
      );
    }
  }

  /// Send a push message via the `send_fcm` edge function.
  Future<void> sendPush({
    List<String>? tokens,
    List<String>? userIds,
    required String title,
    required String body,
    Map<String, dynamic>? data,
  }) async {
    try {
      if (tokens != null && tokens.isNotEmpty) {
        debugPrint(
          'invoking send_fcm with ${tokens.length} tokens, title="$title"',
        );
      } else if (userIds != null && userIds.isNotEmpty) {
        debugPrint(
          'invoking send_fcm with userIds=${userIds.length}, title="$title"',
        );
      } else {
        debugPrint('sendPush called with neither tokens nor userIds');
        return;
      }

      final bodyPayload = <String, dynamic>{
        'tokens': tokens ?? [],
        'user_ids': userIds ?? [],
        'title': title,
        'body': body,
        'data': data ?? {},
      };

      await _client.functions.invoke('send_fcm', body: bodyPayload);
    } catch (err) {
      debugPrint('sendPush invocation error: $err');
    }
  }
}
