using Microsoft.AspNetCore.SignalR;
using Morgana.AI.Interfaces;
using Morgana.Web.Hubs;
using static Morgana.AI.Records;

namespace Morgana.Web.Services;

/// <summary>
/// SignalR-backed implementation of <see cref="IChannelService"/>. This is the reference
/// channel powering the Cauldron web UI and advertises the full expressive capability
/// set (rich cards, quick replies, streaming, markdown).
/// </summary>
/// <remarks>
/// <para><strong>Architecture Role:</strong></para>
/// <para>This service is injected into actors (particularly ConversationManagerActor) to enable them to send
/// messages to clients without direct coupling to SignalR infrastructure.</para>
/// <para><strong>Message Protocol:</strong></para>
/// <para>All messages sent via SignalR use the "ReceiveMessage" event with a structured JSON payload containing:
/// conversationId, text, timestamp, messageType, quickReplies, errorReason, agentName, agentCompleted</para>
/// <para><strong>Group-based Routing:</strong></para>
/// <para>Messages are sent to SignalR groups named by conversation ID. All clients in a group receive the message,
/// enabling multi-user scenarios (monitoring, collaboration, customer + agent viewing the same conversation).</para>
/// </remarks>
public class SignalRChannelService : IChannelService
{
    /// <summary>
    /// SignalR hub context used to push messages to connected clients. Messages are routed
    /// to groups named after the conversation id, so every client that joined that group
    /// (via <c>MorganaHub.JoinConversation</c>) receives the payload through the
    /// "ReceiveMessage" / "ReceiveStreamChunk" events.
    /// </summary>
    private readonly IHubContext<MorganaHub> hubContext;

    /// <summary>
    /// Logger for diagnostic output. Emits informational entries on successful sends and
    /// error entries when the underlying SignalR dispatch fails.
    /// </summary>
    private readonly ILogger logger;

    /// <summary>
    /// Initializes a new instance of the SignalRChannelService.
    /// </summary>
    /// <param name="hubContext">SignalR hub context for sending messages to clients</param>
    /// <param name="logger">Logger instance for message delivery diagnostics</param>
    public SignalRChannelService(IHubContext<MorganaHub> hubContext, ILogger logger)
    {
        this.hubContext = hubContext;
        this.logger = logger;
    }

    /// <summary>
    /// Publishes a <see cref="ChannelMessage"/> to the SignalR group matching
    /// <see cref="ChannelMessage.ConversationId"/> via the "ReceiveMessage" event.
    /// The DTO is forwarded as-is and serialized by SignalR's JSON pipeline; the wire
    /// format stays compatible with Cauldron's client-side <c>SignalRMessage</c> schema.
    /// </summary>
    /// <param name="channelMessage">The fully-formed channel message to deliver.</param>
    /// <returns>Task representing the async send operation</returns>
    public async Task SendMessageAsync(ChannelMessage channelMessage)
    {
        logger.LogInformation(
            $"Sending structured message to conversation {channelMessage.ConversationId}: " +
            $"type={channelMessage.MessageType}, agent={channelMessage.AgentName}, completed={channelMessage.AgentCompleted}, " +
            $"#quickReplies={channelMessage.QuickReplies?.Count ?? 0}, hasRichCard={channelMessage.RichCard != null}");

        // The presentation is the first message of a fresh conversation and races
        // the client's JoinConversation call on the hub. If we send while the client
        // has not joined its group yet, the message is dropped by SignalR into an
        // empty audience. Sleeping 750 ms here is the pragmatic workaround inherited
        // from commit cf55ed1 ("FIX SignalR still not joined during NoAgentsMessage
        // presentation"). This delay is specific to SignalR group semantics and
        // therefore lives here rather than in the channel-agnostic supervisor.
        if (string.Equals(channelMessage.MessageType, "presentation", StringComparison.Ordinal))
            await Task.Delay(750);

        await hubContext.Clients.Group(channelMessage.ConversationId)
            .SendAsync("ReceiveMessage", channelMessage);
    }

    /// <summary>
    /// Sends a streaming chunk to a conversation group via SignalR for progressive response rendering.
    /// Optimized for high-frequency streaming with minimal overhead (no logging, fire-and-forget).
    /// </summary>
    /// <param name="conversationId">Unique identifier of the conversation (SignalR group name)</param>
    /// <param name="chunkText">Partial response text to append to the current message</param>
    /// <returns>Task representing the async send operation</returns>
    /// <remarks>
    /// <para><strong>Streaming Protocol:</strong></para>
    /// <para>Sends chunks via "ReceiveStreamChunk" SignalR event with minimal payload:</para>
    /// <code>
    /// {
    ///   "text": "chunk content here"
    /// }
    /// </code>
    /// <para><strong>No Logging:</strong></para>
    /// <para>Intentionally omits logging to avoid spamming logs with hundreds of partial text fragments.
    /// Only errors are logged if chunk delivery fails.</para>
    /// <para><strong>Client Expectations:</strong></para>
    /// <para>Clients should buffer chunks and append them to the current message being displayed.
    /// The final complete message arrives via the standard "ReceiveMessage" event with full metadata.</para>
    /// </remarks>
    public async Task SendStreamChunkAsync(string conversationId, string chunkText)
    {
        try
        {
            // Send chunk with minimal payload (just the text)
            await hubContext.Clients.Group(conversationId)
                .SendAsync("ReceiveStreamChunk", chunkText);
        }
        catch (Exception ex)
        {
            // Log errors but don't propagate - continue streaming
            logger.LogError(ex, "Failed to send stream chunk to conversation {ConversationId}", conversationId);
        }
    }
}
