import 'dart:io';
import 'dart:ui';

import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_vodozemac/flutter_vodozemac.dart' as vod;
import 'package:matrix/matrix.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:sqflite/sqflite.dart' as sqflite_native;

/// Port name used to locate the main isolate from background isolates.
/// Registered by notification_service.dart when the app starts.
const kMainIsolateName = 'pyramid_main';

int _stableId(String roomId) {
  var h = 0;
  for (final c in roomId.codeUnits) {
    h = ((h * 31) + c) & 0x7FFFFFFF;
  }
  return h == 0 ? 1 : h;
}

// ── FCM background handler ────────────────────────────────────────────────────
// Runs in its own Dart isolate (or fresh process) when the app is killed or
// backgrounded. Firebase calls this after Firebase.initializeApp() is done.

@pragma('vm:entry-point')
Future<void> handleBackgroundMessage(RemoteMessage message) async {
  // PushService.kt handles all FCM-triggered notifications natively.
  // This callback is registered for API completeness but never fires because
  // FlutterFirebaseMessagingService is removed from the manifest.
}

// ── Background engine entrypoint (killed-app E2EE work) ───────────────────────
// PushService.kt / ReplyReceiver bootstrap a headless FlutterEngine running this
// entrypoint when the app is KILLED (no main engine). It exposes a MethodChannel
// so native can ask us to (a) decrypt a push event and update its notification,
// or (b) send an encrypted reply. Building a Matrix client here is safe ONLY
// because native gates this on the main app being dead — never two clients on
// the same olm DB at once.

const _kBgEngineChannel = 'chat.pyramid.pyramid/bg_engine';

@pragma('vm:entry-point')
void notificationEngineMain() {
  WidgetsFlutterBinding.ensureInitialized();
  const channel = MethodChannel(_kBgEngineChannel);

  channel.setMethodCallHandler((call) async {
    try {
      final args = (call.arguments as Map?)?.cast<String, dynamic>() ?? {};
      switch (call.method) {
        case 'decryptAndShow':
          return await _bgDecryptAndShow(
            args['room_id'] as String?,
            args['event_id'] as String?,
            (args['notif_id'] as num?)?.toInt(),
          );
        case 'sendReply':
          return await _bgSendReply(
            args['room_id'] as String?,
            args['text'] as String?,
          );
      }
    } catch (e) {
      debugPrint('[NOTIF-BGENGINE] handler error: $e');
    }
    return false;
  });

  // Signal native that the channel handler is installed and tasks can be sent.
  channel.invokeMethod('bgEngineReady');
}

Future<bool> _bgDecryptAndShow(String? roomId, String? eventId, int? notifId) async {
  if (roomId == null || eventId == null) return false;
  final client = await _buildClient();
  if (client == null) return false;
  try {
    final event = await client
        .getEventByPushNotification(
          PushNotification(roomId: roomId, eventId: eventId, devices: const []),
          storeInDatabase: false,
          returnNullIfSeen: false,
        )
        .timeout(const Duration(seconds: 20));
    if (event == null) return false;
    // Bad-encrypted body still starts with "**" — keep the native placeholder.
    if (event.body.startsWith('**')) return false;

    final room = client.getRoomById(roomId);
    final sender = room?.unsafeGetUserFromMemoryOrFallback(event.senderId).displayName ??
        event.senderId.split(':').first.replaceFirst('@', '');
    final title = (room?.isDirectChat ?? true)
        ? sender
        : '$sender · ${room?.getLocalizedDisplayname() ?? ''}';

    // Hand the decrypted content back to native so it re-shows the SAME
    // notification via NotificationHelper — that keeps the reliable
    // MainActivity-based reply action (the flutter_local_notifications reply
    // path doesn't fire dependably when the app is killed).
    await const MethodChannel(_kBgEngineChannel).invokeMethod('showDecrypted', {
      'title':    title,
      'body':     _buildBody(event),
      'room_id':  roomId,
      'notif_id': notifId ?? _stableId(roomId),
    });
    return true;
  } catch (e) {
    debugPrint('[NOTIF-BGENGINE] decryptAndShow error: $e');
    return false;
  } finally {
    // Let the async key-backup upload settle before closing the DB — otherwise
    // it logs a harmless "database_closed" error mid-flight.
    await Future.delayed(const Duration(seconds: 2));
    await client.dispose().catchError((_) {});
  }
}

Future<bool> _bgSendReply(String? roomId, String? text) async {
  if (roomId == null || text == null || text.trim().isEmpty) return false;
  final client = await _buildClient();
  if (client == null) return false;
  try {
    // A one-shot sync ensures the room and recipient device keys are present so
    // the message can be encrypted. Safe here: main app is dead (native-gated).
    await client.oneShotSync().timeout(const Duration(seconds: 20)).catchError((_) => null);
    final room = client.getRoomById(roomId);
    if (room == null) {
      // Fall back to the prefs queue so the main app sends it on next launch.
      final prefs = await SharedPreferences.getInstance();
      await prefs.setString('notif_pending_reply_room', roomId);
      await prefs.setString('notif_pending_reply_text', text);
      return false;
    }
    await room.sendTextEvent(text.trim());
    debugPrint('[NOTIF-BGENGINE] encrypted reply sent to $roomId');
    return true;
  } catch (e) {
    debugPrint('[NOTIF-BGENGINE] sendReply error: $e');
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString('notif_pending_reply_room', roomId);
    await prefs.setString('notif_pending_reply_text', text);
    return false;
  } finally {
    // Allow the encrypted send + any key upload to flush before closing the DB.
    await Future.delayed(const Duration(seconds: 2));
    await client.dispose().catchError((_) {});
  }
}

/// Stores the raw callback handle of [notificationEngineMain] so native code can
/// bootstrap the background engine. Call once at app startup.
Future<void> registerBgEngineHandle() async {
  if (!Platform.isAndroid) return;
  try {
    final handle = PluginUtilities.getCallbackHandle(notificationEngineMain);
    if (handle == null) return;
    final prefs = await SharedPreferences.getInstance();
    await prefs.setInt('bg_engine_handle', handle.toRawHandle());
  } catch (e) {
    debugPrint('[NOTIF-BGENGINE] registerBgEngineHandle failed: $e');
  }
}

// ── Build a short-lived Matrix client for background work ────────────────────
// Uses NativeImplementationsDummy so Vodozemac runs synchronously in this
// isolate — NativeImplementationsIsolate(compute) would try to spawn a
// sub-isolate, which fails inside a Firebase background isolate.

Future<Client?> _buildClient() async {
  try {
    // vodozemac may already be initialised in this isolate (e.g. a decrypt task
    // ran before this reply task reused the same engine). Calling init() twice
    // throws — swallow it so it doesn't fail the whole client build.
    try {
      await vod.init();
    } catch (_) {}

    final appDir = await getApplicationSupportDirectory();
    final dbPath = p.join(appDir.path, 'pyramid.sqlite');

    final db = await sqflite_native.databaseFactory
        .openDatabase(dbPath)
        .timeout(const Duration(seconds: 5));

    // Prevent "database is locked" if the main process still has the DB open.
    try {
      await db.execute('PRAGMA busy_timeout = 5000');
      await db.execute('PRAGMA journal_mode = WAL');
    } catch (_) {}

    final sdkDb = await MatrixSdkDatabase.init('pyramid', database: db);

    final client = Client(
      'Pyramid',
      database: sdkDb,
      nativeImplementations: NativeImplementationsDummy(),
      logLevel: Level.error,
    );

    await client
        .init(
          waitForFirstSync: false,
          waitUntilLoadCompletedLoaded: false,
        )
        .timeout(const Duration(seconds: 15));

    return client;
  } catch (e) {
    debugPrint('[NOTIF-BGENGINE] _buildClient failed: $e');
    return null;
  }
}

String _buildBody(Event event) {
  final msgType = event.messageType;
  final rawBody = event.body;
  return switch (msgType) {
    'm.text'    => rawBody.length > 100 ? '${rawBody.substring(0, 100)}…' : rawBody,
    'm.image'   => '📷 Bild',
    'm.video'   => '🎥 Video',
    'm.audio'   => '🎵 Audio',
    'm.file'    => '📄 Datei',
    'm.sticker' => '🏷️ Sticker',
    _           => rawBody.isEmpty ? 'Neue Nachricht' : (rawBody.length > 100 ? '${rawBody.substring(0, 100)}…' : rawBody),
  };
}

// ── Notification action handler ───────────────────────────────────────────────
// Called in a fresh Dart isolate when the user presses Reply or Gelesen.
// Must return quickly — Android keeps the notification's "sending" spinner
// visible until this function returns.

@pragma('vm:entry-point')
Future<void> handleBackgroundNotificationResponse(
    NotificationResponse details) async {
  WidgetsFlutterBinding.ensureInitialized();

  debugPrint('[NOTIF-BG] handleBackgroundNotificationResponse fired');
  debugPrint('[NOTIF-BG]   actionId=${details.actionId}  payload=${details.payload}  input=${details.input}  id=${details.id}');

  // ── Fast path: main isolate is running ───────────────────────────────────
  // If the Pyramid UI process is alive (app is backgrounded but not killed),
  // forward the action directly via IsolateNameServer. This avoids starting
  // any new engine and works silently without bringing the app to foreground.
  final sendPort = IsolateNameServer.lookupPortByName(kMainIsolateName);
  if (sendPort != null) {
    debugPrint('[NOTIF-BG] main isolate alive → forwarding via IsolateNameServer');
    sendPort.send(<String, dynamic>{
      'actionId': details.actionId,
      'payload':  details.payload,
      'input':    details.input,
      'id':       details.id,
    });
    return;
  }

  // ── Slow path: app was killed ─────────────────────────────────────────────
  // Main isolate is gone. Dismiss the notification and, for reply actions,
  // persist the text in SharedPreferences so _sendPendingReply picks it up
  // the next time the app opens (via getInitialReply cold-start flow).
  debugPrint('[NOTIF-BG] main isolate not running → SharedPreferences fallback');

  final plugin = FlutterLocalNotificationsPlugin();
  await plugin.initialize(
    const InitializationSettings(
        android: AndroidInitializationSettings('@drawable/ic_notification')),
    onDidReceiveBackgroundNotificationResponse: handleBackgroundNotificationResponse,
  );

  final roomId = details.payload;
  if (roomId != null) {
    await plugin.cancel(_stableId(roomId));
    debugPrint('[NOTIF-BG] cancelled notification for room $roomId');
  } else if (details.id != null) {
    await plugin.cancel(details.id!);
    debugPrint('[NOTIF-BG] cancelled notification id ${details.id}');
  }

  if (details.actionId != 'reply') {
    debugPrint('[NOTIF-BG] not a reply action, done');
    return;
  }

  final text = details.input?.trim() ?? '';
  if (text.isEmpty || roomId == null) {
    debugPrint('[NOTIF-BG] text empty or roomId null, aborting');
    return;
  }

  // App killed — send the encrypted reply directly from this background isolate.
  // _bgSendReply falls back to the prefs queue if it can't send (so the main app
  // delivers it on next launch).
  final sent = await _bgSendReply(roomId, text);
  debugPrint('[NOTIF-BG] background reply sent=$sent room=$roomId');
}

