// Version: 1.3.0 | Created: 2026-04-01 | Updated: 2026-04-10
// Chat repository — bridges Matrix SDK timeline to app domain models.
// Phase 2 additions: sendFile, sendReaction, redactEvent, reply support.

import 'package:matrix/matrix.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import '../../../core/network/matrix_client.dart';
import '../../../shared/utils/matrix_id.dart';
import '../../../shared/utils/mxc_url.dart';
import '../domain/message_model.dart';

part 'chat_repository.g.dart';

@riverpod
ChatRepository chatRepository(Ref ref) {
  return ChatRepository(client: ref.watch(matrixClientProvider));
}

class ChatRepository {
  ChatRepository({required Client client}) : _client = client;

  final Client _client;

  Room? _getRoom(String roomId) => _client.getRoomById(roomId);

  /// Returns a stream of message lists for [roomId].
  Stream<List<MessageModel>> watchTimeline(String roomId) async* {
    final room = _getRoom(roomId);
    if (room == null) return;

    final timeline = await room.getTimeline();

    yield await _mapTimeline(timeline, room);

    await for (final update in _client.onSync.stream) {
      final updatesThisRoom = update.rooms?.join?.containsKey(roomId) ?? false;
      if (updatesThisRoom) {
        yield await _mapTimeline(timeline, room);
      }
    }

    timeline.cancelSubscriptions();
  }

  /// Sends a plain text message. Supports replies via [inReplyToEventId].
  Future<void> sendTextMessage(
    String roomId,
    String text, {
    String? inReplyToEventId,
  }) async {
    final room = _getRoom(roomId);
    if (room == null) return;

    if (inReplyToEventId != null) {
      // Find the original event in the timeline for the in-reply-to relation.
      final timeline = await room.getTimeline();
      final inReplyTo = timeline.events
          .where((e) => e.eventId == inReplyToEventId)
          .firstOrNull;
      timeline.cancelSubscriptions();

      await room.sendTextEvent(text, inReplyTo: inReplyTo);
    } else {
      await room.sendTextEvent(text);
    }
  }

  /// Uploads [file] using Matrix media API, then sends an m.room.message.
  Future<void> sendFile(String roomId, MatrixFile file) async {
    final room = _getRoom(roomId);
    if (room == null) return;
    await room.sendFileEvent(file);
  }

  /// Sends an emoji reaction to [eventId].
  Future<void> sendReaction(String roomId, String eventId, String emoji) async {
    final room = _getRoom(roomId);
    if (room == null) return;
    await room.sendReaction(eventId, emoji);
  }

  /// Redacts (deletes) a message event.
  Future<void> redactEvent(String roomId, String eventId) async {
    final room = _getRoom(roomId);
    if (room == null) return;
    await room.redactEvent(eventId);
  }

  /// Sends a read receipt for the latest event in [roomId].
  Future<void> markAsRead(String roomId) async {
    final room = _getRoom(roomId);
    if (room == null) return;
    final lastEventId = room.lastEvent?.eventId ?? '';
    if (lastEventId.isEmpty) return;
    await room.setReadMarker(lastEventId, mRead: lastEventId);
  }

  /// Requests older messages (pagination).
  Future<void> loadMoreMessages(String roomId) async {
    final room = _getRoom(roomId);
    if (room == null) return;
    await room.requestHistory();
  }

  /// Event types that should never appear in the chat timeline.
  static const _hiddenEventTypes = {
    'org.matrix.msc3401.call.member',
    'org.matrix.msc4075.rtc.notification',
    'com.famedly.call.member',
    'm.call.invite',
    'm.call.answer',
    'm.call.hangup',
    'm.call.candidates',
    'm.call.reject',
    'm.room.member',
    'm.room.power_levels',
    'm.room.join_rules',
    'm.room.history_visibility',
    'm.room.guest_access',
    'm.room.create',
    'm.room.topic',
    'm.room.name',
    'm.room.avatar',
    'm.room.canonical_alias',
    'm.room.encryption',
  };

  Future<List<MessageModel>> _mapTimeline(Timeline timeline, Room room) async {
    final myUserId = _client.userID ?? '';
    final models = <MessageModel>[];
    for (final e in timeline.events) {
      if (_hiddenEventTypes.contains(e.type)) continue;
      models.add(_toModel(e, timeline, myUserId));
    }
    // Matrix SDK timeline.events is newest-first. Keep that order because
    // the chat ListView uses reverse:true — index 0 (newest) at the bottom.
    return models;
  }

  MessageModel _toModel(Event event, Timeline timeline, String myUserId) {
    final senderProfile = event.room.unsafeGetUserFromMemoryOrFallback(
      event.senderId,
    );

    // Resolve mxc:// to an HTTP URL for display.
    final mxcUrl = _extractMxcUrl(event);
    String? resolvedMediaUrl;
    if (mxcUrl != null) {
      resolvedMediaUrl = resolveMxcUrl(_client, Uri.parse(mxcUrl));
    }

    // Build reactions map: emoji → [senderId, ...]
    final reactionEvents = event.aggregatedEvents(
      timeline,
      RelationshipTypes.reaction,
    );
    final reactions = <String, List<String>>{};
    for (final r in reactionEvents) {
      final emoji =
          r.content.tryGet<Map<String, dynamic>>('m.relates_to')?['key']
              as String? ??
          r.content['key'] as String?;
      if (emoji != null) {
        reactions.putIfAbsent(emoji, () => []).add(r.senderId);
      }
    }

    // Read receipts: user IDs that have a receipt pointing to this event.
    final readBy = event.receipts.map((r) => r.user.id).toList();

    return MessageModel(
      eventId: event.eventId,
      roomId: event.roomId ?? '',
      senderId: event.senderId,
      senderDisplayName:
          senderProfile.displayName ?? event.senderId.matrixLocalpart,
      senderAvatarUrl: resolveMxcUrl(_client, senderProfile.avatarUrl),
      timestamp: event.originServerTs,
      type: _messageType(event),
      body: event.redacted ? null : event.body,
      mediaUrl: resolvedMediaUrl,
      mxcUrl: mxcUrl,
      inReplyToEventId: event.relationshipEventId,
      isMine: event.senderId == myUserId,
      isEdited: event.hasAggregatedEvents(timeline, RelationshipTypes.edit),
      reactions: reactions,
      readByUserIds: readBy,
    );
  }

  MessageType _messageType(Event event) {
    if (event.redacted) return MessageType.redacted;
    if (event.type == EventTypes.Encrypted) return MessageType.encrypted;

    return switch (event.messageType) {
      MessageTypes.Text => MessageType.text,
      MessageTypes.Image => MessageType.image,
      MessageTypes.File => MessageType.file,
      MessageTypes.Audio => MessageType.audio,
      MessageTypes.Video => MessageType.video,
      MessageTypes.Sticker => MessageType.sticker,
      _ => MessageType.unsupported,
    };
  }

  String? _extractMxcUrl(Event event) {
    final content = event.content;
    if (content.containsKey('url')) {
      return content['url'] as String?;
    }
    return null;
  }
}
