import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'dart:ui';

import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:http/http.dart' as http;

import '../api/marianumcloud/nextcloud_ocs.dart';
import '../model/account_data.dart';
import '../notification/notification_service.dart';
import 'chat_thread_store.dart';
import 'nid_store.dart';
import 'push_renderer.dart';

/// Notification action identifiers shared between the renderer (which attaches
/// the actions) and the response handlers (which dispatch them).
const String kTalkReplyActionId = 'TALK_REPLY';
const String kTalkMarkReadActionId = 'TALK_MARK_READ';

/// Top-level FLN background entry point. A PLAIN FUNCTION with the pragma is
/// the reliable AOT form (same lesson as [pushOnBackgroundMessage]): resolving
/// static class members from a callback handle has failed in release builds
/// even with annotations present.
@pragma('vm:entry-point')
Future<void> pushActionBackgroundHandler(NotificationResponse response) =>
    PushActions.handleBackgroundResponse(response);

/// Logs to the developer log AND to stdout: `dart:developer` messages are
/// invisible in release logcat, but field debugging of the notification
/// action isolates needs `adb logcat -s flutter` to show what happened.
void _plog(String message) {
  log(message);
  debugPrint('PushActions: $message');
}

/// Handles Talk notification actions (inline reply, mark-as-read). Runs in the
/// background isolate spawned by flutter_local_notifications, so it may not
/// share any app state — it reads credentials straight from secure storage via
/// the [AccountData] singleton after awaiting population.
///
/// The class-level `vm:entry-point` pragma is REQUIRED in addition to the one
/// on [handleBackgroundResponse]: the callback is resolved via
/// `PluginUtilities.getCallbackFromHandle`, and in AOT builds resolving a
/// static method needs its enclosing class to be an entry point too —
/// otherwise the lookup fails with "To access ... PushActions from native
/// code, it must be annotated" and no action ever reaches Dart.
@pragma('vm:entry-point')
class PushActions {
  /// Background entry point for notification actions. Must be a top-level or
  /// static function annotated with `vm:entry-point` so AOT keeps it alive.
  @pragma('vm:entry-point')
  static Future<void> handleBackgroundResponse(
    NotificationResponse response,
  ) async {
    // The FLN action isolate starts WITHOUT main(): unlike the FCM background
    // isolate, plugins are not registered automatically there. Without this,
    // AccountData's secure-storage/prefs reads throw or never complete → no
    // auth header, the Talk POST never happens and the RemoteInput spinner
    // runs forever.
    DartPluginRegistrant.ensureInitialized();

    _plog(
      'action=${response.actionId} payload=${response.payload} '
      'hasInput=${response.input?.isNotEmpty ?? false}',
    );
    final chatToken = _chatTokenFrom(response.payload);
    if (chatToken == null) return;
    switch (response.actionId) {
      case kTalkReplyActionId:
        final text = response.input?.trim();
        final result = text == null || text.isEmpty
            ? (
                ok: false,
                detail: 'Keine Texteingabe empfangen (RemoteInput leer).',
              )
            : await sendReply(chatToken, text);
        // Local cleanup FIRST, mark-read AFTER: markRead makes the server
        // dismiss its notifications and emit delete-pushes. If those arrive
        // in the FCM isolate while the thread history still exists here, the
        // delete handler re-renders the thread and resurrects the just-
        // cancelled notification (with Android re-attaching the pending
        // inline reply on top).
        await finishReply(chatToken: chatToken, sent: result.ok);
        if (result.ok) {
          _cleanupNidEntry(response);
          // The user has evidently seen the chat — set the read marker like
          // the mark-read action does. Best effort: a failure here must not
          // fail the already-delivered reply.
          await markRead(chatToken);
        }
        if (!result.ok) {
          // Never swallow the typed message: surface the failure (and the
          // undelivered text) as its own notification. The nid mapping stays
          // alive — the thread notification is still in the tray.
          await _showActionError(
            chatToken: chatToken,
            title: 'Antwort nicht gesendet',
            body: actionFailureBody(lostText: text, detail: result.detail),
          );
        }
        break;
      case kTalkMarkReadActionId:
        // Optimistic like the in-app read-marker: clean up locally first so
        // the server's delete-pushes (triggered by markRead) can never race a
        // still-present thread history into a resurrected notification.
        await _cleanupChat(chatToken);
        _cleanupNidEntry(response);
        final result = await markRead(chatToken);
        if (!result.ok) {
          await _showActionError(
            chatToken: chatToken,
            title: 'Als gelesen markieren fehlgeschlagen',
            body: actionFailureBody(detail: result.detail),
          );
        }
        break;
      default:
        break;
    }
  }

  /// Body text of an action-failure notification: the undelivered reply (if
  /// any) followed by the technical reason.
  static String actionFailureBody({String? lostText, required String detail}) {
    return [
      if (lostText != null && lostText.isNotEmpty) 'Deine Nachricht: „$lostText“',
      'Grund: $detail',
    ].join('\n');
  }

  static Future<void> _showActionError({
    required String chatToken,
    required String title,
    required String body,
  }) async {
    try {
      await PushRenderer().renderTalkActionError(
        chatToken: chatToken,
        title: title,
        body: body,
      );
    } on Object catch (e) {
      _plog('Push action error notification failed: $e');
    }
  }

  /// Ends the reply interaction. The card is already gone — the action's
  /// native `cancelNotification` removed it when the reply fired (a Dart-side
  /// cancel is unreliable: MIUI/HyperOS ignores app cancels while an inline
  /// reply is pending). Success clears the stacked history (plus a defensive
  /// cancel); failure re-renders silently, a no-op when the card is really
  /// gone — the real failure surface is the error card posted by the caller.
  /// Injectable seams let tests observe the flow without platform channels.
  static Future<void> finishReply({
    required String chatToken,
    required bool sent,
    Future<void> Function(String chatToken)? cleanupChat,
    Future<List<ThreadMessage>> Function(String chatToken)? loadThread,
    Future<void> Function(String chatToken, List<ThreadMessage> messages)?
    renderSilent,
    Future<void> Function(String chatToken)? cancelNotification,
  }) async {
    cleanupChat ??= _cleanupChat;
    loadThread ??= (token) => ChatThreadStore().messages(token);
    renderSilent ??= (token, messages) =>
        PushRenderer().renderTalkThread(token, messages, alert: false);
    cancelNotification ??= _cancelChatNotification;

    if (sent) {
      await cleanupChat(chatToken);
      return;
    }
    final messages = await loadThread(chatToken);
    if (messages.isEmpty) {
      // Nothing to re-render (history cleared meanwhile) — cancel instead so
      // the spinner cannot survive.
      await cancelNotification(chatToken);
      return;
    }
    await renderSilent(chatToken, messages);
  }

  /// Sends the inline reply. Success = any 2xx (the Talk chat POST answers
  /// 201 Created). Path/body match the app's working send path
  /// (`SendMessage`: `v1/chat/{token}`, form field `message`).
  static Future<({bool ok, String detail})> sendReply(
    String chatToken,
    String message,
  ) => _ocsPost('apps/spreed/api/v1/chat/$chatToken', body: {'message': message});

  static Future<({bool ok, String detail})> markRead(String chatToken) =>
      _ocsPost('apps/spreed/api/v1/chat/$chatToken/read');

  static Future<({bool ok, String detail})> _ocsPost(
    String path, {
    Map<String, String>? body,
  }) async {
    try {
      // Bounded: a hanging population (e.g. keystore issue) must fail the
      // action instead of leaving the notification spinner running forever.
      final populated = await AccountData().waitForPopulation().timeout(
        const Duration(seconds: 10),
      );
      if (!populated) {
        _plog('Push action $path aborted: credentials unreadable in isolate');
        return (
          ok: false,
          detail: 'Zugangsdaten im Hintergrund-Prozess nicht lesbar.',
        );
      }
      final response = await http.post(
        NextcloudOcs.uri(path),
        headers: NextcloudOcs.headers(),
        body: body,
      );
      final ok = response.statusCode >= 200 && response.statusCode < 300;
      if (ok) {
        _plog('Push action $path -> HTTP ${response.statusCode}');
        return (ok: true, detail: 'HTTP ${response.statusCode}');
      }
      final preview = response.body.replaceAll(RegExp(r'\s+'), ' ').trim();
      final trimmed = preview.length > 200
          ? '${preview.substring(0, 200)}…'
          : preview;
      _plog('Push action $path -> HTTP ${response.statusCode} body=$trimmed');
      return (
        ok: false,
        detail:
            'HTTP ${response.statusCode}${trimmed.isEmpty ? '' : ' – $trimmed'}',
      );
    } on Object catch (e) {
      _plog('Push action $path failed: $e');
      return (ok: false, detail: e.toString());
    }
  }

  /// Mark-read cleanup: drop the stacked history (so the next push starts
  /// fresh) and cancel the chat's notification.
  static Future<void> _cleanupChat(String chatToken) async {
    try {
      await ChatThreadStore().clearChat(chatToken);
    } on Object catch (e) {
      _plog('Push action thread cleanup failed: $e');
    }
    await _cancelChatNotification(chatToken);
  }

  static Future<void> _cancelChatNotification(String chatToken) async {
    try {
      await NotificationService().flutterLocalNotificationsPlugin.cancel(
        id: stableChatNotificationId(chatToken),
        tag: chatNotificationTag(chatToken),
      );
    } on Object catch (e) {
      _plog('Push action cancel failed: $e');
    }
  }

  static void _cleanupNidEntry(NotificationResponse response) {
    final nid = _nidFrom(response.payload);
    if (nid == null) return;
    unawaited(
      NidStore()
          .delete(nid)
          .then(
            (_) {},
            onError: (Object e) => _plog('Push action nid cleanup failed: $e'),
          ),
    );
  }

  static String? _chatTokenFrom(String? payload) =>
      _payloadField(payload, 'chatToken');

  static int? _nidFrom(String? payload) {
    final raw = _payloadField(payload, 'nid');
    return raw == null ? null : int.tryParse(raw);
  }

  static String? _payloadField(String? payload, String key) {
    if (payload == null || payload.isEmpty) return null;
    try {
      final map = jsonDecode(payload) as Map<String, dynamic>;
      final value = map[key];
      return value?.toString();
    } on Object {
      return null;
    }
  }
}
