import 'dart:developer';

import 'package:crypton/crypton.dart';
import 'package:firebase_messaging/firebase_messaging.dart';

import '../background/widget_background_task.dart';
import '../notification/notification_service.dart';
import 'chat_thread_store.dart';
import 'nid_store.dart';
import 'push_decryptor.dart';
import 'push_keypair.dart';
import 'push_registration_store.dart';
import 'push_renderer.dart';
import 'push_subject.dart';

/// Wire value of the FCM `type` field for silent widget-refresh pushes.
/// Mirrors PUSH_TYPE_WIDGET_REFRESH in MarianumConnect's MarMobileApiService.
const String widgetRefreshPushType = 'widget-refresh';

/// How an incoming FCM payload should be interpreted.
enum PushKind {
  /// Encrypted Nextcloud push-v2 notification (`subject` + `signature`).
  nextcloud,

  /// Plaintext MarianumConnect direct push (`source == "connect"`).
  connect,

  /// Silent MarianumConnect push requesting a home-widget data refresh
  /// (`source == "connect"` + `type == "widget-refresh"`). Never rendered,
  /// processed even with notifications off.
  widgetRefresh,

  /// Neither — ignored.
  unknown,
}

/// Classifies a raw FCM data map. Pure, so it's unit-testable and safe in any
/// isolate. Nextcloud pushes are distinguished by the presence of both
/// `subject` and `signature`; Connect pushes by `source == "connect"`.
PushKind classifyPush(Map<String, dynamic> data) {
  final hasSubject = (data['subject'] as String?)?.isNotEmpty ?? false;
  final hasSignature = (data['signature'] as String?)?.isNotEmpty ?? false;
  if (hasSubject && hasSignature) return PushKind.nextcloud;
  if (data['source'] == 'connect') {
    if (data['type'] == widgetRefreshPushType) return PushKind.widgetRefresh;
    return PushKind.connect;
  }
  return PushKind.unknown;
}

/// FCM background isolate entry point. Must be a TOP-LEVEL function with the
/// entry-point pragma: AOT builds cannot invoke static class members from
/// native code unless the class itself is annotated too (DartVM error
/// "must be annotated"), so a plain function is the reliable form.
@pragma('vm:entry-point')
Future<void> pushOnBackgroundMessage(RemoteMessage message) async {
  await NotificationService().initializeNotifications();
  await PushRenderer.ensureChannels();
  await PushMessageHandler().handle(message);
}

/// Verifies, decrypts, and renders incoming push messages. Delete-pushes cancel
/// the matching tray notification via [NidStore]. Works both in the FCM
/// background isolate and the foreground.
class PushMessageHandler {
  final PushKeypair _keypair;
  final PushRegistrationStore _registrationStore;
  final PushRenderer _renderer;
  final NidStore _nidStore;
  final ChatThreadStore _threadStore;

  PushMessageHandler({
    PushKeypair? keypair,
    PushRegistrationStore? registrationStore,
    PushRenderer? renderer,
    NidStore? nidStore,
    ChatThreadStore? threadStore,
  }) : _keypair = keypair ?? const PushKeypair(),
       _registrationStore = registrationStore ?? const PushRegistrationStore(),
       _renderer = renderer ?? PushRenderer(),
       _nidStore = nidStore ?? NidStore(),
       _threadStore = threadStore ?? ChatThreadStore();

  /// Processes [message]. In the foreground, pass [foreground] true and
  /// [openChatToken] so a message for the currently open chat is suppressed
  /// (the long-poll already shows it) instead of raising a tray notification.
  Future<void> handle(
    RemoteMessage message, {
    bool foreground = false,
    String? openChatToken,
  }) async {
    final data = message.data;
    // The device stays registered even when the user turns notifications off,
    // so silent sync pushes (deletes, data refresh) keep arriving. When off we
    // still process the message but skip raising a visible notification.
    final notificationsEnabled = await _registrationStore.notificationsEnabled();
    switch (classifyPush(data)) {
      case PushKind.connect:
        await _handleConnect(
          message,
          foreground: foreground,
          notificationsEnabled: notificationsEnabled,
        );
        break;
      case PushKind.nextcloud:
        await _handleNextcloud(
          data,
          foreground: foreground,
          openChatToken: openChatToken,
          notificationsEnabled: notificationsEnabled,
        );
        break;
      case PushKind.widgetRefresh:
        // Deliberately before any notificationsEnabled gate: silent sync
        // pushes must work with notifications off.
        await _handleWidgetRefresh();
        break;
      case PushKind.unknown:
        break;
    }
  }

  Future<void> _handleWidgetRefresh() async {
    try {
      // The iOS FCM handler runs in the main isolate with a ~25s APNs
      // budget — bound the inline refresh below that so the completion
      // handler always fires in time.
      await WidgetBackgroundTask.requestImmediateRefresh(
        force: false,
        inlineTimeout: const Duration(seconds: 20),
      );
    } on Exception catch (e) {
      log('[push] widget refresh failed: $e');
    }
  }

  Future<void> _handleConnect(
    RemoteMessage message, {
    required bool foreground,
    required bool notificationsEnabled,
  }) async {
    // Connect pushes carry no silent side effects, so nothing to do when the
    // user has notifications off.
    if (!notificationsEnabled) return;
    // On iOS the alert is delivered natively by the system; only Android needs
    // to render the plaintext payload locally.
    final data = message.data;
    final title = data['title'] as String?;
    final body = data['body'] as String?;
    if (title == null) return;
    await _renderer.renderConnect(
      title: title,
      body: body ?? '',
      data: data.map((k, v) => MapEntry(k, '$v')),
    );
  }

  Future<void> _handleNextcloud(
    Map<String, dynamic> data, {
    required bool foreground,
    required String? openChatToken,
    required bool notificationsEnabled,
  }) async {
    final subjectBase64 = data['subject'] as String;
    final signatureBase64 = data['signature'] as String;

    final privateKey = await _keypair.loadPrivateKey();
    if (privateKey == null) {
      log('Push: no device private key, cannot decrypt');
      return;
    }
    final serverPublicKey = await _loadServerPublicKey();

    final decryptor = PushDecryptor(
      devicePrivateKey: privateKey,
      serverPublicKey: serverPublicKey,
    );
    if (!decryptor.verify(subjectBase64, signatureBase64)) {
      log('Push: signature verification failed');
      return;
    }
    final subject = decryptor.decrypt(subjectBase64);
    if (subject == null) {
      log('Push: could not decrypt subject');
      return;
    }

    if (subject.isAnyDelete) {
      await _handleDelete(subject);
      return;
    }

    // Foreground + the referenced chat already open: the long-poll renders the
    // message, so just make sure no stale tray entry lingers.
    if (foreground &&
        subject.isTalk &&
        subject.id != null &&
        subject.id == openChatToken) {
      return;
    }

    // Notifications turned off: the push was still processed (deletes above,
    // plus the foreground badge/provider refresh in NotificationController) —
    // only the visible tray notification is suppressed.
    if (!notificationsEnabled) return;

    await _renderer.render(subject);
  }

  Future<void> _handleDelete(PushSubject subject) async {
    if (subject.deleteAll) {
      final all = await _nidStore.all();
      for (final entry in all) {
        await _cancel(entry);
      }
      await _nidStore.clear();
      await _threadStore.clearAll();
      return;
    }
    final nids = <int>[
      if (subject.delete && subject.nid != null) subject.nid!,
      ...subject.nids,
    ];
    for (final nid in nids) {
      final entry = await _nidStore.get(nid);
      final chatToken = entry?.chatToken;
      if (chatToken != null && chatToken.isNotEmpty) {
        // Stacked chat notification: drop only this message from the thread.
        // Remaining messages re-render WITHOUT alerting again; the last one
        // going away cancels the whole card.
        final remaining = await _threadStore.removeNid(chatToken, nid);
        if (remaining.isEmpty) {
          await _cancel(entry!);
        } else {
          await _renderer.renderTalkThread(chatToken, remaining, alert: false);
        }
      } else if (entry != null) {
        await _cancel(entry);
      }
      await _nidStore.delete(nid);
    }
  }

  Future<void> _cancel(NidEntry entry) async {
    try {
      await NotificationService().flutterLocalNotificationsPlugin.cancel(
        id: entry.notificationId,
        tag: entry.tag,
      );
    } on Object catch (e) {
      log('Push: cancel ${entry.nid} failed: $e');
    }
  }

  Future<RSAPublicKey?> _loadServerPublicKey() async {
    final pem = await _registrationStore.serverPublicKeyPem();
    if (pem == null || pem.isEmpty) return null;
    try {
      return RSAPublicKey.fromPEM(pem);
    } on Object {
      return null;
    }
  }
}
