using Ikar.Server.Data;
using Ikar.Server.Data.Entities;
using Ikar.Server.Infrastructure.Auth;
using Ikar.Server.Infrastructure.Calls;
using Ikar.Server.Infrastructure.Push;
using Ikar.Shared;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;

namespace Ikar.Server.Infrastructure.Hubs;

[Authorize]
public sealed class MessengerHub(
    PresenceTracker presenceTracker,
    IkarDbContext dbContext,
    CallSessionStore callSessionStore,
    IPushNotificationSender pushNotificationSender,
    IOptions<WebRtcOptions> webRtcOptions) : Hub
{
    public override async Task OnConnectedAsync()
    {
        var userId = Context.User!.GetRequiredUserId();
        await Groups.AddToGroupAsync(Context.ConnectionId, GetUserGroup(userId));
        presenceTracker.MarkOnline(userId);
        await CaptureClientVersionAsync();
        await base.OnConnectedAsync();
    }

    public override async Task OnDisconnectedAsync(Exception? exception)
    {
        if (Context.User?.Identity?.IsAuthenticated == true)
        {
            var userId = Context.User.GetRequiredUserId();
            presenceTracker.MarkOffline(userId);
            if (callSessionStore.TryEndByParticipant(userId, "disconnected", out var endedSession))
            {
                await NotifyCallEndedAsync(endedSession!);
            }
        }

        await base.OnDisconnectedAsync(exception);
    }

    public Task<OutgoingCallDto> StartCall(Guid chatId) =>
        StartMediaCall(chatId, CallMediaType.Audio);

    public async Task SendTyping(Guid chatId, bool isTyping)
    {
        var userId = Context.User!.GetRequiredUserId();
        var chat = await dbContext.Chats
            .Include(x => x.Members)
                .ThenInclude(x => x.User)
            .AsNoTracking()
            .SingleOrDefaultAsync(
                x => x.Id == chatId && x.Members.Any(member => member.UserId == userId),
                Context.ConnectionAborted);

        if (chat is null)
        {
            throw new HubException("Chat not found.");
        }

        if (chat.Type == ChatType.Channel &&
            !DtoMapper.BuildPermissions(chat.Members.FirstOrDefault(member => member.UserId == userId), chat.Type).CanPostMessages)
        {
            throw new HubException("Read-only channel.");
        }

        var sender = chat.Members.Single(member => member.UserId == userId).User;
        var payload = new TypingIndicatorDto(chatId, sender.ToDto(presenceTracker), isTyping, DateTimeOffset.UtcNow);
        var tasks = chat.Members
            .Where(member => member.UserId != userId)
            .Select(member => Clients.User(member.UserId.ToString())
                .SendAsync("TypingChanged", payload, Context.ConnectionAborted));

        await Task.WhenAll(tasks);
    }

    public async Task<OutgoingCallDto> StartMediaCall(Guid chatId, CallMediaType mediaType)
    {
        var callerId = Context.User!.GetRequiredUserId();
        var chat = await LoadDirectChatAsync(chatId, callerId);
        var caller = chat.Members.Single(member => member.UserId == callerId).User;
        var callee = chat.Members.Single(member => member.UserId != callerId).User;
        var calleeDevices = await LoadPushDevicesAsync(callee.Id);
        var hasRealtimePresence = presenceTracker.IsOnline(callee.Id);
        var hasPushDevices = calleeDevices.Count > 0;

        if (!hasRealtimePresence && !hasPushDevices)
        {
            throw new HubException("Собеседник сейчас недоступен для звонка.");
        }

        if (!callSessionStore.TryCreate(chatId, callerId, callee.Id, mediaType, out var session, out var error))
        {
            throw new HubException(error);
        }

        var callerDto = caller.ToDto(presenceTracker);
        var calleeDto = callee.ToDto(presenceTracker);
        var resolvedSession = session!;
        var incomingCall = new IncomingCallDto(resolvedSession.CallId, chatId, callerDto, mediaType);

        if (hasRealtimePresence)
        {
            await Clients.Group(GetUserGroup(callee.Id))
                .SendAsync("IncomingCall", incomingCall);
        }

        if (hasPushDevices)
        {
            await pushNotificationSender.SendIncomingCallAsync(incomingCall, calleeDevices, Context.ConnectionAborted);
        }

        return new OutgoingCallDto(resolvedSession.CallId, chatId, calleeDto, mediaType);
    }

    public async Task AcceptCall(Guid callId)
    {
        var userId = Context.User!.GetRequiredUserId();
        if (!callSessionStore.TryAccept(callId, userId, out var session, out var error))
        {
            throw new HubException(error);
        }

        var resolvedSession = session!;
        var connectedAt = resolvedSession.ConnectedAt ?? DateTimeOffset.UtcNow;
        await NotifyCallConnectedAsync(resolvedSession with { ConnectedAt = connectedAt });
    }

    public async Task DeclineCall(Guid callId)
    {
        var userId = Context.User!.GetRequiredUserId();
        if (!callSessionStore.TryDecline(callId, userId, out var session, out var error))
        {
            throw new HubException(error);
        }

        await NotifyCallEndedAsync(session!);
    }

    public async Task EndCall(Guid callId)
    {
        var userId = Context.User!.GetRequiredUserId();
        if (!callSessionStore.TryEnd(callId, userId, "ended", out var session, out var error))
        {
            throw new HubException(error);
        }

        await NotifyCallEndedAsync(session!);
    }

    public async Task SendCallOffer(CallOfferDto payload)
    {
        if (string.IsNullOrWhiteSpace(payload.Sdp) || string.IsNullOrWhiteSpace(payload.Type))
        {
            throw new HubException("Пустое WebRTC offer.");
        }

        var recipientUserId = ResolveRecipientForConnectedCall(payload.CallId);
        await Clients.Group(GetUserGroup(recipientUserId))
            .SendAsync("CallOffer", payload);
    }

    public async Task SendCallAnswer(CallAnswerDto payload)
    {
        if (string.IsNullOrWhiteSpace(payload.Sdp) || string.IsNullOrWhiteSpace(payload.Type))
        {
            throw new HubException("Пустой WebRTC answer.");
        }

        var recipientUserId = ResolveRecipientForConnectedCall(payload.CallId);
        await Clients.Group(GetUserGroup(recipientUserId))
            .SendAsync("CallAnswer", payload);
    }

    public async Task SendCallIceCandidate(CallIceCandidateDto payload)
    {
        if (string.IsNullOrWhiteSpace(payload.Candidate))
        {
            throw new HubException("Пустой ICE candidate.");
        }

        var recipientUserId = ResolveRecipientForConnectedCall(payload.CallId);
        await Clients.Group(GetUserGroup(recipientUserId))
            .SendAsync("CallIceCandidate", payload);
    }

    private Guid ResolveRecipientForConnectedCall(Guid callId)
    {
        var userId = Context.User!.GetRequiredUserId();
        if (!callSessionStore.TryResolveRecipient(callId, userId, out var recipientUserId, out _, out var error))
        {
            throw new HubException(error);
        }

        return recipientUserId;
    }

    private async Task<Chat> LoadDirectChatAsync(Guid chatId, Guid userId)
    {
        var chat = await dbContext.Chats
            .Include(x => x.Members)
            .ThenInclude(x => x.User)
            .SingleOrDefaultAsync(x => x.Id == chatId);

        if (chat is null)
        {
            throw new HubException("Чат не найден.");
        }

        if (chat.Type != ChatType.Direct)
        {
            throw new HubException("Звонки доступны только в личных чатах.");
        }

        if (!chat.Members.Any(member => member.UserId == userId))
        {
            throw new HubException("Нет доступа к этому чату.");
        }

        if (chat.Members.Count != 2)
        {
            throw new HubException("Для звонка требуется один собеседник.");
        }

        if (chat.Members.Any(member => member.User.IsBot))
        {
            throw new HubException("Звонки недоступны в чатах с ботами.");
        }

        return chat;
    }

    private Task NotifyCallConnectedAsync(ActiveCallSession session)
    {
        var iceServers = webRtcOptions.Value.ToDto();
        var callerPayload = new CallConnectedDto(
            session.CallId,
            session.ChatId,
            session.ConnectedAt ?? DateTimeOffset.UtcNow,
            session.MediaType,
            iceServers,
            ShouldCreateOffer: true);
        var calleePayload = new CallConnectedDto(
            session.CallId,
            session.ChatId,
            session.ConnectedAt ?? DateTimeOffset.UtcNow,
            session.MediaType,
            iceServers,
            ShouldCreateOffer: false);

        return Task.WhenAll(
            Clients.Group(GetUserGroup(session.CallerId)).SendAsync("CallConnected", callerPayload),
            Clients.Group(GetUserGroup(session.CalleeId)).SendAsync("CallConnected", calleePayload));
    }

    private async Task NotifyCallEndedAsync(ActiveCallSession session)
    {
        var payload = new CallEndedDto(session.CallId, session.ChatId, session.EndReason, session.MediaType);
        await Task.WhenAll(
            Clients.Group(GetUserGroup(session.CallerId)).SendAsync("CallEnded", payload),
            Clients.Group(GetUserGroup(session.CalleeId)).SendAsync("CallEnded", payload));

        await PersistCallHistoryMessageAsync(session);

        var calleeDevices = await LoadPushDevicesAsync(session.CalleeId);
        if (calleeDevices.Count > 0)
        {
            await pushNotificationSender.SendCallEndedAsync(payload, calleeDevices, Context.ConnectionAborted);
        }
    }

    private async Task PersistCallHistoryMessageAsync(ActiveCallSession session)
    {
        var chat = await dbContext.Chats
            .Include(x => x.Members)
            .SingleOrDefaultAsync(x => x.Id == session.ChatId, Context.ConnectionAborted);

        if (chat is null || chat.Members.Count == 0)
        {
            return;
        }

        var sender = await dbContext.Users
            .SingleOrDefaultAsync(x => x.Id == session.CallerId, Context.ConnectionAborted);

        if (sender is null)
        {
            return;
        }

        var sentAt = DateTimeOffset.UtcNow;
        var message = new Message
        {
            ChatId = chat.Id,
            Chat = chat,
            SenderId = sender.Id,
            Sender = sender,
            Text = BuildCallHistoryText(session),
            SentAt = sentAt
        };

        chat.LastActivityAt = sentAt;
        dbContext.Messages.Add(message);
        await dbContext.SaveChangesAsync(Context.ConnectionAborted);
        await NotifyMessageCreatedAsync(chat, message);
    }

    private Task NotifyMessageCreatedAsync(Chat chat, Message message)
    {
        var tasks = chat.Members.Select(async member =>
        {
            var dto = message.ToDto(presenceTracker, member.UserId);
            await Clients.User(member.UserId.ToString())
                .SendAsync("MessageCreated", dto, Context.ConnectionAborted);
        });

        return Task.WhenAll(tasks);
    }

    private static string BuildCallHistoryText(ActiveCallSession session)
    {
        var callType = session.MediaType == CallMediaType.Video ? "Видеозвонок" : "Аудиозвонок";
        if (session.ConnectedAt is not null)
        {
            return $"{callType} завершён";
        }

        return session.EndReason switch
        {
            "declined" => "Вызов отклонён",
            _ => $"{callType} без ответа"
        };
    }

    private Task<List<PushDevice>> LoadPushDevicesAsync(Guid userId) =>
        dbContext.PushDevices
            .Where(device => device.UserId == userId && device.NotificationsEnabled)
            .ToListAsync(Context.ConnectionAborted);

    private async Task CaptureClientVersionAsync()
    {
        var httpContext = Context.GetHttpContext();
        if (httpContext is null)
        {
            return;
        }

        var sessionId = Context.User!.GetRequiredSessionId();
        var session = await dbContext.UserSessions
            .SingleOrDefaultAsync(x => x.Id == sessionId, Context.ConnectionAborted);
        if (session is null)
        {
            return;
        }

        if (ClientVersionRequestReader.Apply(session, ClientVersionRequestReader.Read(httpContext.Request)))
        {
            await dbContext.SaveChangesAsync(Context.ConnectionAborted);
        }
    }

    private static string GetUserGroup(Guid userId) => $"user:{userId:D}";
}
