using System; using System.Linq; using System.Threading; using System.Threading.Tasks; using FrameworkBLL.EIPConversation.EIPHandlers.EIPConversationBLL; using FrameworkDAL.DTO.EIPConversation; using GB5Shared.DTO.Framework.Login; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; namespace FrameworkSL.Hubs.EIPChat { // ============================================================ // EIPChatHub — bidirectional SignalR hub for the AppChat channel. // // Architecture: // • Client calls SendMessage(...) → hub runs full EIP pipeline // via IEIPConversationBLL → response pushed back to the // user's group via ReceiveBotMessage. // • AppChatChannelHandler (BLL layer) is a no-op so the response // engine does not fail; actual delivery is handled here. // • Typing indicator is pushed before and after processing so // the UI can show "bot is typing…". // // Group convention: // "appchat:user:{userId}:client:{clientId}" — identity-scoped; // survives reconnects and spans multiple browser tabs. // // LoginDTO: // ALWAYS reconstructed from Context.GetHttpContext() HTTP headers. // NEVER accepted from client payload. // // Registration (Program.cs): // app.MapHub("/hubs/eip-chat"); // ============================================================ public class EIPChatHub : Hub { private readonly IEIPConversationBLL _bll; private readonly ILogger _logger; public EIPChatHub(IEIPConversationBLL bll, ILogger logger) { _bll = bll ?? throw new ArgumentNullException(nameof(bll)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } // ── Group naming ────────────────────────────────────────────────────────── public static string UserGroup(int userId, int clientId) => $"appchat:user:{userId}:client:{clientId}"; // ── Connection lifecycle ────────────────────────────────────────────────── public override async Task OnConnectedAsync() { var login = GetLoginDTO(); var group = UserGroup(login.UserId, login.ClientId); await Groups.AddToGroupAsync(Context.ConnectionId, group); _logger.LogInformation( "EIPChatHub connected: {ConnectionId} user {UserId} tenant {ClientId}", Context.ConnectionId, login.UserId, login.ClientId); await base.OnConnectedAsync(); } public override async Task OnDisconnectedAsync(Exception? exception) { var login = GetLoginDTO(); var group = UserGroup(login.UserId, login.ClientId); await Groups.RemoveFromGroupAsync(Context.ConnectionId, group); if (exception is not null) _logger.LogWarning(exception, "EIPChatHub disconnected with error: {ConnectionId}", Context.ConnectionId); await base.OnDisconnectedAsync(exception); } // ── Hub methods ─────────────────────────────────────────────────────────── /// /// Processes a user message through the full EIP conversation pipeline and pushes /// the bot response back to the user's group via ReceiveBotMessage. /// public async Task SendMessage(string message, string? flowCode) { var ct = Context.ConnectionAborted; var login = GetLoginDTO(); var group = UserGroup(login.UserId, login.ClientId); try { await Clients.Group(group).ReceiveTypingIndicator(true); var payload = new EIPConversationDTO { Message = message, Platform = "APPCHAT", Channel = "APPCHAT", ChannelType = EIPChannelType.AppChat, UserIdentifier = group, TenantId = login.ClientId, FlowCode = flowCode ?? string.Empty, ConversationId = 0 }; var result = await _bll.HandleIncomingConversationAsync(payload, login, ct) .ConfigureAwait(false); await Clients.Group(group).ReceiveTypingIndicator(false); if (!result.IsSuccess) { await Clients.Group(group).ReceiveError( result.ErrorMessage ?? "An error occurred processing your request."); return; } await Clients.Group(group).ReceiveBotMessage(new EIPChatMessageDTO { Message = result.ResponseMessage, CurrentStepCode = result.CurrentStepCode, NextStepCode = result.NextStepCode, IsCompleted = result.IsCompleted, StructuredData = result.StructuredData, QrCodeData = result.QrCodeData, InputHint = result.InputHint, RatingMax = result.RatingMax, RequestLocation = result.RequestLocation, Buttons = result.Buttons?.Count > 0 ? result.Buttons : null }); } catch (OperationCanceledException) { _logger.LogInformation("EIPChatHub.SendMessage cancelled: {ConnectionId}", Context.ConnectionId); } catch (Exception ex) { _logger.LogError(ex, "EIPChatHub.SendMessage failed for user {UserId} tenant {ClientId}", login.UserId, login.ClientId); try { await Clients.Group(group).ReceiveTypingIndicator(false); await Clients.Group(group).ReceiveError("An error occurred. Please try again."); } catch { // Best-effort cleanup — connection may be gone } } } // ── Helper ──────────────────────────────────────────────────────────────── private LoginDTO GetLoginDTO() { var http = Context.GetHttpContext() ?? throw new InvalidOperationException("Hub invoked outside HTTP context."); var raw = http.Request.Headers["Login"].FirstOrDefault() ?? throw new InvalidOperationException("Login header is missing."); return System.Text.Json.JsonSerializer.Deserialize(raw) ?? throw new InvalidOperationException("Login header contains invalid JSON."); } } }