using ControlCenter.Hubs;
using ControlCenter.Models;
using Microsoft.AspNetCore.SignalR;

namespace ControlCenter.Services;

/// <summary>
/// Service that maps Linear agents to minion sprites and positions
/// in the Control Center dashboard.
///
/// <para>Static mappings define where each minion appears:</para>
/// <list type="bullet">
///   <item>Dev side: Rex (0), Dex (1), Hex (2)</item>
///   <item>Business side: Larry (0), Mel (1), Buzz (2)</item>
/// </list>
///
/// <para>Dynamic state is derived from the agent's <see cref="AgentStatus"/>:</para>
/// <list type="bullet">
///   <item><c>Active</c> → <see cref="MinionState.Active"/></item>
///   <item><c>Idle</c> → <see cref="MinionState.Idle"/></item>
///   <item><c>Thinking</c> → <see cref="MinionState.Thinking"/></item>
///   <item><c>Error</c> → <see cref="MinionState.Error"/></item>
/// </list>
///
/// <para>State updates are published through the <see cref="AgentStatusHub"/>
/// SignalR hub so that connected clients can animate minion sprites
/// in real time.</para>
/// </summary>
public class AgentMinionMapperService
{
    private readonly ILogger<AgentMinionMapperService> _logger;
    private readonly IHubContext<AgentStatusHub, IAgentStatusClient> _hubContext;

    /// <summary>
    /// Static agent-to-minion mapping table. Defines which side and position
    /// each agent's minion occupies on the dashboard.
    /// </summary>
    private static readonly Dictionary<string, AgentMinionMapping> Mappings = new()
    {
        // ── Dev Side ──────────────────────────────────
        ["rex"] = new AgentMinionMapping("rex", MinionSide.Dev, 0, "Rex"),
        ["dex"] = new AgentMinionMapping("dex", MinionSide.Dev, 1, "Dex"),
        ["hex"] = new AgentMinionMapping("hex", MinionSide.Dev, 2, "Hex"),

        // ── Business Side ─────────────────────────────
        ["larry"] = new AgentMinionMapping("larry", MinionSide.Business, 0, "Larry"),
        ["mel"]   = new AgentMinionMapping("mel",   MinionSide.Business, 1, "Mel"),
        ["buzz"]  = new AgentMinionMapping("buzz",  MinionSide.Business, 2, "Buzz"),
    };

    /// <summary>
    /// Maps <see cref="AgentStatus"/> string values to <see cref="MinionState"/>.
    /// </summary>
    private static readonly Dictionary<string, MinionState> StatusToMinionState = new()
    {
        ["active"]   = MinionState.Active,
        ["idle"]     = MinionState.Idle,
        ["thinking"] = MinionState.Thinking,
        ["error"]    = MinionState.Error,
    };

    public AgentMinionMapperService(
        ILogger<AgentMinionMapperService> logger,
        IHubContext<AgentStatusHub, IAgentStatusClient> hubContext)
    {
        _logger = logger;
        _hubContext = hubContext;
    }

    /// <summary>
    /// Gets the minion mapping for a given agent ID.
    /// Returns null if the agent is not mapped to a minion position.
    /// </summary>
    /// <param name="agentId">The agent identifier, e.g. "rex", "dex".</param>
    /// <returns>The mapping record, or null if unmapped.</returns>
    public AgentMinionMapping? GetMapping(string agentId)
    {
        return Mappings.GetValueOrDefault(agentId?.ToLowerInvariant() ?? string.Empty);
    }

    /// <summary>
    /// Gets all minion mappings, ordered by side then position index.
    /// </summary>
    /// <returns>All mappings, sorted for consistent display order.</returns>
    public IReadOnlyList<AgentMinionMapping> GetAllMappings()
    {
        return Mappings.Values
            .OrderBy(m => m.Side)
            .ThenBy(m => m.PositionIndex)
            .ToList();
    }

    /// <summary>
    /// Converts an agent status string to a <see cref="MinionState"/>.
    /// Falls back to <see cref="MinionState.Idle"/> for unrecognized statuses.
    /// </summary>
    /// <param name="status">Agent status string: "active", "idle", "thinking", or "error".</param>
    /// <returns>The corresponding minion state.</returns>
    public MinionState StatusToState(string status)
    {
        return StatusToMinionState.GetValueOrDefault(
            status?.ToLowerInvariant() ?? string.Empty,
            MinionState.Idle);
    }

    /// <summary>
    /// Publishes a minion state update through SignalR when an agent's
    /// status changes. Only publishes for agents that have a minion mapping.
    ///
    /// <para>This is the primary integration point: the
    /// <see cref="GatewayEventBridgeService"/> calls this method
    /// whenever it detects a status change from the OpenClaw Gateway.</para>
    /// </summary>
    /// <param name="agentId">The agent whose status changed, e.g. "dex".</param>
    /// <param name="status">The new status string: "active", "idle", "thinking", or "error".</param>
    /// <returns>A task that completes when the SignalR message has been sent.</returns>
    public async Task PublishMinionStateUpdateAsync(string agentId, string status)
    {
        var mapping = GetMapping(agentId);
        if (mapping is null)
        {
            _logger.LogDebug("No minion mapping for agent {AgentId}; skipping state update", agentId);
            return;
        }

        var minionState = StatusToState(status);
        var update = new MinionStateUpdate(
            AgentId: mapping.AgentId,
            DisplayName: mapping.DisplayName,
            Side: mapping.Side,
            PositionIndex: mapping.PositionIndex,
            State: minionState,
            Timestamp: DateTime.UtcNow.ToString("o")
        );

        // Broadcast to the fleet group (all subscribers)
        await _hubContext.Clients.Group(AgentStatusHub.FleetGroupName)
            .AgentStatusChanged(ToAgentStatusUpdate(agentId, status));

        // Also push to the specific agent's group
        var agentGroup = AgentStatusHub.AgentGroupName(agentId);
        await _hubContext.Clients.Group(agentGroup)
            .AgentStatusChanged(ToAgentStatusUpdate(agentId, status));

        _logger.LogInformation(
            "Minion state update: {AgentId} → {State} (Side: {Side}, Position: {Index})",
            agentId, minionState, mapping.Side, mapping.PositionIndex);
    }

    /// <summary>
    /// Gets the current minion state for all mapped agents, suitable
    /// for building an initial fleet snapshot.
    /// </summary>
    /// <returns>All minion mappings with their current (idle) state.</returns>
    public IReadOnlyList<MinionStateUpdate> GetFullMinionState()
    {
        return Mappings.Values
            .OrderBy(m => m.Side)
            .ThenBy(m => m.PositionIndex)
            .Select(m => new MinionStateUpdate(
                AgentId: m.AgentId,
                DisplayName: m.DisplayName,
                Side: m.Side,
                PositionIndex: m.PositionIndex,
                State: MinionState.Idle,
                Timestamp: DateTime.UtcNow.ToString("o")))
            .ToList();
    }

    /// <summary>
    /// Converts a status string to an <see cref="AgentStatusUpdate"/>
    /// for SignalR push. Uses the mapping table for display names and roles.
    /// </summary>
    private AgentStatusUpdate ToAgentStatusUpdate(string agentId, string status)
    {
        var mapping = GetMapping(agentId);
        var displayName = mapping?.DisplayName ?? char.ToUpperInvariant(agentId[0]) + agentId[1..];

        return new AgentStatusUpdate(
            AgentId: agentId,
            DisplayName: displayName,
            Role: mapping is not null
                ? $"{mapping.Side} Agent"
                : "Agent",
            Status: status,
            CurrentTask: null,
            SessionKey: string.Empty,
            Channel: string.Empty,
            LastActivity: DateTime.UtcNow.ToString("o"),
            ErrorMessage: status == "error" ? "Agent encountered an error" : null
        );
    }
}