using Microsoft.AspNetCore.SignalR;

namespace Extrudex.API.Hubs;

/// <summary>
/// SignalR hub for real-time printer status updates.
/// 
/// Clients connect to this hub to receive push notifications when
/// a printer's status changes (e.g., Idle → Printing, Offline → Idle)
/// or when a heartbeat confirms the printer is still reachable.
/// 
/// <para>Usage flow:</para>
/// <list type="number">
///   <item>Client connects to /hubs/printer</item>
///   <item>Client calls <see cref="JoinPrinterGroup"/> with a printer ID</item>
///   <item>Server adds the connection to a SignalR group named after the printer ID</item>
///   <item>When the backend detects a status change, it calls
///         <see cref="PrinterHubExtensions.PushPrinterStatusAsync"/>
///         which broadcasts to all subscribers of that printer</item>
/// </list>
/// 
/// <para>Group naming: <c>printer:{printerId}</c> (lowercase GUID).</para>
/// 
/// <para>Typed client: <see cref="IPrinterClient"/> — all server-to-client
/// calls go through this interface for compile-time safety.</para>
/// </summary>
public class PrinterHub : Hub<IPrinterClient>
{
    /// <summary>
    /// Adds the calling connection to the SignalR group for a specific printer.
    /// Once joined, the client will receive all status updates and heartbeats
    /// for that printer until it disconnects or calls <see cref="LeavePrinterGroup"/>.
    /// </summary>
    /// <param name="printerId">
    /// The unique identifier of the printer to subscribe to.
    /// The GUID is normalized to lowercase for consistent group naming.
    /// </param>
    /// <exception cref="HubException">
    /// Thrown if <paramref name="printerId"/> cannot be parsed as a valid GUID.
    /// </exception>
    public async Task JoinPrinterGroup(Guid printerId)
    {
        var groupName = PrinterGroupName(printerId);
        await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
    }

    /// <summary>
    /// Removes the calling connection from the SignalR group for a specific printer.
    /// After leaving, the client will no longer receive updates for that printer.
    /// </summary>
    /// <param name="printerId">
    /// The unique identifier of the printer to unsubscribe from.
    /// </param>
    public async Task LeavePrinterGroup(Guid printerId)
    {
        var groupName = PrinterGroupName(printerId);
        await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
    }

    /// <summary>
    /// Overrides <see cref="Hub.OnDisconnectedAsync"/> to perform cleanup.
    /// SignalR automatically removes disconnected connections from all groups,
    /// so no manual cleanup is required here.
    /// </summary>
    /// <param name="exception">Exception that caused the disconnection, if any.</param>
    public override Task OnDisconnectedAsync(Exception? exception)
    {
        // SignalR automatically removes the connection from all groups on disconnect.
        // No manual cleanup needed.
        return base.OnDisconnectedAsync(exception);
    }

    /// <summary>
    /// Returns the SignalR group name for a given printer ID.
    /// Format: <c>printer:{printerId}</c> (lowercase to avoid case-sensitivity issues).
    /// </summary>
    /// <param name="printerId">The unique identifier of the printer.</param>
    /// <returns>A consistent, lowercase group name string.</returns>
    internal static string PrinterGroupName(Guid printerId) =>
        $"printer:{printerId.ToString().ToLowerInvariant()}";
}

/// <summary>
/// Extension methods for pushing real-time printer updates through
/// the <see cref="IHubContext{T}"/> of <see cref="PrinterHub"/>.
/// 
/// These methods are intended to be called from background services
/// (e.g., MQTT message handlers, Moonraker pollers) or other
/// server-side code that detects a printer state change.
/// </summary>
public static class PrinterHubExtensions
{
    /// <summary>
    /// Pushes a printer status change to all clients subscribed to
    /// the given printer's SignalR group.
    /// 
    /// Call this from any background service or controller when a printer's
    /// operational status changes (e.g., a Bambu Lab MQTT message reports
    /// the printer started printing, or a Moonraker poller detects an error).
    /// </summary>
    /// <param name="hubContext">The hub context injected via DI.</param>
    /// <param name="printerId">The unique identifier of the printer that changed.</param>
    /// <param name="status">The new status string (e.g., "Idle", "Printing", "Offline").</param>
    /// <param name="lastSeenAt">Timestamp (UTC) of when the status was observed, or null if unknown.</param>
    /// <returns>A Task that completes when the message has been sent to all group members.</returns>
    public static async Task PushPrinterStatusAsync(
        this IHubContext<PrinterHub, IPrinterClient> hubContext,
        Guid printerId,
        string status,
        DateTime? lastSeenAt = null)
    {
        var groupName = PrinterHub.PrinterGroupName(printerId);
        await hubContext.Clients.Group(groupName)
            .PrinterStatusChanged(printerId, status, lastSeenAt);
    }

    /// <summary>
    /// Pushes a heartbeat signal to all clients subscribed to the given
    /// printer's SignalR group. Use this for lightweight "still alive"
    /// notifications that don't require a full status payload.
    /// </summary>
    /// <param name="hubContext">The hub context injected via DI.</param>
    /// <param name="printerId">The unique identifier of the printer.</param>
    /// <param name="isActive">Whether the printer is currently active and accepting jobs.</param>
    /// <param name="lastSeenAt">Timestamp (UTC) of the last telemetry from the printer, or null.</param>
    /// <returns>A Task that completes when the message has been sent to all group members.</returns>
    public static async Task PushPrinterHeartbeatAsync(
        this IHubContext<PrinterHub, IPrinterClient> hubContext,
        Guid printerId,
        bool isActive,
        DateTime? lastSeenAt = null)
    {
        var groupName = PrinterHub.PrinterGroupName(printerId);
        await hubContext.Clients.Group(groupName)
            .PrinterHeartbeat(printerId, isActive, lastSeenAt);
    }
}