import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdfrx/pdfrx.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'firebase_options.dart';
// removed unused imports
import 'app.dart';
import 'providers/notifications_provider.dart';
import 'providers/notification_navigation_provider.dart';
import 'utils/app_time.dart';
import 'utils/notification_permission.dart';
import 'services/notification_service.dart';
import 'services/notification_bridge.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';

// audio player not used at top-level; instantiate where needed
StreamSubscription<String?>? _fcmTokenRefreshSub;
late ProviderContainer _globalProviderContainer;

Map<String, String> _formatNotificationFromData(Map<String, dynamic> data) {
  String actor = '';
  if (data['actor_name'] != null) {
    actor = data['actor_name'].toString();
  } else if (data['mentioner_name'] != null) {
    actor = data['mentioner_name'].toString();
  } else if (data['user_name'] != null) {
    actor = data['user_name'].toString();
  } else if (data['from'] != null) {
    actor = data['from'].toString();
  } else if (data['actor'] != null) {
    final a = data['actor'];
    if (a is Map && a['name'] != null) {
      actor = a['name'].toString();
    } else if (a is String) {
      try {
        final parsed = jsonDecode(a);
        if (parsed is Map && parsed['name'] != null) {
          actor = parsed['name'].toString();
        }
      } catch (_) {
        // ignore JSON parse errors
      }
    }
  }
  if (actor.isEmpty) {
    actor = 'Someone';
  }

  final taskNumber =
      (data['task_number'] ?? data['taskNumber'] ?? data['task_no'])
          ?.toString() ??
      '';
  final taskId =
      (data['task_id'] ?? data['taskId'] ?? data['task'])?.toString() ?? '';
  final ticketId =
      data['ticket_id'] ?? data['ticketId'] ?? data['ticket'] ?? '';
  final type = (data['type'] ?? '').toString().toLowerCase();

  final taskLabel = taskNumber.isNotEmpty
      ? 'Task $taskNumber'
      : (taskId.isNotEmpty ? 'Task #$taskId' : 'Task');

  if ((taskId.isNotEmpty || taskNumber.isNotEmpty) &&
      (type.contains('assign') ||
          data['action'] == 'assign' ||
          data['assigned'] == 'true')) {
    return {
      'title': 'Task assigned',
      'body': '$actor has assigned you $taskLabel',
    };
  }

  if ((taskId.isNotEmpty || taskNumber.isNotEmpty) &&
      (type.contains('mention') ||
          data['action'] == 'mention' ||
          data['mentioned'] == 'true')) {
    return {
      'title': 'Mention',
      'body': '$actor has mentioned you in $taskLabel',
    };
  }

  if (ticketId.isNotEmpty &&
      (type.contains('mention') || data['action'] == 'mention')) {
    return {
      'title': 'Mention',
      'body': '$actor has mentioned you in Ticket #$ticketId',
    };
  }

  // Fallback to supplied title/body or generic
  final title = data['title']?.toString() ?? 'New Notification';
  final body = data['body']?.toString() ?? 'You have a new update in TasQ.';
  return {'title': title, 'body': body};
}

// Initialize the plugin
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
    FlutterLocalNotificationsPlugin();

/// Handle messages received while the app is terminated or in background.
@pragma('vm:entry-point') // Required for background execution
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  // 1. Initialize the plugin inside the background isolate
  final FlutterLocalNotificationsPlugin localNotifPlugin =
      FlutterLocalNotificationsPlugin();

  // 2. Extract and format title/body from the DATA payload (not message.notification)
  final formatted = _formatNotificationFromData(message.data);
  final String title = formatted['title']!;
  final String body = formatted['body']!;

  // Determine a stable ID for deduplication (prefer server-provided id)
  String? stableId =
      (message.data['notification_id'] as String?) ?? message.messageId;
  if (stableId == null) {
    final sb = StringBuffer();
    final taskNumber =
        (message.data['task_number'] ??
                message.data['taskNumber'] ??
                message.data['task_no'])
            ?.toString();
    final taskId =
        (message.data['task_id'] ??
                message.data['taskId'] ??
                message.data['task'])
            ?.toString();
    final ticketId =
        (message.data['ticket_id'] ??
                message.data['ticketId'] ??
                message.data['ticket'])
            ?.toString();
    final type = (message.data['type'] ?? '').toString();
    final actorId =
        (message.data['actor_id'] ??
                message.data['actorId'] ??
                message.data['actor'])
            ?.toString();
    if (taskNumber != null && taskNumber.isNotEmpty) {
      sb.write('tasknum:$taskNumber');
    } else if (taskId != null && taskId.isNotEmpty) {
      sb.write('task:$taskId');
    }
    if (ticketId != null && ticketId.isNotEmpty) {
      if (sb.isNotEmpty) sb.write('|');
      sb.write('ticket:$ticketId');
    }
    if (type.isNotEmpty) {
      if (sb.isNotEmpty) sb.write('|');
      sb.write('type:$type');
    }
    if (actorId != null && actorId.isNotEmpty) {
      if (sb.isNotEmpty) sb.write('|');
      sb.write('actor:$actorId');
    }
    stableId = sb.isNotEmpty
        ? sb.toString()
        : (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString();
  }

  // Dedupe: keep a short-lived cache of recent notification IDs to avoid duplicates
  try {
    final prefs = await SharedPreferences.getInstance();
    final raw = prefs.getString('recent_notifs') ?? '{}';
    final Map<String, dynamic> recent = jsonDecode(raw);
    final int now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
    const int ttl = 60; // seconds
    // prune old entries
    recent.removeWhere(
      (k, v) => (v is int ? v : int.parse(v.toString())) < now - ttl,
    );
    if (recent.containsKey(stableId)) {
      // already shown recently — skip
      return;
    }
    recent[stableId] = now;
    await prefs.setString('recent_notifs', jsonEncode(recent));
  } catch (e) {
    // If prefs fail in background isolate, fall back to showing notification
  }

  // Create a unique ID for the notification display
  final int id = DateTime.now().millisecondsSinceEpoch ~/ 1000;

  // Build payload string with ticket/task information for navigation
  final payloadParts = <String>[];
  final taskId =
      (message.data['task_id'] ??
              message.data['taskId'] ??
              message.data['task'])
          ?.toString();
  final ticketId =
      message.data['ticket_id'] ??
      message.data['ticketId'] ??
      message.data['ticket']?.toString() ??
      '';

  if (taskId != null && taskId.isNotEmpty) {
    payloadParts.add('task:$taskId');
  }
  if (ticketId.isNotEmpty) {
    payloadParts.add('ticket:$ticketId');
  }
  final payload = payloadParts.join('|').isNotEmpty
      ? payloadParts.join('|')
      : message.data['type']?.toString() ?? '';

  // 3. Define the exact same channel specifics
  const androidDetails = AndroidNotificationDetails(
    'tasq_custom_sound_channel_3',
    'High Importance Notifications',
    importance: Importance.max,
    priority: Priority.high,
    playSound: true,
    sound: RawResourceAndroidNotificationSound('tasq_notification'),
  );

  // 4. Show the notification manually
  await localNotifPlugin.show(
    id: id,
    title: title,
    body: body,
    notificationDetails: const NotificationDetails(android: androidDetails),
    payload: payload,
  );
}

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  // The flag optionally hides annoying WASM warnings in your Chrome dev console
  pdfrxFlutterInitialize(dismissPdfiumWasmWarnings: true);

  // initialize Firebase before anything that uses messaging
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);

  await dotenv.load(fileName: '.env');

  AppTime.initialize(location: 'Asia/Manila');

  final supabaseUrl = dotenv.env['SUPABASE_URL'] ?? '';
  final supabaseAnonKey = dotenv.env['SUPABASE_ANON_KEY'] ?? '';

  if (supabaseUrl.isEmpty || supabaseAnonKey.isEmpty) {
    runApp(const _MissingConfigApp());
    return;
  }

  await Supabase.initialize(url: supabaseUrl, anonKey: supabaseAnonKey);

  // ensure token saved shortly after startup if already signed in.
  // Run this after runApp so startup is not blocked by network/token ops.
  final supaClient = Supabase.instance.client;

  // listen for auth changes to register/unregister token accordingly
  supaClient.auth.onAuthStateChange.listen((data) async {
    final event = data.event;
    if (kIsWeb) {
      debugPrint(
        'auth state change $event on web: skipping FCM token handling',
      );
      return;
    }
    String? token;
    try {
      token = await FirebaseMessaging.instance.getToken();
    } catch (e) {
      debugPrint('FCM getToken failed during auth change: $e');
      token = null;
    }
    debugPrint('auth state change $event, token=$token');
    if (token == null) return;
    final ctrl = NotificationsController(supaClient);
    if (event == AuthChangeEvent.signedIn) {
      // register current token and ensure we listen for refreshes
      await ctrl.registerFcmToken(token);
      try {
        // cancel any previous subscription
        await _fcmTokenRefreshSub?.cancel();
      } catch (_) {}
      _fcmTokenRefreshSub = FirebaseMessaging.instance.onTokenRefresh.listen((
        t,
      ) {
        debugPrint('token refreshed (auth listener): $t');
        ctrl.registerFcmToken(t);
      });
    } else if (event == AuthChangeEvent.signedOut) {
      // cancel token refresh subscription and unregister
      try {
        await _fcmTokenRefreshSub?.cancel();
      } catch (_) {}
      _fcmTokenRefreshSub = null;
      await ctrl.unregisterFcmToken(token);
    }
  });

  if (!kIsWeb) {
    // on Android 13+ we must request POST_NOTIFICATIONS at runtime; without it
    // notifications are automatically denied and cannot be re‑enabled from the
    // system settings. The helper uses `permission_handler`.
    final granted = await ensureNotificationPermission();
    if (!granted) {
      // we don’t block startup, but it’s worth logging so developers notice.
      // A real app might show a dialog pointing the user to settings.
      // debugPrint('notification permission not granted');
    }

    // request FCM permission (iOS/Android13+) and handle foreground messages
    await FirebaseMessaging.instance.requestPermission();
  }

  FirebaseMessaging.onMessage.listen((RemoteMessage message) async {
    // Prefer the data payload and format friendly messages, with dedupe.
    // If actor_name is not present but actor_id is, try to resolve the
    // display name using the Supabase client (foreground only).
    Map<String, dynamic> dataForFormatting = Map<String, dynamic>.from(
      message.data,
    );
    try {
      final hasActorName =
          (dataForFormatting['actor_name'] ??
              dataForFormatting['mentioner_name'] ??
              dataForFormatting['user_name'] ??
              dataForFormatting['from'] ??
              dataForFormatting['actor']) !=
          null;
      final actorId =
          dataForFormatting['actor_id'] ??
          dataForFormatting['actorId'] ??
          dataForFormatting['actor'];
      if (!hasActorName && actorId is String && actorId.isNotEmpty) {
        try {
          final client = Supabase.instance.client;
          final res = await client
              .from('profiles')
              .select('full_name,display_name,name')
              .eq('id', actorId)
              .maybeSingle();
          if (res != null) {
            String? name;
            if (res['full_name'] != null) {
              name = res['full_name'].toString();
            } else if (res['display_name'] != null) {
              name = res['display_name'].toString();
            } else if (res['name'] != null) {
              name = res['name'].toString();
            }
            if (name != null && name.isNotEmpty) {
              dataForFormatting['actor_name'] = name;
            }
          }
        } catch (_) {
          // ignore lookup failures and fall back to data payload
        }
      }
    } catch (_) {}

    final formatted = _formatNotificationFromData(dataForFormatting);
    String? stableId =
        (message.data['notification_id'] as String?) ?? message.messageId;
    if (stableId == null) {
      final sb = StringBuffer();
      final taskNumber =
          (message.data['task_number'] ??
                  message.data['taskNumber'] ??
                  message.data['task_no'])
              ?.toString();
      final taskId =
          (message.data['task_id'] ??
                  message.data['taskId'] ??
                  message.data['task'])
              ?.toString();
      final ticketId =
          (message.data['ticket_id'] ??
                  message.data['ticketId'] ??
                  message.data['ticket'])
              ?.toString();
      final type = (message.data['type'] ?? '').toString();
      final actorId =
          (message.data['actor_id'] ??
                  message.data['actorId'] ??
                  message.data['actor'])
              ?.toString();
      if (taskNumber != null && taskNumber.isNotEmpty) {
        sb.write('tasknum:$taskNumber');
      } else if (taskId != null && taskId.isNotEmpty) {
        sb.write('task:$taskId');
      }
      if (ticketId != null && ticketId.isNotEmpty) {
        if (sb.isNotEmpty) sb.write('|');
        sb.write('ticket:$ticketId');
      }
      if (type.isNotEmpty) {
        if (sb.isNotEmpty) sb.write('|');
        sb.write('type:$type');
      }
      if (actorId != null && actorId.isNotEmpty) {
        if (sb.isNotEmpty) sb.write('|');
        sb.write('actor:$actorId');
      }
      stableId = sb.isNotEmpty
          ? sb.toString()
          : (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString();
    }
    try {
      final prefs = await SharedPreferences.getInstance();
      final raw = prefs.getString('recent_notifs') ?? '{}';
      final Map<String, dynamic> recent = jsonDecode(raw);
      final int now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
      const int ttl = 60;
      recent.removeWhere(
        (k, v) => (v is int ? v : int.parse(v.toString())) < now - ttl,
      );
      if (!recent.containsKey(stableId)) {
        recent[stableId] = now;
        await prefs.setString('recent_notifs', jsonEncode(recent));

        // Build payload string with ticket/task information for navigation
        final payloadParts = <String>[];
        final taskId =
            (message.data['task_id'] ??
                    message.data['taskId'] ??
                    message.data['task'])
                ?.toString();
        final ticketId =
            message.data['ticket_id'] ??
            message.data['ticketId'] ??
            message.data['ticket']?.toString() ??
            '';

        if (taskId != null && taskId.isNotEmpty) {
          payloadParts.add('task:$taskId');
        }
        if (ticketId.isNotEmpty) {
          payloadParts.add('ticket:$ticketId');
        }
        final payload = payloadParts.join('|').isNotEmpty
            ? payloadParts.join('|')
            : message.data['payload']?.toString() ?? '';

        NotificationService.show(
          id: DateTime.now().millisecondsSinceEpoch ~/ 1000,
          title: formatted['title']!,
          body: formatted['body']!,
          payload: payload,
        );
      }
    } catch (e) {
      // On failure, just show the notification to avoid dropping alerts
      NotificationService.show(
        id: DateTime.now().millisecondsSinceEpoch ~/ 1000,
        title: formatted['title']!,
        body: formatted['body']!,
        payload: '',
      );
    }
  });

  // 1. Define the High Importance Channel (This MUST match your manifest exactly)
  const AndroidNotificationChannel channel = AndroidNotificationChannel(
    'tasq_custom_sound_channel', // id
    'High Importance Notifications', // title visible to user in phone settings
    description: 'This channel is used for important TasQ notifications.',
    importance:
        Importance.max, // THIS is what forces the sound and heads-up banner
    playSound: true,
    // 👇 Tell Android to use your specific file in the raw folder
    sound: RawResourceAndroidNotificationSound('tasq_notification'),
  );

  // 2. Create the channel on the device
  await flutterLocalNotificationsPlugin
      .resolvePlatformSpecificImplementation<
        AndroidFlutterLocalNotificationsPlugin
      >()
      ?.createNotificationChannel(channel);

  // Create the global provider container BEFORE initializing local
  // notifications, because the tap callback needs to write to it and
  // flutter_local_notifications may fire the callback synchronously if
  // the app was launched by tapping a notification.
  _globalProviderContainer = ProviderContainer(
    observers: [NotificationSoundObserver()],
  );

  // initialize the local notifications plugin so we can post alerts later
  await NotificationService.initialize(
    onDidReceiveNotificationResponse: (response) {
      // handle user tapping a notification; the payload format is "ticket:ID",
      // "task:ID", "tasknum:NUMBER", or a combination separated by "|"
      final payload = response.payload;
      if (payload != null && payload.isNotEmpty) {
        // Parse the payload to extract ticket and task information
        final parts = payload.split('|');
        String? ticketId;
        String? taskId;

        for (final part in parts) {
          if (part.startsWith('ticket:')) {
            ticketId = part.substring('ticket:'.length);
          } else if (part.startsWith('task:')) {
            taskId = part.substring('task:'.length);
          }
        }

        // Update the pending navigation provider.
        // Prefer task over ticket — assignment notifications include both
        // IDs but the primary entity is the task.
        if (taskId != null && taskId.isNotEmpty) {
          _globalProviderContainer
              .read(pendingNotificationNavigationProvider.notifier)
              .state = (
            type: 'task',
            id: taskId,
          );
        } else if (ticketId != null && ticketId.isNotEmpty) {
          _globalProviderContainer
              .read(pendingNotificationNavigationProvider.notifier)
              .state = (
            type: 'ticket',
            id: ticketId,
          );
        }
      }
    },
  );

  runApp(
    UncontrolledProviderScope(
      container: _globalProviderContainer,
      child: const NotificationBridge(child: TasqApp()),
    ),
  );

  // Post-startup registration removed: token registration is handled
  // centrally in the auth state change listener to avoid duplicate inserts.
}

class NotificationSoundObserver extends ProviderObserver {
  @override
  void didUpdateProvider(
    ProviderBase provider,
    Object? previousValue,
    Object? newValue,
    ProviderContainer container,
  ) {
    // play sound + show OS notification on unread-count increase
    // if (provider == unreadNotificationsCountProvider) {
    //   final prev = previousValue as int?;
    //   final next = newValue as int?;
    //   if (prev != null && next != null && next > prev) {
    //     _maybeShowUnreadNotification(next);
    //   }
    // }

    // Profile changes no longer perform token registration here.
    // Token registration is handled centrally in the auth state change listener
    // to avoid duplicate DB rows and duplicate deliveries.
  }
}

// void _maybeShowUnreadNotification(int next) async {
//   try {
//     final prefs = await SharedPreferences.getInstance();
//     final raw = prefs.getString('recent_notifs') ?? '{}';
//     final Map<String, dynamic> recent = jsonDecode(raw);
//     final int now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
//     const int ttl = 60; // seconds
//     // prune old entries
//     recent.removeWhere(
//       (k, v) => (v is int ? v : int.parse(v.toString())) < now - ttl,
//     );
//     // if there is any recent notification (from server or other), skip duplicating
//     if (recent.isNotEmpty) return;

//     // mark a synthetic unread-notif to prevent immediate duplicates
//     recent['unread_summary'] = now;
//     await prefs.setString('recent_notifs', jsonEncode(recent));

//     _player.play(AssetSource('tasq_notification.wav'));
//     NotificationService.show(
//       id: DateTime.now().millisecondsSinceEpoch ~/ 1000,
//       title: 'New notifications',
//       body: 'You have $next unread notifications.',
//     );
//   } catch (e) {
//     // fallback: show notification
//     _player.play(AssetSource('tasq_notification.wav'));
//     NotificationService.show(
//       id: DateTime.now().millisecondsSinceEpoch ~/ 1000,
//       title: 'New notifications',
//       body: 'You have $next unread notifications.',
//     );
//   }
// }

class _MissingConfigApp extends StatelessWidget {
  const _MissingConfigApp();

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: Scaffold(
        body: Center(
          child: Padding(
            padding: EdgeInsets.all(24),
            child: Text(
              'Missing SUPABASE_URL or SUPABASE_ANON_KEY. '
              'Provide them in the .env file.',
              textAlign: TextAlign.center,
            ),
          ),
        ),
      ),
    );
  }
}
