using System.Collections.Concurrent;
using System.Security.Claims;
using Knot.Contracts.Auth.Application.Abstractions;
using Knot.Contracts.Auth.Domain;
using Knot.Contracts.Conversations.Application.Abstractions;
using Knot.Contracts.Conversations.Domain;
using Knot.Contracts.Messaging.Application.Abstractions;
using Knot.Contracts.Messaging.Domain;
using Knot.Modules.Conversations.Application.DTOs;
using Knot.Modules.Conversations.Application.Messages.Delete;
using Knot.Modules.Conversations.Application.Messages.Edit;
using Knot.Modules.Conversations.Application.Messages.Pin;
using Knot.Modules.Conversations.Application.Messages.React;
using Knot.Modules.Conversations.Application.Messages.Read;
using Knot.Modules.Conversations.Application.Messages.Send;
using Knot.Modules.Conversations.Application.Messages.Unpin;
using Knot.Modules.Conversations.Application.Messages.Vote;
using Knot.Shared.Kernel;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;

namespace Knot.Modules.Conversations.Infrastructure.SignalR;

/// <summary>
/// Хаб SignalR для обработки сообщений и WebRTC сигналинга в реальном времени.
/// </summary>
[Authorize]
public sealed class ChatHub : Hub
{
    // Маппинг userId → список connectionId
    private static readonly ConcurrentDictionary<string, HashSet<string>> _userConnections = new();
    // chatId → (userId → ParticipantInfo)
    private static readonly ConcurrentDictionary<string, ConcurrentDictionary<string, ParticipantInfo>> _groupCallParticipants = new();

    public static int OnlineUsersCount => _userConnections.Count;
    public static bool IsUserOnline(string userId) => _userConnections.ContainsKey(userId);

    // userId → CallSession (one user can be in only one call at a time)
    private static readonly ConcurrentDictionary<string, CallSession> _activeSessionsByUser = new();
    // chatId → (startTime, callType)
    private static readonly ConcurrentDictionary<string, (DateTime StartTime, string CallType)> _activeGroupCalls = new();

    private readonly ISender _sender;
    private readonly IUserContext _userContext;
    private readonly IChatRepository _chatRepository;
    private readonly IUserRepository _userRepository;
    private readonly IMessageRepository _messageRepository;
    private readonly ILogger<ChatHub> _logger;
    private readonly IMemoryCache _cache;
    private readonly IUserDisplayNameProvider _userProvider;

    public ChatHub(
        ISender sender,
        IUserContext userContext,
        IChatRepository chatRepository,
        IUserRepository userRepository,
        IMessageRepository messageRepository,
        ILogger<ChatHub> logger,
        IMemoryCache cache,
        IUserDisplayNameProvider userProvider)
    {
        _sender = sender;
        _userContext = userContext;
        _chatRepository = chatRepository;
        _userRepository = userRepository;
        _messageRepository = messageRepository;
        _logger = logger;
        _cache = cache;
        _userProvider = userProvider;
    }

    public override async Task OnConnectedAsync()
    {
        if (_userContext.IsAuthenticated)
        {
            var userId = _userContext.UserId.ToString();
            _userConnections.AddOrUpdate(
                userId,
                _ => new HashSet<string> { Context.ConnectionId },
                (_, set) => { lock (set) { set.Add(Context.ConnectionId); } return set; }
            );

            _cache.Set("Global_OnlineUsersCount", _userConnections.Count);

            var userChats = await _chatRepository.GetUserChatsAsync(_userContext.UserId, Context.ConnectionAborted);
            foreach (var chat in userChats)
            {
                await Groups.AddToGroupAsync(Context.ConnectionId, chat.Id.ToString());
            }

            _logger.LogInformation("User {UserId} connected with {ConnectionId}, added to {ChatCount} chats",

                userId, Context.ConnectionId, userChats.Count);

            await Clients.Others.SendAsync("user_online", new { userId });
        }
        await base.OnConnectedAsync();
    }

    public override async Task OnDisconnectedAsync(Exception? exception)
    {
        if (_userContext.IsAuthenticated)
        {
            var userId = _userContext.UserId.ToString();
            if (_userConnections.TryGetValue(userId, out var set))
            {
                lock (set) { set.Remove(Context.ConnectionId); }
                if (set.Count == 0)
                {
                    _userConnections.TryRemove(userId, out _);
                    await Clients.Others.SendAsync("user_offline", new { userId, lastSeen = DateTime.UtcNow });
                }
            }
            _cache.Set("Global_OnlineUsersCount", _userConnections.Count);
            _logger.LogInformation("User {UserId} disconnected", userId);
        }
        await base.OnDisconnectedAsync(exception);
    }

    // ────────────────────────────────────────────────────────────────
    // Chat methods
    // ────────────────────────────────────────────────────────────────

    [HubMethodName("send_message")]
    public async Task SendMessage(SendMessageHubRequest request)
    {
        var attachments = request.Attachments?.Select(a =>

            new AttachmentRequest(a.Type, a.Url, a.FileName, a.FileSize)).ToList();

        var command = new SendMessageCommand(
            ChatId: request.ChatId,
            SenderId: _userContext.UserId,
            Content: request.Content,
            Type: request.Type,
            Attachments: attachments,
            ReplyToId: request.ReplyToId,
            Quote: request.Quote,
            ForwardedFromId: request.ForwardedFromId,
            PollOptions: request.PollOptions,
            PollIsAnonymous: request.PollIsAnonymous,
            PollAllowMultipleAnswers: request.PollAllowMultipleAnswers
        );

        await _sender.Send(command);
    }

    [HubMethodName("read_messages")]
    public async Task ReadMessages(ReadMessagesRequest request)
    {
        if (request.LastReadMessageId != Guid.Empty && request.LastReadSequenceId > 0)
        {
            var command = new ReadMessagesCommand(
                request.ChatId, _userContext.UserId, request.LastReadMessageId, request.LastReadSequenceId);
            await _sender.Send(command);
        }

        // Отправляем событие всем в чате о том, что пользователь прочитал сообщения
        await Clients.Group(request.ChatId.ToString()).SendAsync("messages_read", new
        {
            ChatId = request.ChatId.ToString(),
            UserId = _userContext.UserId,
            LastReadMessageId = request.LastReadMessageId,
            LastReadSequenceId = request.LastReadSequenceId
        });
    }

    [HubMethodName("delete_messages")]
    public async Task DeleteMessages(DeleteMessagesHubRequest request)
    {
        var parsedIds = request.MessageIds
            .Select(id => Guid.TryParse(id, out var parsed) ? parsed : Guid.Empty)
            .Where(id => id != Guid.Empty)
            .ToList();

        if (parsedIds.Any())
        {
            var command = new DeleteMessagesCommand(
                request.ChatId, _userContext.UserId, parsedIds, request.DeleteForAll);
            await _sender.Send(command);
        }
    }

    [HubMethodName("typing_start")]
    public async Task TypingStart(string chatId)
    {
        await Clients.Group(chatId).SendAsync("user_typing", new { ChatId = chatId, UserId = _userContext.UserId });
    }

    [HubMethodName("typing_stop")]
    public async Task TypingStop(string chatId)
    {
        await Clients.Group(chatId).SendAsync("user_stopped_typing", new { ChatId = chatId, UserId = _userContext.UserId });
    }

    [HubMethodName("join_chat")]
    public async Task JoinChat(string chatId)
    {
        // Simple security check: check if user is member of chat (optional but recommended)
        if (Guid.TryParse(chatId, out var chatGuid))
        {
            var userChats = await _chatRepository.GetUserChatsAsync(_userContext.UserId, Context.ConnectionAborted);
            if (userChats.Any(c => c.Id == chatGuid))
            {
                await Groups.AddToGroupAsync(Context.ConnectionId, chatId);
            }
        }
    }

    [HubMethodName("add_reaction")]
    public async Task AddReaction(AddReactionRequest request)
    {
        _logger.LogInformation("AddReaction called: MessageId={MessageId}, ChatId={ChatId}, Emoji={Emoji}, UserId={UserId}",

            request.MessageId, request.ChatId, request.Emoji, _userContext.UserId);


        var command = new AddReactionCommand(
            request.MessageId, _userContext.UserId, request.Emoji, request.ChatId);
        var result = await _sender.Send(command);
        if (result.IsFailure)
        {
            _logger.LogWarning("AddReaction failed: {Error}", result.Error.Description);
            throw new HubException(result.Error.Description);
        }


        _logger.LogInformation("AddReaction completed successfully");
    }

    [HubMethodName("remove_reaction")]
    public async Task RemoveReaction(RemoveReactionRequest request)
    {
        _logger.LogInformation("RemoveReaction called: MessageId={MessageId}, ChatId={ChatId}, Emoji={Emoji}, UserId={UserId}",

            request.MessageId, request.ChatId, request.Emoji, _userContext.UserId);


        var command = new RemoveReactionCommand(
            request.MessageId, _userContext.UserId, request.Emoji, request.ChatId);
        var result = await _sender.Send(command);
        if (result.IsFailure)
        {
            _logger.LogWarning("RemoveReaction failed: {Error}", result.Error.Description);
            throw new HubException(result.Error.Description);
        }


        _logger.LogInformation("RemoveReaction completed successfully");
    }

    [HubMethodName("pin_message")]
    public async Task PinMessage(PinMessageRequest request)
    {
        var command = new PinMessageCommand(request.MessageId, request.ChatId, _userContext.UserId);
        var result = await _sender.Send(command);
        var message = await _messageRepository.GetByIdAsync(request.MessageId, Context.ConnectionAborted);
        if (message != null)
        {
            var senderInfo = await _userProvider.GetUsersInfoAsync(new[] { message.SenderId });
            var dto = MessageMapper.MapToDto(message, senderInfo, Enumerable.Empty<MessageReaction>(), Enumerable.Empty<Guid>());

            await Clients.Group(request.ChatId.ToString()).SendAsync("message_pinned", new
            {
                chatId = request.ChatId,
                message = dto,
                userId = _userContext.UserId
            });
        }
    }

    [HubMethodName("unpin_message")]
    public async Task UnpinMessage(PinMessageRequest request)
    {
        var command = new UnpinMessageCommand(request.MessageId, request.ChatId, _userContext.UserId);
        var result = await _sender.Send(command);

        await Clients.Group(request.ChatId.ToString()).SendAsync("message_unpinned", new
        {
            chatId = request.ChatId,
            messageId = request.MessageId,
            userId = _userContext.UserId
        });
    }

    [HubMethodName("edit_message")]
    public async Task EditMessage(EditMessageHubRequest request)
    {
        var command = new EditMessageCommand(request.MessageId, request.ChatId, _userContext.UserId, request.Content);
        var result = await _sender.Send(command);
        if (result.IsFailure)
        {
            throw new HubException(result.Error.Description);
        }
    }

    [HubMethodName("vote_poll")]
    public async Task VotePoll(VotePollRequest request)
    {
        var command = new VotePollCommand(request.MessageId, request.ChatId, _userContext.UserId, request.OptionId);
        var result = await _sender.Send(command);
        if (result.IsFailure)
        {
            throw new HubException(result.Error.Description);
        }
    }

    // ────────────────────────────────────────────────────────────────
    // Friend signals (Proxy methods for real-time notification)
    // ────────────────────────────────────────────────────────────────

    [HubMethodName("friend_request")]
    public async Task FriendRequest(FriendSignalRequest request)
    {
        if (request == null || string.IsNullOrEmpty(request.FriendId))
        {
            _logger.LogWarning("FriendRequest called with null request or empty FriendId");
            return;
        }

        _logger.LogInformation("Signaling friend_request_received to {FriendId} from {UserId}", request.FriendId, _userContext.UserId);
        await SendToUserAsync(request.FriendId, "friend_request_received", new { userId = _userContext.UserId });
    }

    [HubMethodName("friend_accepted")]
    public async Task FriendAccepted(FriendSignalRequest request)
    {
        if (request == null || string.IsNullOrEmpty(request.FriendId)) return;

        _logger.LogInformation("Signaling friend_request_accepted to {FriendId} from {UserId}", request.FriendId, _userContext.UserId);
        await SendToUserAsync(request.FriendId, "friend_request_accepted", new { userId = _userContext.UserId });
    }

    [HubMethodName("friend_removed")]
    public async Task FriendRemoved(FriendSignalRequest request)
    {
        if (request == null || string.IsNullOrEmpty(request.FriendId)) return;

        _logger.LogInformation("Signaling friend_removed_notify to {FriendId} from {UserId}", request.FriendId, _userContext.UserId);
        await SendToUserAsync(request.FriendId, "friend_removed_notify", new { userId = _userContext.UserId });
    }

    // ────────────────────────────────────────────────────────────────
    // WebRTC signaling
    // ────────────────────────────────────────────────────────────────

    private async Task SendToUserAsync(string targetUserId, string method, object payload)
    {
        if (string.IsNullOrEmpty(targetUserId))
        {
            _logger.LogWarning("SendToUserAsync called with null or empty targetUserId");
            return;
        }

        if (_userConnections.TryGetValue(targetUserId, out var connectionIds))
        {
            string[] ids;
            lock (connectionIds) { ids = connectionIds.ToArray(); }
            _logger.LogDebug("Sending {Method} to user {TargetUserId} ({ConnectionCount} connections)", method, targetUserId, ids.Length);
            foreach (var connId in ids)
            {
                await Clients.Client(connId).SendAsync(method, payload);
            }
        }
        else
        {
            _logger.LogDebug("User {TargetUserId} not online, skipping {Method} signal", targetUserId, method);
        }
    }

    [HubMethodName("call_offer")]
    public async Task CallOffer(CallOfferRequest request)
    {
        // Fetch fresh user info from repository instead of relying on potentially stale JWT claims
        var user = await _userRepository.GetByIdAsync(_userContext.UserId);

        var displayName = user?.DisplayName ?? Context.User?.FindFirstValue("name") ?? Context.User?.FindFirstValue(ClaimTypes.Name) ?? "User";
        var avatar = user?.Avatar ?? Context.User?.FindFirstValue("avatar");
        var username = user?.Username ?? Context.User?.FindFirstValue("unique_name") ?? Context.User?.Identity?.Name;

        await SendToUserAsync(request.TargetUserId, "call_incoming", new
        {
            from = _userContext.UserId.ToString(),
            offer = request.Offer,
            callType = request.CallType,
            chatId = request.ChatId,
            callerInfo = new
            {
                id = _userContext.UserId.ToString(),
                displayName = displayName,
                avatar = avatar,
                username = username
            }
        });

        // Track session for history
        Guid? chatId = null;
        if (Guid.TryParse(request.ChatId, out var parsedChatId)) chatId = parsedChatId;

        if (!chatId.HasValue)
        {
            var userChats = await _chatRepository.GetUserChatsAsync(_userContext.UserId, Context.ConnectionAborted);
            if (Guid.TryParse(request.TargetUserId, out var targetId))
            {
                var personalChat = userChats.FirstOrDefault(c => c.Type == ChatType.Personal && c.Members.Any(m => m.UserId == targetId));
                if (personalChat != null) chatId = personalChat.Id;
            }
        }

        var session = new CallSession(chatId, _userContext.UserId, Guid.Parse(request.TargetUserId), request.CallType, DateTime.UtcNow);
        _activeSessionsByUser[_userContext.UserId.ToString()] = session;
        _activeSessionsByUser[request.TargetUserId] = session;
    }

    [HubMethodName("call_answer")]
    public async Task CallAnswer(CallAnswerRequest request)
    {
        if (_activeSessionsByUser.TryGetValue(_userContext.UserId.ToString(), out var session))
        {
            session.IsAnswered = true;
            session.AnswerTime = DateTime.UtcNow;
        }

        await SendToUserAsync(request.TargetUserId, "call_answered", new
        {
            from = _userContext.UserId.ToString(),
            answer = request.Answer,
        });
    }

    [HubMethodName("call_decline")]
    public async Task CallDecline(TargetUserRequest request)
    {
        var currentUserIdStr = _userContext.UserId.ToString();
        if (_activeSessionsByUser.TryRemove(currentUserIdStr, out var session))
        {
            _activeSessionsByUser.TryRemove(request.TargetUserId, out _);
            if (session.ChatId.HasValue)
            {
                // If declined by recipient, it's a "declined" call
                // If current user is recipient (not the one who started), status is declined
                string status = _userContext.UserId == session.FromUserId ? "cancelled" : "declined";
                await CreateCallMessage(session.ChatId.Value, session.FromUserId, session.CallType, status, 0);
            }
        }

        await SendToUserAsync(request.TargetUserId, "call_declined", new
        {
            from = _userContext.UserId.ToString(),
        });
    }

    [HubMethodName("call_end")]
    public async Task CallEnd(TargetUserRequest request)
    {
        var currentUserIdStr = _userContext.UserId.ToString();
        if (_activeSessionsByUser.TryRemove(currentUserIdStr, out var session))
        {
            _activeSessionsByUser.TryRemove(request.TargetUserId, out _);
            if (session.ChatId.HasValue)
            {
                int duration = session.IsAnswered && session.AnswerTime.HasValue
                    ? (int)(DateTime.UtcNow - session.AnswerTime.Value).TotalSeconds
                    : 0;

                string status = session.IsAnswered ? "completed" : (_userContext.UserId == session.FromUserId ? "cancelled" : "missed");
                await CreateCallMessage(session.ChatId.Value, session.FromUserId, session.CallType, status, duration);
            }
        }

        await SendToUserAsync(request.TargetUserId, "call_ended", new
        {
            from = _userContext.UserId.ToString(),
        });
    }

    private async Task CreateCallMessage(Guid chatId, Guid senderId, string callType, string status, int duration)
    {
        var command = new SendMessageCommand(
            chatId,
            senderId,
            null,
            "call",
            null,
            null,
            null,
            null,
            null,
            null,
            null,
            null,
            null,
            null,
            null,
            callType,
            status,
            duration
        );

        await _sender.Send(command);
    }

    [HubMethodName("ice_candidate")]
    public async Task IceCandidate(IceCandidateRequest request)
    {
        await SendToUserAsync(request.TargetUserId, "ice_candidate", new
        {
            from = _userContext.UserId.ToString(),
            candidate = request.Candidate,
        });
    }

    [HubMethodName("renegotiate")]
    public async Task Renegotiate(RenegotiateRequest request)
    {
        await SendToUserAsync(request.TargetUserId, "renegotiate", new
        {
            from = _userContext.UserId.ToString(),
            offer = request.Offer,
        });
    }

    [HubMethodName("renegotiate_answer")]
    public async Task RenegotiateAnswer(RenegotiateAnswerRequest request)
    {
        await SendToUserAsync(request.TargetUserId, "renegotiate_answer", new
        {
            from = _userContext.UserId.ToString(),
            answer = request.Answer,
        });
    }

    [HubMethodName("call_type_changed")]
    public async Task CallTypeChanged(CallTypeChangedRequest request)
    {
        await SendToUserAsync(request.TargetUserId, "call_type_changed", new
        {
            from = _userContext.UserId.ToString(),
            callType = request.CallType,
            isScreenSharing = request.IsScreenSharing
        });
    }

    [HubMethodName("call_status")]
    public async Task CallStatus(CallStatusRequest request)
    {
        await SendToUserAsync(request.TargetUserId, "call_status_updated", new
        {
            from = _userContext.UserId.ToString(),
            isMuted = request.IsMuted,
            isVideoOff = request.IsVideoOff
        });
    }

    // ────────────────────────────────────────────────────────────────
    // Group Call signals
    // ────────────────────────────────────────────────────────────────

    [HubMethodName("group_call_join")]
    public async Task GroupCallJoin(GroupCallJoinRequest request)
    {
        var chatId = request.ChatId;
        var userId = _userContext.UserId.ToString();

        // Fetch fresh user info from repository
        var user = await _userRepository.GetByIdAsync(_userContext.UserId);

        var displayName = user?.DisplayName ?? Context.User?.FindFirstValue("name") ?? Context.User?.FindFirstValue(ClaimTypes.Name) ?? "User";
        var avatar = user?.Avatar ?? Context.User?.FindFirstValue("avatar");
        var username = user?.Username ?? Context.User?.FindFirstValue("unique_name") ?? Context.User?.Identity?.Name ?? "user";

        var userInfo = new ParticipantInfo(userId, username, displayName, avatar);

        var participants = _groupCallParticipants.GetOrAdd(chatId, _ =>
        {
            _activeGroupCalls[chatId] = (DateTime.UtcNow, request.CallType);
            return new ConcurrentDictionary<string, ParticipantInfo>();
        });
        var isFirst = participants.IsEmpty;
        participants.TryAdd(userId, userInfo);

        // Notify others
        await Clients.Group(chatId).SendAsync("group_call_user_joined", new
        {
            chatId = chatId,
            userId = userId,
            userInfo = userInfo
        });

        // Send current participants to joiner (excluding self)
        var others = participants.Values.Where(p => p.Id != userId).ToList();
        await Clients.Caller.SendAsync("group_call_participants", new
        {
            chatId = chatId,
            participants = others
        });

        // Broadcast active call participants to everyone in the chat
        await Clients.Group(chatId).SendAsync("group_call_active", new
        {
            chatId = chatId,
            participants = participants.Keys.ToList()
        });

        if (isFirst)
        {
            await Clients.Group(chatId).SendAsync("group_call_incoming", new
            {
                chatId = chatId,
                from = userId,
                callerInfo = userInfo,
                callType = request.CallType
            });
        }
    }

    [HubMethodName("group_call_leave")]
    public async Task GroupCallLeave(GroupLeaveRequest request)
    {
        var chatId = request.ChatId;
        var userId = _userContext.UserId.ToString();
        if (_groupCallParticipants.TryGetValue(chatId, out var participants))
        {
            participants.TryRemove(userId, out _);

            // Broadcast updated participants list
            await Clients.Group(chatId).SendAsync("group_call_active", new
            {
                chatId = chatId,
                participants = participants.Keys.ToList()
            });

            if (participants.IsEmpty)
            {
                _groupCallParticipants.TryRemove(chatId, out _);
                if (_activeGroupCalls.TryRemove(chatId, out var info))
                {
                    var duration = (int)(DateTime.UtcNow - info.StartTime).TotalSeconds;
                    await CreateCallMessage(Guid.Parse(chatId), _userContext.UserId, info.CallType, "completed", duration);
                }
                await Clients.Group(chatId).SendAsync("group_call_ended", new { chatId = chatId });
            }
        }

        await Clients.Group(chatId).SendAsync("group_call_user_left", new
        {
            chatId = chatId,
            userId = userId
        });
    }

    [HubMethodName("group_call_offer")]
    public async Task GroupCallOffer(GroupCallOfferRequest request)
    {
        await SendToUserAsync(request.TargetUserId, "group_call_offer", new
        {
            chatId = request.ChatId,
            from = _userContext.UserId.ToString(),
            offer = request.Offer
        });
    }

    [HubMethodName("group_call_answer")]
    public async Task GroupCallAnswer(GroupCallAnswerRequest request)
    {
        await SendToUserAsync(request.TargetUserId, "group_call_answer", new
        {
            chatId = request.ChatId,
            from = _userContext.UserId.ToString(),
            answer = request.Answer
        });
    }

    [HubMethodName("group_ice_candidate")]
    public async Task GroupIceCandidate(GroupIceCandidateRequest request)
    {
        await SendToUserAsync(request.TargetUserId, "group_ice_candidate", new
        {
            chatId = request.ChatId,
            from = _userContext.UserId.ToString(),
            candidate = request.Candidate
        });
    }

    [HubMethodName("group_call_renegotiate")]
    public async Task GroupRenegotiate(GroupRenegotiateRequest request)
    {
        await SendToUserAsync(request.TargetUserId, "group_call_renegotiate", new
        {
            chatId = request.ChatId,
            from = _userContext.UserId.ToString(),
            offer = request.Offer
        });
    }

    [HubMethodName("group_call_renegotiate_answer")]
    public async Task GroupRenegotiateAnswer(GroupRenegotiateAnswerRequest request)
    {
        await SendToUserAsync(request.TargetUserId, "group_call_renegotiate_answer", new
        {
            chatId = request.ChatId,
            from = _userContext.UserId.ToString(),
            answer = request.Answer
        });
    }

    [HubMethodName("group_call_status")]
    public async Task GroupCallStatus(GroupCallStatusRequest request)
    {
        var chatId = request.ChatId;
        var userId = _userContext.UserId.ToString();

        if (_groupCallParticipants.TryGetValue(chatId, out var participants))
        {
            if (participants.TryGetValue(userId, out var info))
            {
                // Update local state if needed (e.g. muted status)
                participants[userId] = info with
                {
                    IsMuted = request.IsMuted,
                    IsVideoOff = request.IsVideoOff
                };
            }
        }

        await Clients.Group(request.ChatId).SendAsync("group_call_status_updated", new
        {
            chatId = request.ChatId,
            userId = _userContext.UserId.ToString(),
            isMuted = request.IsMuted,
            isVideoOff = request.IsVideoOff
        });
    }

    [HubMethodName("group_call_status_params")]
    public async Task GroupCallStatusParams(string chatId, bool isMuted, bool isVideoOff)
    {
        var userId = _userContext.UserId.ToString();

        if (_groupCallParticipants.TryGetValue(chatId, out var participants))
        {
            if (participants.TryGetValue(userId, out var info))
            {
                // Update local state if needed (e.g. muted status)
                participants[userId] = info with
                {
                    IsMuted = isMuted,
                    IsVideoOff = isVideoOff
                };
            }
        }

        await Clients.Group(chatId).SendAsync("group_call_status_updated", new
        {
            chatId = chatId,
            userId = _userContext.UserId.ToString(),
            isMuted = isMuted,
            isVideoOff = isVideoOff
        });
    }

    [HubMethodName("get_group_call_status")]
    public async Task GetGroupCallStatus(GetGroupCallStatusRequest request)
    {
        var chatId = request.ChatId;
        if (_groupCallParticipants.TryGetValue(chatId, out var participants))
        {
            var others = participants.Values.ToList();
            _logger.LogInformation("Found {Count} participants for chat {ChatId}", others.Count, chatId);
            await Clients.Caller.SendAsync("group_call_active", new
            {
                chatId = chatId,
                participants = others.Select(p => p.Id).ToList()
            });
        }
        else
        {
            _logger.LogInformation("No active call for chat {ChatId}", chatId);
            await Clients.Caller.SendAsync("group_call_active", new
            {
                chatId = chatId,
                participants = new List<string>()
            });
        }
    }

    [HubMethodName("screen_share_started")]
    public async Task ScreenShareStarted(string chatId)
    {
        var userId = _userContext.UserId.ToString();
        if (_groupCallParticipants.TryGetValue(chatId, out var participants))
        {
            if (participants.TryGetValue(userId, out var info))
            {
                participants[userId] = info with { IsSharingScreen = true };
            }
        }

        await Clients.Group(chatId).SendAsync("screen_share_started", new
        {
            chatId = chatId,
            userId = userId
        });
    }

    [HubMethodName("screen_share_stopped")]
    public async Task ScreenShareStopped(string chatId)
    {
        var userId = _userContext.UserId.ToString();
        if (_groupCallParticipants.TryGetValue(chatId, out var participants))
        {
            if (participants.TryGetValue(userId, out var info))
            {
                participants[userId] = info with { IsSharingScreen = false };
            }
        }

        await Clients.Group(chatId).SendAsync("screen_share_stopped", new
        {
            chatId = chatId,
            userId = userId
        });
    }

    // ────────────────────────────────────────────────────────────────
    // Records
    // ────────────────────────────────────────────────────────────────

    public record AttachmentHubRequest(string Type, string Url, string? FileName, long? FileSize);
    public record SendMessageHubRequest(
        Guid ChatId,

        string? Content,

        string Type,

        List<AttachmentHubRequest>? Attachments = null,
        Guid? ReplyToId = null,
        string? Quote = null,
        Guid? ForwardedFromId = null,
        List<string>? PollOptions = null,
        bool? PollIsAnonymous = null,
        bool? PollAllowMultipleAnswers = null);
    public record ReadMessagesRequest(Guid ChatId, Guid LastReadMessageId, long LastReadSequenceId);
    public record CallOfferRequest(string TargetUserId, object Offer, string CallType, string? ChatId);
    public record CallAnswerRequest(string TargetUserId, object Answer);
    public record TargetUserRequest(string TargetUserId);
    public record IceCandidateRequest(string TargetUserId, object Candidate);
    public record RenegotiateRequest(string TargetUserId, object Offer);
    public record RenegotiateAnswerRequest(string TargetUserId, object Answer);
    public record CallTypeChangedRequest(string TargetUserId, string CallType, bool IsScreenSharing = false);
    public record CallStatusRequest(string TargetUserId, bool IsMuted, bool IsVideoOff);
    public record AddReactionRequest(Guid MessageId, Guid ChatId, string Emoji);
    public record RemoveReactionRequest(Guid MessageId, Guid ChatId, string Emoji);
    public record PinMessageRequest(Guid MessageId, Guid ChatId);
    public record DeleteMessagesHubRequest(Guid ChatId, List<string> MessageIds, bool DeleteForAll);
    public record GroupCallJoinRequest(string ChatId, string CallType);
    public record ParticipantInfo(string Id, string Username, string DisplayName, string? Avatar, bool IsSharingScreen = false, bool IsMuted = false, bool IsVideoOff = false);
    public record GroupLeaveRequest(string ChatId);
    public record GetGroupCallStatusRequest(string ChatId);
    public record GroupCallStatusRequest(string ChatId, bool IsMuted, bool IsVideoOff);
    public record GroupCallOfferRequest(string ChatId, string TargetUserId, object Offer);
    public record GroupCallAnswerRequest(string ChatId, string TargetUserId, object Answer);
    public record GroupIceCandidateRequest(string ChatId, string TargetUserId, object Candidate);
    public record GroupRenegotiateRequest(string ChatId, string TargetUserId, object Offer);
    public record GroupRenegotiateAnswerRequest(string ChatId, string TargetUserId, object Answer);
    public record FriendSignalRequest(string FriendId);
    public record VotePollRequest(Guid MessageId, Guid ChatId, Guid OptionId);
    public record EditMessageHubRequest(Guid MessageId, Guid ChatId, string Content);

    public class CallSession
    {
        public Guid? ChatId { get; }
        public Guid FromUserId { get; }
        public Guid ToUserId { get; }
        public string CallType { get; }
        public DateTime StartTime { get; }
        public bool IsAnswered { get; set; }
        public DateTime? AnswerTime { get; set; }

        public CallSession(Guid? chatId, Guid fromUserId, Guid toUserId, string callType, DateTime startTime)
        {
            ChatId = chatId;
            FromUserId = fromUserId;
            ToUserId = toUserId;
            CallType = callType;
            StartTime = startTime;
        }
    }
}

