import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';

import 'package:Capsule/models/messages.dart';
import 'package:Capsule/models/text_messages.dart';
import 'package:mime/mime.dart';
import 'package:pointycastle/export.dart';
import 'package:sqflite/sqflite.dart';
import 'package:uuid/uuid.dart';

import '../utils/storage/write_file.dart';
import '/models/conversation_users.dart';
import '/models/friends.dart';
import '/models/my_profile.dart';
import '/utils/encryption/aes_helper.dart';
import '/utils/encryption/crypto_utils.dart';
import '/utils/storage/database.dart';
import '/utils/strings.dart';

Future<Conversation> createConversation(String title, List<Friend> friends, bool twoUser) async {
  final db = await getDatabaseConnection();

  MyProfile profile = await MyProfile.getProfile();

  var uuid = const Uuid();
  final String conversationId = uuid.v4();

  Uint8List symmetricKey = AesHelper.deriveKey(generateRandomString(32));

  Conversation conversation = Conversation(
    id: conversationId,
    userId: profile.id,
    symmetricKey: base64.encode(symmetricKey),
    admin: true,
    name: title,
    twoUser: twoUser,
    status: ConversationStatus.pending,
    isRead: true,
  );

  await db.insert(
    'conversations',
    conversation.toMap(),
    conflictAlgorithm: ConflictAlgorithm.replace,
  );

  await db.insert(
    'conversation_users',
    ConversationUser(
      id: uuid.v4(),
      userId: profile.id,
      conversationId: conversationId,
      username: profile.username,
      associationKey: uuid.v4(),
      publicKey: profile.publicKey!,
      admin: true,
    ).toMap(),
    conflictAlgorithm: ConflictAlgorithm.fail,
  );

  for (Friend friend in friends) {
    await db.insert(
      'conversation_users',
      ConversationUser(
        id: uuid.v4(),
        userId: friend.friendId,
        conversationId: conversationId,
        username: friend.username,
        associationKey: uuid.v4(),
        publicKey: friend.publicKey,
        admin: twoUser ? true : false,
      ).toMap(),
      conflictAlgorithm: ConflictAlgorithm.replace,
    );
  }

  if (twoUser) {
    List<Map<String, dynamic>> maps = await db.query(
      'conversation_users',
      where: 'conversation_id = ? AND user_id != ?',
      whereArgs: [ conversation.id, profile.id ],
    );

    if (maps.length != 1) {
      throw ArgumentError('Invalid user id');
    }

    conversation.name = maps[0]['username'];

    await db.insert(
      'conversations',
      conversation.toMap(),
      conflictAlgorithm: ConflictAlgorithm.replace,
    );
  }

  return conversation;
}


Future<Conversation> addUsersToConversation(Conversation conversation, List<Friend> friends) async {
  final db = await getDatabaseConnection();

  var uuid = const Uuid();

 for (Friend friend in friends) {
    await db.insert(
      'conversation_users',
      ConversationUser(
        id: uuid.v4(),
        userId: friend.friendId,
        conversationId: conversation.id,
        username: friend.username,
        associationKey: uuid.v4(),
        publicKey: friend.publicKey,
        admin: false,
      ).toMap(),
      conflictAlgorithm: ConflictAlgorithm.replace,
    );
  }

  return conversation;
}

Conversation findConversationByDetailId(List<Conversation> conversations, String id) {
  for (var conversation in conversations) {
    if (conversation.id == id) {
      return conversation;
    }
  }
  // Or return `null`.
  throw ArgumentError.value(id, 'id', 'No element with that id');
}

Future<Conversation> getConversationById(String id) async {
  final db = await getDatabaseConnection();

  final List<Map<String, dynamic>> maps = await db.query(
    'conversations',
    where: 'id = ?',
    whereArgs: [id],
  );

  if (maps.length != 1) {
    throw ArgumentError('Invalid user id');
  }

  File? file;
  if (maps[0]['file'] != null && maps[0]['file'] != '') {
    file = File(maps[0]['file']);
  }

  return Conversation(
    id: maps[0]['id'],
    userId: maps[0]['user_id'],
    symmetricKey: maps[0]['symmetric_key'],
    admin: maps[0]['admin'] == 1,
    name: maps[0]['name'],
    twoUser: maps[0]['two_user'] == 1,
    status: ConversationStatus.values[maps[0]['status']],
    isRead: maps[0]['is_read'] == 1,
    icon: file,
  );
}

// A method that retrieves all the dogs from the dogs table.
Future<List<Conversation>> getConversations() async {
  final db = await getDatabaseConnection();

  final List<Map<String, dynamic>> maps = await db.query(
    'conversations',
    orderBy: 'name',
  );

  return List.generate(maps.length, (i) {

    File? file;
    if (maps[i]['file'] != null && maps[i]['file'] != '') {
      file = File(maps[i]['file']);
    }

    return Conversation(
      id: maps[i]['id'],
      userId: maps[i]['user_id'],
      symmetricKey: maps[i]['symmetric_key'],
      admin: maps[i]['admin'] == 1,
      name: maps[i]['name'],
      twoUser: maps[i]['two_user'] == 1,
      status: ConversationStatus.values[maps[i]['status']],
      isRead: maps[i]['is_read'] == 1,
      icon: file,
    );
  });
}

Future<Conversation?> getTwoUserConversation(String userId) async {
  final db = await getDatabaseConnection();

  MyProfile profile = await MyProfile.getProfile();

  final List<Map<String, dynamic>> maps = await db.rawQuery(
    '''
    SELECT conversations.* FROM conversations
    LEFT JOIN conversation_users ON conversation_users.conversation_id = conversations.id
    WHERE conversation_users.user_id = ?
    AND conversation_users.user_id != ?
    AND conversations.two_user = 1
    ''',
    [ userId, profile.id ],
  );

  if (maps.length != 1) {
    return null;
  }

  return Conversation(
    id: maps[0]['id'],
    userId: maps[0]['user_id'],
    symmetricKey: maps[0]['symmetric_key'],
    admin: maps[0]['admin'] == 1,
    name: maps[0]['name'],
    twoUser: maps[0]['two_user'] == 1,
    status: ConversationStatus.values[maps[0]['status']],
    isRead: maps[0]['is_read'] == 1,
  );

}

class Conversation {
  String id;
  String userId;
  String symmetricKey;
  bool admin;
  String name;
  bool twoUser;
  ConversationStatus status;
  bool isRead;
  File? icon;

  Conversation({
    required this.id,
    required this.userId,
    required this.symmetricKey,
    required this.admin,
    required this.name,
    required this.twoUser,
    required this.status,
    required this.isRead,
    this.icon,
  });


  factory Conversation.fromJson(Map<String, dynamic> json, RSAPrivateKey privKey) {
    var symmetricKeyDecrypted = CryptoUtils.rsaDecrypt(
      base64.decode(json['symmetric_key']),
      privKey,
    );

    var id = AesHelper.aesDecrypt(
      symmetricKeyDecrypted,
      base64.decode(json['conversation_detail_id']),
    );

    var admin = AesHelper.aesDecrypt(
      symmetricKeyDecrypted,
      base64.decode(json['admin']),
    );

    return Conversation(
      id: id,
      userId: json['user_id'],
      symmetricKey: base64.encode(symmetricKeyDecrypted),
      admin: admin == 'true',
      name: 'Unknown',
      twoUser: false,
      status: ConversationStatus.complete,
      isRead: true,
    );
  }

  Future<Map<String, dynamic>> payloadJson({ bool includeUsers = true }) async {
    MyProfile profile = await MyProfile.getProfile();

    var symKey = base64.decode(symmetricKey);

    if (!includeUsers) {
      return {
        'id': id,
        'name': AesHelper.aesEncrypt(symKey, Uint8List.fromList(name.codeUnits)),
        'users': await getEncryptedConversationUsers(this, symKey),
      };
    }

    List<ConversationUser> users = await getConversationUsers(this);

    List<Object> userConversations = [];

    for (ConversationUser user in users) {

      RSAPublicKey pubKey = profile.publicKey!;
      String newId = id;

      if (profile.id != user.userId) {
        Friend friend = await getFriendByFriendId(user.userId);
        pubKey = friend.publicKey;
        newId = (const Uuid()).v4();
      }

      userConversations.add({
        'id': newId,
        'user_id': user.userId,
        'conversation_detail_id': AesHelper.aesEncrypt(symKey, Uint8List.fromList(id.codeUnits)),
        'admin': AesHelper.aesEncrypt(symKey, Uint8List.fromList((user.admin ? 'true' : 'false').codeUnits)),
        'symmetric_key': base64.encode(CryptoUtils.rsaEncrypt(symKey, pubKey)),
      });
    }

    Map<String, dynamic> returnData = {
      'id': id,
      'name': AesHelper.aesEncrypt(symKey, Uint8List.fromList(name.codeUnits)),
      'users': await getEncryptedConversationUsers(this, symKey),
      'two_user': AesHelper.aesEncrypt(symKey, Uint8List.fromList((twoUser ? 'true' : 'false').codeUnits)),
      'user_conversations': userConversations,
    };

    if (icon != null) {
      returnData['attachment'] = {
        'data': AesHelper.aesEncrypt(symmetricKey, Uint8List.fromList(icon!.readAsBytesSync())),
        'mimetype': lookupMimeType(icon!.path),
        'extension': getExtension(icon!.path),
      };
    }

    return returnData;
  }

  Map<String, dynamic> payloadImageJson() {
    if (icon == null) {
      return {};
    }

    return {
      'data': AesHelper.aesEncrypt(symmetricKey, Uint8List.fromList(icon!.readAsBytesSync())),
      'mimetype': lookupMimeType(icon!.path),
      'extension': getExtension(icon!.path),
    };
  }

  Map<String, dynamic> toMap() {
    return {
      'id': id,
      'user_id': userId,
      'symmetric_key': symmetricKey,
      'admin': admin ? 1 : 0,
      'name': name,
      'two_user': twoUser ? 1 : 0,
      'status': status.index,
      'is_read': isRead ? 1 : 0,
      'file': icon != null ? icon!.path : null,
    };
  }

  @override
  String toString() {
    return '''


        id: $id
        userId: $userId
        name: $name
        admin: $admin''';
  }

  Future<Message?> getRecentMessage() async {
    final db = await getDatabaseConnection();

    final List<Map<String, dynamic>> maps = await db.rawQuery(
      '''
        SELECT * FROM messages WHERE association_key IN (
          SELECT association_key FROM conversation_users WHERE conversation_id = ?
        )
        ORDER BY created_at DESC
        LIMIT 1;
      ''',
      [ id ],
    );

    if (maps.isEmpty) {
      return null;
    }

    return TextMessage(
        id: maps[0]['id'],
        symmetricKey: maps[0]['symmetric_key'],
        userSymmetricKey: maps[0]['user_symmetric_key'],
        text: maps[0]['data'] ?? 'Image',
        senderId: maps[0]['sender_id'],
        senderUsername: maps[0]['sender_username'],
        associationKey: maps[0]['association_key'],
        createdAt: maps[0]['created_at'],
        failedToSend: maps[0]['failed_to_send'] == 1,
    );
  }
}


enum ConversationStatus {
  complete,
  pending,
  error,
}
