using Ikar.Server.Data.Entities;
using Ikar.Shared;

namespace Ikar.Server.Infrastructure;

public static class DtoMapper
{
    private static readonly HashSet<string> AudioExtensions =
    [
        ".aac",
        ".flac",
        ".m4a",
        ".mp3",
        ".oga",
        ".ogg",
        ".opus",
        ".wav",
        ".webm"
    ];

    public static UserSummaryDto ToDto(this User user, PresenceTracker presenceTracker) =>
        new(
            user.Id,
            user.Username,
            user.DisplayName,
            presenceTracker.IsOnline(user.Id),
            user.PhoneNumber,
            user.Sessions
                .OrderByDescending(x => x.LastSeenAt)
                .Select(x => (DateTimeOffset?)x.LastSeenAt)
                .FirstOrDefault(),
            user.About,
            ResolveAvatarPath(user),
            user.IsBot);

    public static BotSummaryDto ToDto(this Bot bot) =>
        new(
            bot.Id,
            bot.UserId,
            bot.User.Username,
            bot.User.DisplayName,
            bot.User.About,
            bot.IsEnabled,
            bot.CreatedAt,
            bot.Commands
                .OrderBy(x => x.SortOrder)
                .ThenBy(x => x.Command, StringComparer.OrdinalIgnoreCase)
                .Select(x => new BotCommandDto(x.Command, x.Description))
                .ToList());

    public static PushDeviceDto ToDto(this PushDevice device) =>
        new(
            device.Id,
            device.Platform,
            device.InstallationId,
            device.DeviceName,
            device.NotificationsEnabled,
            device.UpdatedAt);

    public static MessageDto ToDto(this Message message, PresenceTracker presenceTracker, Guid currentUserId)
    {
        var sender = ResolveMessageSenderDto(message, presenceTracker, currentUserId);
        var displayAuthorName = ResolveDisplayAuthorName(message);
        var viewCount = message.Chat?.Type == ChatType.Channel && message.PostedAsChannel
            ? message.Views.Count
            : (int?)null;

        return new MessageDto(
            message.Id,
            message.ChatId,
            message.Chat?.Type ?? ChatType.Direct,
            sender,
            message.Text,
            message.Attachments
                .OrderBy(x => x.SortOrder)
                .ThenBy(x => x.OriginalFileName)
                .Select(x => x.ToDto(message.ChatId))
                .ToList(),
            message.SentAt,
            message.EditedAt,
            message.DeletedAt,
            ResolveDeliveryState(message, currentUserId),
            message.ForwardedFromDisplayName,
            BuildReplyDto(message.ReplyToMessage, message.Chat),
            message.PinnedAt,
            message.PinnedByUser?.ToDto(presenceTracker),
            message.MediaAlbumId,
            BuildReactions(message, currentUserId),
            message.HasProtectedContent || message.Chat?.HasProtectedContent == true,
            message.ExpiresAt,
            message.TtlSeconds,
            message.PostedAsChannel,
            displayAuthorName,
            viewCount);
    }

    public static AttachmentDto ToDto(this MessageAttachment attachment, Guid chatId) =>
        new(
            attachment.Id,
            attachment.OriginalFileName,
            attachment.ContentType,
            attachment.FileSizeBytes,
            $"/api/chats/{chatId}/attachments/{attachment.Id}/content",
            ResolveAttachmentKind(attachment),
            attachment.SortOrder);

    public static ChatSummaryDto ToSummaryDto(
        this Chat chat,
        Guid currentUserId,
        PresenceTracker presenceTracker,
        Message? latestMessage = null,
        int? unreadCountOverride = null)
    {
        var currentMembership = chat.Members.FirstOrDefault(x => x.UserId == currentUserId);
        var permissions = BuildPermissions(currentMembership, chat.Type);
        var canSendMessages = permissions.CanPostMessages;
        var channelStats = BuildChannelStats(chat);
        var visibleMembers = BuildVisibleMembers(chat, currentMembership);
        var orderedMembers = visibleMembers
            .Select(x => x.User)
            .OrderBy(x => x.DisplayName)
            .ToList();

        var participants = orderedMembers
            .Select(user => user.ToDto(presenceTracker))
            .ToList();

        var counterpart = chat.Type == ChatType.Direct
            ? orderedMembers.FirstOrDefault(x => x.Id != currentUserId)
            : null;
        var title = chat.Type == ChatType.Direct
            ? counterpart?.DisplayName ?? (string.IsNullOrWhiteSpace(chat.Title) ? "Dialog" : chat.Title)
            : string.IsNullOrWhiteSpace(chat.Title) ? "New Group" : chat.Title;

        var secondary = chat.Type switch
        {
            ChatType.Direct => counterpart is null
                ? null
                : counterpart.IsBot
                    ? $"бот · @{counterpart.Username}"
                    : $"@{counterpart.Username}",
            ChatType.Channel => $"{FormatSubscribers(channelStats.SubscriberCount)}{(canSendMessages ? " · публикация разрешена" : string.Empty)}",
            _ => $"{participants.Count} members"
        };

        var lastMessage = latestMessage ?? chat.Messages.OrderByDescending(x => x.SentAt).FirstOrDefault();
        if (lastMessage is not null)
        {
            lastMessage.Chat ??= chat;
        }

        var lastPreview = lastMessage is null ? null : BuildMessagePreview(lastMessage);
        var lastMessageAttachment = lastMessage is null ||
            lastMessage.DeletedAt is not null ||
            !string.IsNullOrWhiteSpace(lastMessage.Text)
                ? null
                : lastMessage.Attachments
                    .OrderBy(attachment => attachment.SortOrder)
                    .ThenBy(attachment => attachment.Id)
                    .FirstOrDefault()
                    ?.ToDto(chat.Id);
        var unreadCount = unreadCountOverride ?? chat.Messages.Count(message =>
            message.SenderId != currentUserId &&
            message.DeletedAt is null &&
            (currentMembership?.LastReadAt is null || message.SentAt > currentMembership.LastReadAt.Value));

        return new ChatSummaryDto(
            chat.Id,
            title,
            chat.Type,
            secondary,
            chat.LastActivityAt ?? chat.CreatedAt,
            lastPreview,
            participants,
            canSendMessages,
            unreadCount,
            currentMembership?.IsPinned == true,
            currentMembership?.IsArchived == true,
            currentMembership?.MutedUntil is not null && currentMembership.MutedUntil > DateTimeOffset.UtcNow,
            currentMembership?.FolderKey,
            chat.HasProtectedContent,
            chat.DefaultMessageTtlSeconds,
            lastMessageAttachment,
            channelStats.SubscriberCount,
            channelStats.AdminCount,
            currentMembership is not null,
            chat.Type == ChatType.Channel && currentMembership is null && !string.IsNullOrWhiteSpace(chat.PublicUsername),
            permissions.CanManageMembers,
            chat.Type == ChatType.Channel && canSendMessages,
            chat.PublicUsername,
            chat.LinkedDiscussionChatId,
            chat.ChannelSignaturesEnabled,
            chat.Description);
    }

    public static ChatDetailsDto ToDetailsDto(
        this Chat chat,
        Guid currentUserId,
        PresenceTracker presenceTracker,
        Guid? firstUnreadMessageId = null,
        IReadOnlyList<Message>? messagesOverride = null,
        bool hasMoreMessages = false,
        IReadOnlyList<Message>? pinnedMessagesOverride = null)
    {
        var currentMembership = chat.Members.FirstOrDefault(x => x.UserId == currentUserId);
        var permissions = BuildPermissions(currentMembership, chat.Type);
        var canSendMessages = permissions.CanPostMessages;
        var channelStats = BuildChannelStats(chat);
        var visibleMembers = BuildVisibleMembers(chat, currentMembership);
        var participants = visibleMembers
            .Select(x => x.User)
            .OrderBy(x => x.DisplayName)
            .Select(x => x.ToDto(presenceTracker))
            .ToList();
        var members = visibleMembers
            .OrderByDescending(x => x.IsOwner || x.Role == ChatMemberRole.Owner)
            .ThenByDescending(x => x.Role == ChatMemberRole.Admin)
            .ThenBy(x => x.User.DisplayName)
            .Select(x => new ChatMemberDto(
                x.User.ToDto(presenceTracker),
                x.Role,
                x.IsOwner,
                BuildPermissions(x, chat.Type)))
            .ToList();

        var counterpart = chat.Type == ChatType.Direct
            ? chat.Members.Select(x => x.User).FirstOrDefault(x => x.Id != currentUserId)
            : null;

        var title = chat.Type == ChatType.Direct
            ? counterpart?.DisplayName ?? (string.IsNullOrWhiteSpace(chat.Title) ? "Dialog" : chat.Title)
            : string.IsNullOrWhiteSpace(chat.Title) ? "New Group" : chat.Title;

        var sourceMessages = messagesOverride ?? chat.Messages.OrderBy(x => x.SentAt).ToList();
        foreach (var message in sourceMessages)
        {
            message.Chat = chat;
        }

        var messages = sourceMessages
            .OrderBy(x => x.SentAt)
            .Select(x => x.ToDto(presenceTracker, currentUserId))
            .ToList();

        var pinnedMessages = (pinnedMessagesOverride ?? chat.Messages
                .Where(x => x.PinnedAt is not null && x.DeletedAt is null)
                .OrderByDescending(x => x.PinnedAt)
                .Take(5)
                .ToList())
            .Select(message =>
            {
                message.Chat = chat;
                return message.ToDto(presenceTracker, currentUserId);
            })
            .ToList();

        return new ChatDetailsDto(
            chat.Id,
            title,
            chat.Type,
            participants,
            canSendMessages,
            messages,
            firstUnreadMessageId,
            hasMoreMessages,
            pinnedMessages,
            permissions,
            members,
            chat.HasProtectedContent,
            chat.DefaultMessageTtlSeconds,
            channelStats.SubscriberCount,
            channelStats.AdminCount,
            currentMembership is not null,
            chat.Type == ChatType.Channel && currentMembership is null && !string.IsNullOrWhiteSpace(chat.PublicUsername),
            permissions.CanManageMembers,
            chat.Type == ChatType.Channel && canSendMessages,
            chat.PublicUsername,
            chat.LinkedDiscussionChatId,
            chat.ChannelSignaturesEnabled,
            chat.Description);
    }

    public static ModerationLogDto ToDto(this ModerationLogEntry entry, PresenceTracker presenceTracker) =>
        new(
            entry.Id,
            entry.ChatId,
            entry.ActorUserId,
            string.IsNullOrWhiteSpace(entry.ActorUser.DisplayName)
                ? entry.ActorUser.Username
                : entry.ActorUser.DisplayName,
            entry.TargetType,
            entry.TargetId,
            entry.Action,
            entry.Reason,
            entry.CreatedAt);

    public static StoryDto ToDto(this Story story, PresenceTracker presenceTracker, Guid currentUserId) =>
        new(
            story.Id,
            story.AuthorUser.ToDto(presenceTracker),
            story.DeletedAt is null ? story.Text : string.Empty,
            story.DeletedAt is null
                ? story.Attachments
                    .OrderBy(x => x.SortOrder)
                    .ThenBy(x => x.OriginalFileName)
                    .Select(x => x.ToDto(story.Id))
                    .ToList()
                : [],
            story.Visibility,
            story.HasProtectedContent,
            story.CreatedAt,
            story.ExpiresAt,
            story.DeletedAt,
            story.Views.Count,
            story.Views.Any(x => x.ViewerUserId == currentUserId));

    public static AttachmentDto ToDto(this StoryAttachment attachment, Guid storyId) =>
        new(
            attachment.Id,
            attachment.OriginalFileName,
            attachment.ContentType,
            attachment.FileSizeBytes,
            $"/api/stories/{storyId}/attachments/{attachment.Id}/content",
            AttachmentKind.File,
            attachment.SortOrder);

    public static ChatPermissionsDto BuildPermissions(ChatMember? membership, ChatType chatType)
    {
        if (membership is null)
        {
            return new ChatPermissionsDto(false, false, false, false, false);
        }

        var isManager = membership.IsOwner || membership.Role is ChatMemberRole.Owner or ChatMemberRole.Admin;
        var canPost = chatType != ChatType.Channel || isManager || membership.CanPostMessages;
        return new ChatPermissionsDto(
            CanPostMessages: canPost,
            CanPinMessages: isManager || membership.CanPinMessages,
            CanDeleteMessages: isManager || membership.CanDeleteMessages,
            CanInviteUsers: isManager || membership.CanInviteUsers,
            CanManageMembers: isManager);
    }

    private static IReadOnlyList<ChatMember> BuildVisibleMembers(Chat chat, ChatMember? currentMembership)
    {
        if (chat.Type != ChatType.Channel)
        {
            return chat.Members.ToList();
        }

        if (currentMembership is not null && BuildPermissions(currentMembership, chat.Type).CanManageMembers)
        {
            return chat.Members.ToList();
        }

        if (currentMembership is null)
        {
            return chat.Members
                .Where(IsChannelManager)
                .ToList();
        }

        return chat.Members
            .Where(member => member.UserId == currentMembership.UserId || IsChannelManager(member))
            .ToList();
    }

    private static ChannelStats BuildChannelStats(Chat chat)
    {
        if (chat.Type != ChatType.Channel)
        {
            var memberCount = chat.Members.Count;
            return new ChannelStats(memberCount, chat.Members.Count(IsChannelManager));
        }

        return new ChannelStats(
            chat.Members.Count,
            chat.Members.Count(IsChannelManager));
    }

    private static bool IsChannelManager(ChatMember member) =>
        member.IsOwner || member.Role is ChatMemberRole.Owner or ChatMemberRole.Admin;

    private static string FormatSubscribers(int count) =>
        $"{count} {FormatRussianPlural(count, "подписчик", "подписчика", "подписчиков")}";

    private static string FormatRussianPlural(int count, string one, string few, string many)
    {
        var value = Math.Abs(count) % 100;
        var lastDigit = value % 10;
        if (value is >= 11 and <= 14)
        {
            return many;
        }

        return lastDigit switch
        {
            1 => one,
            >= 2 and <= 4 => few,
            _ => many
        };
    }

    private static UserSummaryDto ResolveMessageSenderDto(Message message, PresenceTracker presenceTracker, Guid currentUserId)
    {
        if (message.Chat is { Type: ChatType.Channel } chat &&
            message.PostedAsChannel &&
            !CanInspectChannelSender(chat, currentUserId))
        {
            return BuildChannelIdentityDto(chat);
        }

        return message.Sender.ToDto(presenceTracker);
    }

    private static bool CanInspectChannelSender(Chat chat, Guid currentUserId)
    {
        var membership = chat.Members.FirstOrDefault(member => member.UserId == currentUserId);
        return membership is not null && BuildPermissions(membership, chat.Type).CanManageMembers;
    }

    private static UserSummaryDto BuildChannelIdentityDto(Chat chat) =>
        new(
            chat.Id,
            chat.PublicUsername ?? $"channel-{chat.Id:N}",
            string.IsNullOrWhiteSpace(chat.Title) ? "Channel" : chat.Title!,
            false,
            null,
            null,
            null,
            null);

    private static string ResolveDisplayAuthorName(Message message)
    {
        if (message.Chat?.Type == ChatType.Channel && message.PostedAsChannel)
        {
            if (!string.IsNullOrWhiteSpace(message.AuthorSignature))
            {
                return message.AuthorSignature!;
            }

            return string.IsNullOrWhiteSpace(message.Chat.Title) ? "Channel" : message.Chat.Title!;
        }

        return string.IsNullOrWhiteSpace(message.Sender.DisplayName)
            ? message.Sender.Username
            : message.Sender.DisplayName;
    }

    private static string ResolveReplyAuthorName(Message replyToMessage, Chat? currentChat)
    {
        if (currentChat?.Type == ChatType.Channel && replyToMessage.PostedAsChannel)
        {
            if (!string.IsNullOrWhiteSpace(replyToMessage.AuthorSignature))
            {
                return replyToMessage.AuthorSignature!;
            }

            return string.IsNullOrWhiteSpace(currentChat.Title) ? "Channel" : currentChat.Title!;
        }

        return string.IsNullOrWhiteSpace(replyToMessage.Sender.DisplayName)
            ? replyToMessage.Sender.Username
            : replyToMessage.Sender.DisplayName;
    }

    public static string BuildPushTitle(this Chat chat, Message message) =>
        chat.Type switch
        {
            ChatType.Direct => message.Sender.DisplayName,
            ChatType.Channel when message.PostedAsChannel => string.IsNullOrWhiteSpace(chat.Title) ? "Channel" : chat.Title!,
            _ => string.IsNullOrWhiteSpace(chat.Title) ? message.Sender.DisplayName : chat.Title!
        };

    public static string BuildPushBody(this Message message)
    {
        if (message.DeletedAt is not null)
        {
            return "Message removed";
        }

        if (!string.IsNullOrWhiteSpace(message.Text))
        {
            return message.Attachments.Count == 0
                ? message.Text
                : $"{message.Text} ({BuildAttachmentPreview(message.Attachments)})";
        }

        return BuildAttachmentPreview(message.Attachments);
    }

    private static string BuildMessagePreview(Message lastMessage)
    {
        var isChannelPost = lastMessage.Chat?.Type == ChatType.Channel && lastMessage.PostedAsChannel;

        if (lastMessage.DeletedAt is not null)
        {
            return isChannelPost
                ? "Публикация удалена"
                : $"{ResolveDisplayAuthorName(lastMessage)}: message removed";
        }

        if (string.IsNullOrWhiteSpace(lastMessage.Text))
        {
            var attachmentPreview = BuildAttachmentPreview(lastMessage.Attachments);
            return isChannelPost
                ? attachmentPreview
                : $"{ResolveDisplayAuthorName(lastMessage)}: {attachmentPreview}";
        }

        var textPreview = lastMessage.Attachments.Count == 0
            ? lastMessage.Text
            : $"{lastMessage.Text} ({BuildAttachmentPreview(lastMessage.Attachments)})";

        return isChannelPost
            ? textPreview
            : $"{ResolveDisplayAuthorName(lastMessage)}: {textPreview}";
    }

    private static MessageReplyDto? BuildReplyDto(Message? replyToMessage, Chat? currentChat)
    {
        if (replyToMessage is null)
        {
            return null;
        }

        return new MessageReplyDto(
            replyToMessage.Id,
            replyToMessage.SenderId,
            ResolveReplyAuthorName(replyToMessage, currentChat),
            BuildReplyPreviewText(replyToMessage));
    }

    private static IReadOnlyList<MessageReactionDto> BuildReactions(Message message, Guid currentUserId) =>
        message.Reactions
            .Where(reaction => !string.IsNullOrWhiteSpace(reaction.Emoji))
            .GroupBy(reaction => reaction.Emoji)
            .Select(group => new MessageReactionDto(
                group.Key,
                group.Count(),
                group.Any(reaction => reaction.UserId == currentUserId)))
            .OrderByDescending(reaction => reaction.Count)
            .ThenBy(reaction => reaction.Emoji, StringComparer.Ordinal)
            .ToList();

    private static string BuildAttachmentPreview(ICollection<MessageAttachment> attachments)
    {
        if (attachments.Count == 0)
        {
            return string.Empty;
        }

        if (attachments.Count == 1)
        {
            var attachment = attachments.First();
            if (ResolveAttachmentKind(attachment) == AttachmentKind.VoiceNote)
            {
                return "голосовое сообщение";
            }

            if (IsImageAttachment(attachment.ContentType, attachment.OriginalFileName))
            {
                return "фотография";
            }

            return $"файл: {attachment.OriginalFileName}";
        }

        if (attachments.All(attachment => IsImageAttachment(attachment.ContentType, attachment.OriginalFileName)))
        {
            return $"{attachments.Count} {FormatRussianPlural(attachments.Count, "фотография", "фотографии", "фотографий")}";
        }

        return $"{attachments.Count} {FormatRussianPlural(attachments.Count, "файл", "файла", "файлов")}";
    }

    private static string BuildReplyPreviewText(Message message)
    {
        if (message.DeletedAt is not null)
        {
            return "Сообщение удалено";
        }

        if (!string.IsNullOrWhiteSpace(message.Text))
        {
            return message.Attachments.Count == 0
                ? message.Text
                : $"{message.Text} ({BuildAttachmentPreview(message.Attachments)})";
        }

        return BuildAttachmentPreview(message.Attachments);
    }

    private static AttachmentKind ResolveAttachmentKind(MessageAttachment attachment) =>
        IsAudioAttachment(attachment.ContentType, attachment.OriginalFileName)
            ? AttachmentKind.VoiceNote
            : AttachmentKind.File;

    private static MessageDeliveryState ResolveDeliveryState(Message message, Guid currentUserId)
    {
        if (message.SenderId != currentUserId)
        {
            return MessageDeliveryState.Sent;
        }

        var recipients = (message.Chat?.Members ?? [])
            .Where(member => member.UserId != currentUserId)
            .ToList();

        if (recipients.Count == 0)
        {
            return MessageDeliveryState.Sent;
        }

        if (recipients.All(member => member.LastReadAt is not null && member.LastReadAt.Value >= message.SentAt))
        {
            return MessageDeliveryState.Read;
        }

        if (recipients.All(member => member.LastDeliveredAt is not null && member.LastDeliveredAt.Value >= message.SentAt))
        {
            return MessageDeliveryState.Delivered;
        }

        return MessageDeliveryState.Sent;
    }

    private static bool IsAudioAttachment(string? contentType, string? fileName)
    {
        if (!string.IsNullOrWhiteSpace(contentType) &&
            contentType.StartsWith("audio/", StringComparison.OrdinalIgnoreCase))
        {
            return true;
        }

        var extension = Path.GetExtension(fileName ?? string.Empty);
        return AudioExtensions.Contains(extension);
    }

    private static bool IsImageAttachment(string? contentType, string? fileName)
    {
        if (!string.IsNullOrWhiteSpace(contentType) &&
            contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
        {
            return true;
        }

        var extension = Path.GetExtension(fileName ?? string.Empty).ToLowerInvariant();
        return extension is ".jpg" or ".jpeg" or ".png" or ".webp" or ".gif";
    }

    private sealed record ChannelStats(int SubscriberCount, int AdminCount);

    private static string? ResolveAvatarPath(User user)
    {
        if (string.IsNullOrWhiteSpace(user.AvatarStoragePath))
        {
            return null;
        }

        var fileName = Path.GetFileName(user.AvatarStoragePath);
        return string.IsNullOrWhiteSpace(fileName)
            ? null
            : $"/api/users/{user.Id}/avatar/{Uri.EscapeDataString(fileName)}";
    }
}
