import 'dart:convert';

import '../core/utils/parse.dart';
import '../core/utils/text_format.dart';

enum AttachmentType {
  photo,
  video,
  audio,
  file,
  contact,
  location,
  sticker,
  control,
  poll,
  share,
  call,
  inlineKeyboard,
  forward,
  unknown,
}

String? decodeAttachPreview(dynamic raw) {
  if (raw is String) return raw;
  if (raw is List) {
    try {
      final bytes = List<int>.from(raw);
      return 'data:image/webp;base64,${base64Encode(bytes)}';
    } catch (_) {}
  }
  return null;
}

abstract class MessageAttachment {
  final AttachmentType type;
  final String? previewData;
  final String? baseUrl;
  final String? fileUrl;

  const MessageAttachment({
    required this.type,
    this.previewData,
    this.baseUrl,
    this.fileUrl,
  });

  factory MessageAttachment.fromMap(Map<String, dynamic> map) {
    final type = (map['_type'] as String? ?? '').toUpperCase();
    switch (type) {
      case 'PHOTO':
        return PhotoAttachment.fromMap(map);
      case 'VIDEO':
        return VideoAttachment.fromMap(map);
      case 'AUDIO':
        return AudioAttachment.fromMap(map);
      case 'FILE':
        return FileAttachment.fromMap(map);
      case 'STICKER':
        return StickerAttachment.fromMap(map);
      case 'CONTACT':
        return ContactAttachment.fromMap(map);
      case 'LOCATION':
        return LocationAttachment.fromMap(map);
      case 'CONTROL':
        return ControlAttachment.fromMap(map);
      case 'POLL':
        return PollAttachment.fromMap(map);
      case 'CALL':
        return CallAttachment.fromMap(map);
      case 'SHARE':
        return ShareAttachment.fromMap(map);
      case 'INLINE_KEYBOARD':
        return InlineKeyboardAttachment.fromMap(map);
      default:
        return UnknownAttachment(map);
    }
  }

  Map<String, dynamic> toMap();
}

class PhotoAttachment extends MessageAttachment {
  final int? photoId;
  final String? photoToken;
  final int? width;
  final int? height;
  final int? size;
  final String? localPath;

  const PhotoAttachment({
    super.previewData,
    super.baseUrl,
    super.fileUrl,
    this.photoId,
    this.photoToken,
    this.width,
    this.height,
    this.size,
    this.localPath,
  }) : super(type: AttachmentType.photo);

  factory PhotoAttachment.fromMap(Map<String, dynamic> map) {
    return PhotoAttachment(
      previewData: decodeAttachPreview(map['previewData']),
      baseUrl: (map['baseUrl'] ?? map['url']) as String?,
      photoId: map['photoId'] as int?,
      photoToken: map['photoToken'] as String?,
      width: map['width'] as int?,
      height: map['height'] as int?,
      size: map['size'] as int?,
    );
  }

  @override
  Map<String, dynamic> toMap() => {
    '_type': 'PHOTO',
    'previewData': previewData,
    'baseUrl': baseUrl,
    'photoId': photoId,
    'photoToken': photoToken,
    'width': width,
    'height': height,
    'size': size,
  };
}

class VideoAttachment extends MessageAttachment {
  final int? videoId;
  final String? videoToken;
  final String? thumbnail;
  final int? width;
  final int? height;
  final int? duration;
  final int? size;
  final String? localPath;

  final int? videoType;

  bool get isNote => videoType == 1;

  const VideoAttachment({
    super.previewData,
    super.baseUrl,
    super.fileUrl,
    this.videoId,
    this.videoToken,
    this.thumbnail,
    this.width,
    this.height,
    this.duration,
    this.size,
    this.videoType,
    this.localPath,
  }) : super(type: AttachmentType.video);

  factory VideoAttachment.fromMap(Map<String, dynamic> map) {
    return VideoAttachment(
      previewData: decodeAttachPreview(map['previewData']),
      baseUrl: map['baseUrl'] as String?,
      videoId: map['videoId'] as int?,
      videoToken: (map['token'] ?? map['videoToken'])?.toString(),
      thumbnail: map['thumbnail'] as String?,
      width: map['width'] as int?,
      height: map['height'] as int?,
      duration: map['duration'] as int?,
      size: map['size'] as int?,
      videoType: map['videoType'] as int?,
    );
  }

  @override
  Map<String, dynamic> toMap() => {
    '_type': 'VIDEO',
    'previewData': previewData,
    'baseUrl': baseUrl,
    'videoId': videoId,
    'videoToken': videoToken,
    'thumbnail': thumbnail,
    'width': width,
    'height': height,
    'duration': duration,
    'size': size,
    'videoType': videoType,
  };
}

class AudioAttachment extends MessageAttachment {
  final int? audioId;
  final String? audioToken;
  final int? duration;
  final int? size;
  final String? waveform;

  const AudioAttachment({
    super.previewData,
    super.baseUrl,
    super.fileUrl,
    this.audioId,
    this.audioToken,
    this.duration,
    this.size,
    this.waveform,
  }) : super(type: AttachmentType.audio);

  factory AudioAttachment.fromMap(Map<String, dynamic> map) {
    String? waveStr;
    final waveRaw = map['wave'];
    if (waveRaw is String) {
      waveStr = waveRaw;
    } else if (waveRaw is List) {
      try {
        final bytes = List<int>.from(waveRaw);
        waveStr = String.fromCharCodes(bytes);
      } catch (_) {}
    }

    return AudioAttachment(
      previewData: decodeAttachPreview(map['previewData']),
      baseUrl: map['baseUrl']?.toString(),
      fileUrl: map['url']?.toString(),
      audioId: map['audioId'] as int?,
      audioToken: map['token']?.toString(),
      duration: map['duration'] as int?,
      size: map['size'] as int?,
      waveform: waveStr,
    );
  }

  @override
  Map<String, dynamic> toMap() => {
    '_type': 'AUDIO',
    'previewData': previewData,
    'baseUrl': baseUrl,
    'audioId': audioId,
    'audioToken': audioToken,
    'duration': duration,
    'size': size,
    'waveform': waveform,
  };
}

class FileAttachment extends MessageAttachment {
  final int? fileId;
  final String? fileToken;
  final String? name;
  final int? size;
  final PhotoAttachment? preview;

  const FileAttachment({
    super.previewData,
    super.baseUrl,
    super.fileUrl,
    this.fileId,
    this.fileToken,
    this.name,
    this.size,
    this.preview,
  }) : super(type: AttachmentType.file);

  factory FileAttachment.fromMap(Map<String, dynamic> map) {
    PhotoAttachment? preview;
    final previewRaw = map['preview'];
    if (previewRaw is Map) {
      preview = PhotoAttachment.fromMap(Map<String, dynamic>.from(previewRaw));
    }

    return FileAttachment(
      previewData: decodeAttachPreview(map['previewData']),
      baseUrl: map['baseUrl'] as String?,
      fileId: map['fileId'] as int?,
      fileToken: (map['fileToken'] ?? map['token'])?.toString(),
      name: map['name'] as String?,
      size: map['size'] as int?,
      preview: preview,
    );
  }

  @override
  Map<String, dynamic> toMap() => {
    '_type': 'FILE',
    'previewData': previewData,
    'baseUrl': baseUrl,
    'fileId': fileId,
    'fileToken': fileToken,
    'name': name,
    'size': size,
    if (preview != null) 'preview': preview!.toMap(),
  };
}

class StickerAttachment extends MessageAttachment {
  final String? stickerId;
  final String? stickerPackId;
  final String? lottieUrl;
  final int? width;
  final int? height;

  const StickerAttachment({
    super.previewData,
    super.baseUrl,
    super.fileUrl,
    this.stickerId,
    this.stickerPackId,
    this.lottieUrl,
    this.width,
    this.height,
  }) : super(type: AttachmentType.sticker);

  bool get isAnimated => lottieUrl != null && lottieUrl!.isNotEmpty;

  factory StickerAttachment.fromMap(Map<String, dynamic> map) {
    return StickerAttachment(
      previewData: decodeAttachPreview(map['previewData']),
      baseUrl: (map['url'] ?? map['baseUrl'])?.toString(),
      stickerId: map['stickerId']?.toString(),
      stickerPackId:
          map['setId']?.toString() ?? map['stickerPackId']?.toString(),
      lottieUrl: map['lottieUrl']?.toString(),
      width: map['width'] as int?,
      height: map['height'] as int?,
    );
  }

  @override
  Map<String, dynamic> toMap() => {
    '_type': 'STICKER',
    'previewData': previewData,
    'baseUrl': baseUrl,
    'stickerId': stickerId,
    'stickerPackId': stickerPackId,
    'lottieUrl': lottieUrl,
    'width': width,
    'height': height,
  };
}

class ContactAttachment extends MessageAttachment {
  final String? userId;
  final String? firstName;
  final String? lastName;
  final String? phoneNumber;
  final String? photoUrl;
  final int? contactId;
  final String? name;

  const ContactAttachment({
    super.previewData,
    super.baseUrl,
    super.fileUrl,
    this.userId,
    this.firstName,
    this.lastName,
    this.phoneNumber,
    this.photoUrl,
    this.contactId,
    this.name,
  }) : super(type: AttachmentType.contact);

  factory ContactAttachment.fromMap(Map<String, dynamic> map) {
    return ContactAttachment(
      previewData: map['previewData']?.toString(),
      baseUrl: map['baseUrl']?.toString(),
      userId: map['userId']?.toString(),
      firstName: map['firstName']?.toString(),
      lastName: map['lastName']?.toString(),
      phoneNumber: map['phoneNumber']?.toString(),
      photoUrl: map['photoUrl']?.toString(),
      contactId: map['contactId'] is int
          ? map['contactId'] as int
          : int.tryParse(map['contactId']?.toString() ?? ''),
      name: map['name']?.toString(),
    );
  }

  @override
  Map<String, dynamic> toMap() => {
    '_type': 'CONTACT',
    'previewData': previewData,
    'baseUrl': baseUrl,
    'userId': userId,
    'firstName': firstName,
    'lastName': lastName,
    'phoneNumber': phoneNumber,
    'photoUrl': photoUrl,
    'contactId': contactId,
    'name': name,
  };
}

class LocationAttachment extends MessageAttachment {
  final double? latitude;
  final double? longitude;
  final double? zoom;
  final String? title;
  final String? address;

  const LocationAttachment({
    super.previewData,
    super.baseUrl,
    super.fileUrl,
    this.latitude,
    this.longitude,
    this.zoom,
    this.title,
    this.address,
  }) : super(type: AttachmentType.location);

  factory LocationAttachment.fromMap(Map<String, dynamic> map) {
    return LocationAttachment(
      previewData: map['previewData'] as String?,
      baseUrl: map['baseUrl'] as String?,
      latitude: (map['latitude'] as num?)?.toDouble(),
      longitude: (map['longitude'] as num?)?.toDouble(),
      zoom: (map['zoom'] as num?)?.toDouble(),
      title: map['title'] as String?,
      address: map['address'] as String?,
    );
  }

  @override
  Map<String, dynamic> toMap() => {
    '_type': 'LOCATION',
    'previewData': previewData,
    'baseUrl': baseUrl,
    'latitude': latitude,
    'longitude': longitude,
    'zoom': zoom,
    'title': title,
    'address': address,
  };
}

class ControlAttachment extends MessageAttachment {
  static const String botStartedEvent = 'botStarted';

  final String? event;
  final String? title;
  final List<int>? userIds;
  final int? userId;
  final String? startPayload;

  const ControlAttachment({
    super.previewData,
    super.baseUrl,
    super.fileUrl,
    this.event,
    this.title,
    this.userIds,
    this.userId,
    this.startPayload,
  }) : super(type: AttachmentType.control);

  bool get isBotStart => event == botStartedEvent;

  factory ControlAttachment.fromMap(Map<String, dynamic> map) {
    String? title = map['title']?.toString();
    if ((title == null || title.isEmpty) && map['shortMessage'] != null) {
      title = map['shortMessage'].toString();
    }

    return ControlAttachment(
      previewData: map['previewData']?.toString(),
      baseUrl: map['baseUrl']?.toString(),
      event: map['event']?.toString(),
      title: title,
      userIds: map['userIds'] is List ? parseIntList(map['userIds']) : null,
      userId: map['userId'] is int
          ? map['userId'] as int
          : int.tryParse(map['userId']?.toString() ?? ''),
      startPayload: map['startPayload']?.toString(),
    );
  }

  @override
  Map<String, dynamic> toMap() => {
    '_type': 'CONTROL',
    'previewData': previewData,
    'baseUrl': baseUrl,
    'event': event,
    'title': title,
    'userIds': userIds,
    'userId': userId,
    'startPayload': startPayload,
  };
}

class PollAttachment extends MessageAttachment {
  final int pollId;
  final String? title;

  const PollAttachment({required this.pollId, this.title})
    : super(type: AttachmentType.poll);

  factory PollAttachment.fromMap(Map<String, dynamic> map) {
    final id = map['pollId'] ?? map['id'];
    return PollAttachment(
      pollId: id is int ? id : int.tryParse(id?.toString() ?? '') ?? 0,
      title: (map['title'] ?? map['question'])?.toString(),
    );
  }

  @override
  Map<String, dynamic> toMap() => {
    '_type': 'POLL',
    'pollId': pollId,
    'title': title,
  };
}

class CallAttachment extends MessageAttachment {
  final bool isVideo;
  final int durationMs;
  final String? hangupType;
  final String? conversationId;
  final String? joinLink;
  final List<int> contactIds;

  const CallAttachment({
    required this.isVideo,
    this.durationMs = 0,
    this.hangupType,
    this.conversationId,
    this.joinLink,
    this.contactIds = const [],
  }) : super(type: AttachmentType.call);

  bool get isGroup => joinLink != null;

  bool get isMissedOrFailed =>
      durationMs == 0 ||
      hangupType == 'CANCELED' ||
      hangupType == 'REJECTED' ||
      hangupType == 'MISSED';

  factory CallAttachment.fromMap(Map<String, dynamic> map) {
    return CallAttachment(
      isVideo: (map['callType']?.toString().toUpperCase() == 'VIDEO'),
      durationMs: (map['duration'] as num?)?.toInt() ?? 0,
      hangupType: map['hangupType']?.toString(),
      conversationId: map['conversationId']?.toString(),
      joinLink: map['joinLink']?.toString(),
      contactIds: parseIntList(map['contactIds']),
    );
  }

  @override
  Map<String, dynamic> toMap() => {
    '_type': 'CALL',
    'callType': isVideo ? 'VIDEO' : 'AUDIO',
    'duration': durationMs,
    'hangupType': hangupType,
    'conversationId': conversationId,
    if (joinLink != null) 'joinLink': joinLink,
    if (contactIds.isNotEmpty) 'contactIds': contactIds,
  };
}

class ShareAttachment extends MessageAttachment {
  final int? shareId;
  final String? title;
  final String? description;
  final String? url;
  final String? host;
  final PhotoAttachment? image;

  const ShareAttachment({
    this.shareId,
    this.title,
    this.description,
    this.url,
    this.host,
    this.image,
  }) : super(type: AttachmentType.share);

  factory ShareAttachment.fromMap(Map<String, dynamic> map) {
    PhotoAttachment? image;
    final imageRaw = map['image'];
    if (imageRaw is Map) {
      image = PhotoAttachment.fromMap(Map<String, dynamic>.from(imageRaw));
    }

    return ShareAttachment(
      shareId: map['shareId'] as int?,
      title: map['title']?.toString(),
      description: map['description']?.toString(),
      url: map['url']?.toString(),
      host: map['host']?.toString(),
      image: image,
    );
  }

  @override
  Map<String, dynamic> toMap() => {
    '_type': 'SHARE',
    'shareId': shareId,
    'title': title,
    'description': description,
    'url': url,
    'host': host,
    if (image != null) 'image': image!.toMap(),
  };
}

class InlineKeyboardButton {
  final String type;
  final String text;
  final String? url;
  final String? webApp;
  final int? contactId;
  final String? payload;

  const InlineKeyboardButton({
    required this.type,
    required this.text,
    this.url,
    this.webApp,
    this.contactId,
    this.payload,
  });

  factory InlineKeyboardButton.fromMap(Map<String, dynamic> map) {
    return InlineKeyboardButton(
      type: (map['type'] as String? ?? '').toUpperCase(),
      text: map['text']?.toString() ?? '',
      url: map['url']?.toString(),
      webApp: map['webApp']?.toString(),
      contactId: map['contactId'] is int
          ? map['contactId'] as int
          : int.tryParse(map['contactId']?.toString() ?? ''),
      payload: map['payload']?.toString(),
    );
  }

  Map<String, dynamic> toMap() => {
    'type': type,
    'text': text,
    if (url != null) 'url': url,
    if (webApp != null) 'webApp': webApp,
    if (contactId != null) 'contactId': contactId,
    if (payload != null) 'payload': payload,
  };
}

class InlineKeyboardAttachment extends MessageAttachment {
  final String? callbackId;
  final List<List<InlineKeyboardButton>> rows;

  const InlineKeyboardAttachment({this.callbackId, required this.rows})
    : super(type: AttachmentType.inlineKeyboard);

  bool get isEmpty => rows.every((row) => row.isEmpty);

  factory InlineKeyboardAttachment.fromMap(Map<String, dynamic> map) {
    final keyboard = map['keyboard'];
    final rawRows = keyboard is Map ? keyboard['buttons'] as List? : null;
    final rows = <List<InlineKeyboardButton>>[];
    if (rawRows != null) {
      for (final row in rawRows) {
        if (row is! List) continue;
        rows.add(
          row
              .whereType<Map>()
              .map(
                (b) =>
                    InlineKeyboardButton.fromMap(Map<String, dynamic>.from(b)),
              )
              .toList(),
        );
      }
    }
    return InlineKeyboardAttachment(
      callbackId: map['callbackId']?.toString(),
      rows: rows,
    );
  }

  @override
  Map<String, dynamic> toMap() => {
    '_type': 'INLINE_KEYBOARD',
    if (callbackId != null) 'callbackId': callbackId,
    'keyboard': {
      'buttons': rows.map((row) => row.map((b) => b.toMap()).toList()).toList(),
    },
  };
}

class ForwardedMessageAttachment extends MessageAttachment {
  final int originalSenderId;
  final String? originalSenderName;
  final String? originalSenderAvatar;
  final String? originalType;
  final String? originalMessageId;
  final int? originalTime;
  final String? originalText;
  final int? originalChatId;
  final List<FormatRange> originalFormatRanges;
  final List<MessageAttachment>? originalAttachments;
  final ContactAttachment? originalContact;

  const ForwardedMessageAttachment({
    required this.originalSenderId,
    this.originalSenderName,
    this.originalSenderAvatar,
    this.originalType,
    this.originalMessageId,
    this.originalTime,
    this.originalText,
    this.originalChatId,
    this.originalFormatRanges = const [],
    this.originalAttachments,
    this.originalContact,
  }) : super(type: AttachmentType.forward);

  bool get isChannel => originalType == 'CHANNEL';

  factory ForwardedMessageAttachment.fromMap(Map<String, dynamic> map) {
    final linkRaw = map['link'];
    Map<String, dynamic>? link;
    if (linkRaw is Map) {
      link = Map<String, dynamic>.from(linkRaw);
    }

    Map<String, dynamic>? message;
    if (link != null) {
      final msgRaw = link['message'];
      if (msgRaw is Map) {
        message = Map<String, dynamic>.from(msgRaw);
      }
    }

    List<MessageAttachment>? originalAttaches;
    ContactAttachment? originalContact;
    if (message != null) {
      final attaches = message['attaches'] as List?;
      if (attaches != null) {
        final contactAttaches = attaches
            .whereType<Map>()
            .where((a) => (a['_type'] as String?)?.toUpperCase() == 'CONTACT')
            .toList();
        if (contactAttaches.isNotEmpty) {
          originalContact = ContactAttachment.fromMap(
            Map<String, dynamic>.from(contactAttaches.first),
          );
        }
        originalAttaches = attaches
            .whereType<Map>()
            .where((a) {
              final type = (a['_type'] as String?)?.toUpperCase();
              return type != 'CONTACT';
            })
            .map((a) => MessageAttachment.fromMap(Map<String, dynamic>.from(a)))
            .toList();
      }
    }

    final originalType = message?['type']?.toString().toUpperCase();
    final isChannel = originalType == 'CHANNEL';
    final channelName = link?['chatName']?.toString().trim();
    final channelAvatar = link?['chatIconUrl']?.toString().trim();

    return ForwardedMessageAttachment(
      originalSenderId: parseIntOrNull(message?['sender']) ?? 0,
      originalSenderName:
          isChannel && channelName != null && channelName.isNotEmpty
          ? channelName
          : null,
      originalSenderAvatar:
          isChannel && channelAvatar != null && channelAvatar.isNotEmpty
          ? channelAvatar
          : null,
      originalType: originalType,
      originalMessageId: message?['id']?.toString(),
      originalTime: parseIntOrNull(message?['time']),
      originalText: message?['text']?.toString(),
      originalChatId: parseIntOrNull(link?['chatId']),
      originalFormatRanges: parseFormatElements(message?['elements']),
      originalAttachments: originalAttaches,
      originalContact: originalContact,
    );
  }

  @override
  Map<String, dynamic> toMap() => {
    '_type': 'FORWARD',
    'originalSenderId': originalSenderId,
    'originalSenderName': originalSenderName,
    'originalSenderAvatar': originalSenderAvatar,
    'originalType': originalType,
    'originalMessageId': originalMessageId,
    'originalTime': originalTime,
    'originalText': originalText,
    'originalChatId': originalChatId,
    'originalElements': serializeFormatElements(originalFormatRanges),
  };
}

class UnknownAttachment extends MessageAttachment {
  final Map<String, dynamic> rawData;

  const UnknownAttachment(this.rawData) : super(type: AttachmentType.unknown);

  @override
  Map<String, dynamic> toMap() => rawData;
}
