import 'dart:async';

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:matrix/matrix.dart';
import 'package:pyramid/core/e2ee_diagnostics.dart';
import 'package:pyramid/core/matrix_client.dart';

// AsyncNotifier so onUpdate can refresh state in-place without recreating the
// timeline (which would reset the loaded event window back to the default 30).
class TimelineNotifier extends FamilyAsyncNotifier<Timeline, String> {
  Timeline? _timeline;
  StreamSubscription<String>? _roomUpdateSub;
  Timer? _redecryptTimer;

  @override
  Future<Timeline> build(String arg) async {
    final roomId = arg;
    final client = await ref.watch(matrixClientProvider.future);
    final room = client.getRoomById(roomId);
    if (room == null) throw Exception('Raum nicht gefunden: $roomId');

    _timeline?.cancelSubscriptions();
    _roomUpdateSub?.cancel();
    _redecryptTimer?.cancel();

    final diag = ref.read(e2eeDiagnosticsProvider.notifier);

    final timeline = await room.getTimeline(
      onUpdate: _onUpdate,
    );
    _timeline = timeline;

    if (timeline.events.length < 20) {
      await timeline.requestHistory(historyCount: 60);
    }

    timeline.requestKeys(onlineKeyBackupOnly: false);
    unawaited(_decryptLegacyEvents(client, timeline, roomId, diag));

    // Re-decrypt whenever the room updates (e.g. after new keys arrive from
    // key requests). Debounced to avoid redundant passes on rapid updates.
    _roomUpdateSub = room.onUpdate.stream.listen((_) {
      _redecryptTimer?.cancel();
      _redecryptTimer = Timer(const Duration(milliseconds: 800), () {
        final t = _timeline;
        if (t != null) {
          unawaited(_decryptLegacyEvents(client, t, roomId, diag));
        }
      });
    });

    // Ensure device keys are downloaded for all room members so outbound
    // Megolm session key sharing works on the first send.
    if (room.encrypted && client.encryptionEnabled) {
      unawaited(_preloadDeviceKeys(client, room));
    }

    ref.onDispose(() {
      _roomUpdateSub?.cancel();
      _redecryptTimer?.cancel();
      _timeline?.cancelSubscriptions();
      _timeline = null;
    });
    return timeline;
  }

  void _onUpdate() {
    final t = _timeline;
    if (t != null) state = AsyncData(t);
  }
}

final timelineProvider =
    AsyncNotifierProvider.family<TimelineNotifier, Timeline, String>(
  TimelineNotifier.new,
);

Future<void> _decryptLegacyEvents(
  Client client,
  Timeline timeline,
  String roomId,
  E2eeDiagnosticsNotifier diag,
) async {
  final enc = client.encryption;
  if (enc == null) return;

  try {
    await enc.keyManager.loadAllKeysFromRoom(roomId);
  } catch (e) {
    Logs().e('[ChatProvider] loadAllKeysFromRoom failed', e);
  }

  if (!enc.enabled) return;

  var changed = false;
  for (var i = 0; i < timeline.events.length; i++) {
    final event = timeline.events[i];
    if (event.type != EventTypes.Encrypted) continue;
    try {
      final decrypted = await enc.decryptRoomEvent(
        event,
        store: true,
        updateType: EventUpdateType.history,
      );
      if (decrypted.type != EventTypes.Encrypted) {
        timeline.events[i] = decrypted;
        changed = true;
      } else {
        // Still encrypted — log for diagnostics
        final sessionId = event.content.tryGet<String>('session_id') ?? '';
        diag.add(E2eeDiagEntry(
          timestamp: DateTime.now(),
          roomId: roomId,
          eventId: event.eventId,
          sessionId: sessionId,
          error: decrypted.content.tryGet<String>('body') ?? 'unknown',
        ));
      }
    } catch (e) {
      final sessionId = event.content.tryGet<String>('session_id') ?? '';
      diag.add(E2eeDiagEntry(
        timestamp: DateTime.now(),
        roomId: roomId,
        eventId: event.eventId,
        sessionId: sessionId,
        error: e.toString(),
      ));
    }
  }
  // Notify UI of in-place changes only when something actually decrypted
  if (changed) timeline.onUpdate?.call();
}

Future<void> _preloadDeviceKeys(Client client, Room room) async {
  try {
    final members = await room.requestParticipants([Membership.join, Membership.invite]);
    final userIds = members.map((m) => m.id).toSet();
    await client.updateUserDeviceKeys(additionalUsers: userIds);
  } catch (e) {
    Logs().w('[ChatProvider] Device key preload failed: $e');
  }
}

final roomProvider = Provider.family<Room?, String>((ref, roomId) {
  final client = ref.watch(matrixClientProvider).valueOrNull;
  return client?.getRoomById(roomId);
});
