import 'package:dio/dio.dart';
import 'package:island/database/drift_db.dart';
import 'package:island/database/message.dart';
import 'package:island/models/chat.dart';
import 'package:island/models/file.dart';
import 'package:island/services/file.dart';
import 'package:island/widgets/alert.dart';
import 'package:uuid/uuid.dart';

class MessageRepository {
  final SnChatRoom room;
  final SnChatMember identity;
  final Dio _apiClient;
  final AppDatabase _database;

  final Map<String, LocalChatMessage> pendingMessages = {};

  MessageRepository(this.room, this.identity, this._apiClient, this._database);

  Future<LocalChatMessage?> getLastMessages() async {
    final dbMessages = await _database.getMessagesForRoom(
      room.id,
      offset: 0,
      limit: 1,
    );

    if (dbMessages.isEmpty) {
      return null;
    }

    return _database.companionToMessage(dbMessages.first);
  }

  Future<bool> syncMessages() async {
    final lastMessage = await getLastMessages();
    if (lastMessage == null) return false;
    try {
      final resp = await _apiClient.post(
        '/chat/${room.id}/sync',
        data: {
          'last_sync_timestamp':
              lastMessage.toRemoteMessage().updatedAt.millisecondsSinceEpoch,
        },
      );

      final response = MessageSyncResponse.fromJson(resp.data);
      for (final change in response.changes) {
        switch (change.action) {
          case MessageChangeAction.create:
            await receiveMessage(change.message!);
            break;
          case MessageChangeAction.update:
            await receiveMessageUpdate(change.message!);
            break;
          case MessageChangeAction.delete:
            await receiveMessageDeletion(change.messageId.toString());
            break;
        }
      }
    } catch (err) {
      showErrorAlert(err);
    }
    return true;
  }

  Future<List<LocalChatMessage>> listMessages({
    int offset = 0,
    int take = 20,
    bool synced = false,
  }) async {
    try {
      final localMessages = await _getCachedMessages(
        room.id,
        offset: offset,
        take: take,
      );

      // If it already synced with the remote, skip this
      if (offset == 0 && !synced) {
        // Fetch latest messages
        _fetchAndCacheMessages(room.id, offset: offset, take: take);

        if (localMessages.isNotEmpty) {
          return localMessages;
        }
      }

      return await _fetchAndCacheMessages(room.id, offset: offset, take: take);
    } catch (e) {
      // If API fails but we have local messages, return them
      final localMessages = await _getCachedMessages(
        room.id,
        offset: offset,
        take: take,
      );

      if (localMessages.isNotEmpty) {
        return localMessages;
      }
      rethrow;
    }
  }

  Future<List<LocalChatMessage>> _getCachedMessages(
    int roomId, {
    int offset = 0,
    int take = 20,
  }) async {
    // Get messages from local database
    final dbMessages = await _database.getMessagesForRoom(
      roomId,
      offset: offset,
      limit: take,
    );
    final dbLocalMessages =
        dbMessages.map(_database.companionToMessage).toList();

    // Combine with pending messages
    final pendingForRoom =
        pendingMessages.values.where((msg) => msg.roomId == roomId).toList();

    // Sort by timestamp descending (newest first)
    final allMessages = [...pendingForRoom, ...dbLocalMessages];
    allMessages.sort((a, b) => b.createdAt.compareTo(a.createdAt));

    // Apply pagination
    if (offset >= allMessages.length) {
      return [];
    }

    final end =
        (offset + take) > allMessages.length
            ? allMessages.length
            : (offset + take);
    return allMessages.sublist(offset, end);
  }

  Future<List<LocalChatMessage>> _fetchAndCacheMessages(
    int roomId, {
    int offset = 0,
    int take = 20,
  }) async {
    final response = await _apiClient.get(
      '/chat/$roomId/messages',
      queryParameters: {'offset': offset, 'take': take},
    );

    final total = int.parse(response.headers.value('X-Total') ?? '0');
    final List<dynamic> data = response.data;

    final messages =
        data.map((json) {
          final remoteMessage = SnChatMessage.fromJson(json);
          return LocalChatMessage.fromRemoteMessage(
            remoteMessage,
            MessageStatus.sent,
          );
        }).toList();

    for (final message in messages) {
      await _database.saveMessage(_database.messageToCompanion(message));
      if (message.nonce != null) {
        pendingMessages.removeWhere(
          (_, pendingMsg) => pendingMsg.nonce == message.nonce,
        );
      }
    }

    return messages;
  }

  Future<LocalChatMessage> sendMessage(
    String atk,
    String baseUrl,
    int roomId,
    String content,
    String nonce, {
    required List<UniversalFile> attachments,
    Map<String, dynamic>? meta,
    SnChatMessage? replyingTo,
    SnChatMessage? forwardingTo,
    SnChatMessage? editingTo,
    Function(LocalChatMessage)? onPending,
  }) async {
    // Generate a unique nonce for this message
    final nonce = const Uuid().v4();

    // Create a local message with pending status
    final mockMessage = SnChatMessage(
      id: 'pending_$nonce',
      chatRoomId: roomId,
      senderId: identity.id,
      content: content,
      createdAt: DateTime.now(),
      updatedAt: DateTime.now(),
      nonce: nonce,
      sender: identity,
    );

    final localMessage = LocalChatMessage.fromRemoteMessage(
      mockMessage,
      MessageStatus.pending,
    );

    // Store in memory and database
    pendingMessages[localMessage.id] = localMessage;
    await _database.saveMessage(_database.messageToCompanion(localMessage));
    onPending?.call(localMessage);

    try {
      var cloudAttachments = List.empty(growable: true);
      // Upload files
      for (var idx = 0; idx < attachments.length; idx++) {
        final cloudFile =
            await putMediaToCloud(
              fileData: attachments[idx].data,
              atk: atk,
              baseUrl: baseUrl,
              filename: attachments[idx].data.name ?? 'Post media',
              mimetype:
                  attachments[idx].data.mimeType ??
                  switch (attachments[idx].type) {
                    UniversalFileType.image => 'image/unknown',
                    UniversalFileType.video => 'video/unknown',
                    UniversalFileType.audio => 'audio/unknown',
                    UniversalFileType.file => 'application/octet-stream',
                  },
            ).future;
        if (cloudFile == null) {
          throw ArgumentError('Failed to upload the file...');
        }
        cloudAttachments.add(cloudFile);
      }

      // Send to server
      final response = await _apiClient.request(
        editingTo == null
            ? '/chat/$roomId/messages'
            : '/chat/$roomId/messages/${editingTo.id}',
        data: {
          'content': content,
          'attachments_id': cloudAttachments.map((e) => e.id).toList(),
          'replied_message_id': replyingTo?.id,
          'forwarded_message_id': forwardingTo?.id,
          'meta': meta,
          'nonce': nonce,
        },
        options: Options(method: editingTo == null ? 'POST' : 'PATCH'),
      );

      // Update with server response
      final remoteMessage = SnChatMessage.fromJson(response.data);
      final updatedMessage = LocalChatMessage.fromRemoteMessage(
        remoteMessage,
        MessageStatus.sent,
      );

      // Remove from pending and update in database
      pendingMessages.remove(localMessage.id);
      await _database.deleteMessage(localMessage.id);
      await _database.saveMessage(_database.messageToCompanion(updatedMessage));

      return updatedMessage;
    } catch (e) {
      // Update status to failed
      localMessage.status = MessageStatus.failed;
      pendingMessages[localMessage.id] = localMessage;
      await _database.updateMessageStatus(
        localMessage.id,
        MessageStatus.failed,
      );
      rethrow;
    }
  }

  Future<LocalChatMessage> retryMessage(String pendingMessageId) async {
    final message = pendingMessages[pendingMessageId];
    if (message == null) {
      throw Exception('Message not found');
    }

    // Update status back to pending
    message.status = MessageStatus.pending;
    pendingMessages[pendingMessageId] = message;
    await _database.updateMessageStatus(
      pendingMessageId,
      MessageStatus.pending,
    );

    try {
      // Send to server
      var remoteMessage = message.toRemoteMessage();
      final response = await _apiClient.post(
        '/chat/${message.roomId}/messages',
        data: {
          'content': remoteMessage.content,
          'attachments_id': remoteMessage.attachments,
          'meta': remoteMessage.meta,
          'nonce': message.nonce,
        },
      );

      // Update with server response
      remoteMessage = SnChatMessage.fromJson(response.data);
      final updatedMessage = LocalChatMessage.fromRemoteMessage(
        remoteMessage,
        MessageStatus.sent,
      );

      // Remove from pending and update in database
      pendingMessages.remove(pendingMessageId);
      await _database.deleteMessage(pendingMessageId);
      await _database.saveMessage(_database.messageToCompanion(updatedMessage));

      return updatedMessage;
    } catch (e) {
      // Update status to failed
      message.status = MessageStatus.failed;
      pendingMessages[pendingMessageId] = message;
      await _database.updateMessageStatus(
        pendingMessageId,
        MessageStatus.failed,
      );
      rethrow;
    }
  }

  Future<LocalChatMessage> receiveMessage(SnChatMessage remoteMessage) async {
    final localMessage = LocalChatMessage.fromRemoteMessage(
      remoteMessage,
      MessageStatus.sent,
    );

    if (remoteMessage.nonce != null) {
      pendingMessages.removeWhere(
        (_, pendingMsg) => pendingMsg.nonce == remoteMessage.nonce,
      );
    }

    await _database.saveMessage(_database.messageToCompanion(localMessage));
    return localMessage;
  }

  Future<LocalChatMessage> receiveMessageUpdate(
    SnChatMessage remoteMessage,
  ) async {
    final localMessage = LocalChatMessage.fromRemoteMessage(
      remoteMessage,
      MessageStatus.sent,
    );

    await _database.updateMessage(_database.messageToCompanion(localMessage));
    return localMessage;
  }

  Future<void> receiveMessageDeletion(String messageId) async {
    // Remove from pending messages if exists
    pendingMessages.remove(messageId);

    // Delete from local database
    await _database.deleteMessage(messageId);
  }

  Future<LocalChatMessage> updateMessage(
    String messageId,
    String content, {
    List<SnCloudFile>? attachments,
    Map<String, dynamic>? meta,
  }) async {
    final message = pendingMessages[messageId];
    if (message != null) {
      // Update pending message
      final rmMessage = message.toRemoteMessage();
      final updatedRemoteMessage = rmMessage.copyWith(
        content: content,
        meta: meta ?? rmMessage.meta,
      );
      final updatedLocalMessage = LocalChatMessage.fromRemoteMessage(
        updatedRemoteMessage,
        MessageStatus.pending,
      );
      pendingMessages[messageId] = updatedLocalMessage;
      await _database.updateMessage(
        _database.messageToCompanion(updatedLocalMessage),
      );
      return message;
    }

    try {
      // Update on server
      final response = await _apiClient.put(
        '/chat/${room.id}/messages/$messageId',
        data: {'content': content, 'attachments': attachments, 'meta': meta},
      );

      // Update local copy
      final remoteMessage = SnChatMessage.fromJson(response.data);
      final updatedMessage = LocalChatMessage.fromRemoteMessage(
        remoteMessage,
        MessageStatus.sent,
      );
      await _database.updateMessage(
        _database.messageToCompanion(updatedMessage),
      );
      return updatedMessage;
    } catch (e) {
      rethrow;
    }
  }

  Future<void> deleteMessage(String messageId) async {
    try {
      await _apiClient.delete('/chat/${room.id}/messages/$messageId');
      pendingMessages.remove(messageId);
      await _database.deleteMessage(messageId);
    } catch (e) {
      rethrow;
    }
  }

  Future<LocalChatMessage?> getMessageById(String messageId) async {
    try {
      // Attempt to get the message from the local database
      final localMessage =
          await (_database.select(_database.chatMessages)
            ..where((tbl) => tbl.id.equals(messageId))).getSingleOrNull();
      if (localMessage != null) {
        return _database.companionToMessage(localMessage);
      }

      // If not found locally, fetch from the server
      final response = await _apiClient.get(
        '/chat/${room.id}/messages/$messageId',
      );
      final remoteMessage = SnChatMessage.fromJson(response.data);
      final message = LocalChatMessage.fromRemoteMessage(
        remoteMessage,
        MessageStatus.sent,
      );

      // Save the fetched message to the local database
      await _database.saveMessage(_database.messageToCompanion(message));
      return message;
    } catch (e) {
      if (e is DioException) return null;
      // Handle errors
      rethrow;
    }
  }
}