using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Nodes;
using Nexus.Api.Models;

namespace Nexus.Api.Services;

public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration configuration) : IOpenClawGatewayClient
{
    private static readonly JsonSerializerOptions JsonOptions = new()
    {
        PropertyNameCaseInsensitive = true,
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase
    };

    private string? GetPassword()
    {
        var password = configuration["Integrations:OpenClaw:Password"];
        if (string.IsNullOrWhiteSpace(password))
            password = configuration["Integrations:OpenClaw:Token"];
        return string.IsNullOrWhiteSpace(password) ? null : password;
    }

    private void ApplyAuth(HttpRequestMessage request)
    {
        var password = GetPassword();
        if (password is not null)
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", password);
    }

    public async Task<JsonNode?> InvokeToolAsync(string tool, object? args = null)
    {
        try
        {
            using var request = new HttpRequestMessage(HttpMethod.Post, "/tools/invoke");
            ApplyAuth(request);

            var body = new Dictionary<string, object?> { ["tool"] = tool };
            if (args is not null)
                body["args"] = args;

            request.Content = JsonContent.Create(body);

            using var response = await httpClient.SendAsync(request);
            if (!response.IsSuccessStatusCode)
                return null;

            var json = await response.Content.ReadAsStringAsync();
            if (string.IsNullOrWhiteSpace(json))
                return null;

            var node = JsonNode.Parse(json);
            if (node?["ok"]?.GetValue<bool>() == true && node["result"] is not null)
                return node["result"];
            return node;
        }
        catch
        {
            return null;
        }
    }

    /// <summary>
    /// Reads the agent session status from the Gateway via session_status tool.
    /// Returns fields like model, provider, status, lastActivity, isActive, currentTask.
    /// Returns null if the session is unreachable.
    /// </summary>
    private async Task<JsonNode?> TryGetAgentStatusAsync(string agentId)
    {
        try
        {
            return await InvokeToolAsync("session_status", new { sessionKey = "agent:" + agentId + ":main" });
        }
        catch
        {
            return null;
        }
    }

    public async Task<List<DashboardAgentInfo>> GetAgentsAsync()
    {
        // Fallback hardcoded descriptions and tags known for each agent
        var knownInfo = new Dictionary<string, (string Name, string Description, string[] Tags)>
        {
            ["iris"] = (
                "Iris",
                "Zentrale operative Führungsinstanz. Strukturiert Aufgaben, bewertet Risiken, steuert spezialisierte Agenten und eskaliert kritische Entscheidungen.",
                new[] { "Orchestration", "Delegation", "Approval", "Risk Management" }
            ),
            ["programmer"] = (
                "Full-Stack Developer",
                "Primärer Entwicklungsagent. Implementiert Features, behebt Bugs und schreibt Code im gesamten Stack — autonom im Rahmen seines Scopes.",
                new[] { "Full-Stack", "TypeScript", "C#", "Vue", ".NET", "Builds" }
            ),
            ["reviewer"] = (
                "Code Quality Assurance",
                "Code-Qualitätskontrolle. Prüft Diffs auf Bugs, Regressionen, Sicherheitslücken und Wartbarkeit. Berichtet Findings strukturiert und knapp.",
                new[] { "Code Review", "Testing", "Security", "Quality" }
            ),
            ["architekt"] = (
                "Infrastructure Architect",
                "Verwaltet die gesamte Server-Infrastruktur. Deployt Services, konfiguriert Docker, Nginx und Firewall. Stellt sicher, dass die Produktivumgebung stabil und sicher läuft.",
                new[] { "Docker", "Nginx", "CI/CD", "Firewall", "VPS" }
            ),
            ["executor"] = (
                "Host Executor",
                "Einziger Agent mit Host-Exec-Rechten. Führt Docker- und Shell-Befehle auf dem VPS aus — ausschließlich im Auftrag von Iris. Handelt niemals eigeninitiativ.",
                new[] { "Docker", "Shell", "Host", "Deployment" }
            ),
            ["researcher"] = (
                "Research & Analysis",
                "Spezialisierter Recherche-Agent. Sucht online, prüft Quellen, analysiert Inhalte (inkl. YouTube-Videos) und übergibt strukturierte Erkenntnisse. Ausschließlich Lese- und Analyse-Rechte.",
                new[] { "Research", "Quellenprüfung", "Analyse", "Docs" }
            )
        };

        // Load agent IDs from openclaw.json config
        var agentIds = LoadAgentIdsFromConfig();

        var agents = new List<DashboardAgentInfo>();

        // Read queue once for workload calculation
        var queueItems = new List<QueueItem>();
        try { queueItems = await GetQueueAsync(); } catch { }

        foreach (var id in agentIds)
        {
            // Skip the "main" agent (it's the default assistant, not a sub-agent)
            if (string.Equals(id, "main", StringComparison.OrdinalIgnoreCase))
                continue;

            // 1. Try to get dynamic session status from Gateway
            var status = await TryGetAgentStatusAsync(id);

            // 2. Extract model from session_status (dynamic)
            var model = status?["model"]?.GetValue<string>();

            // 3. Extract activity from session_status
            var isActive = false;
            string? currentTask = null;
            if (status is not null)
            {
                // Check explicit isActive field
                var activeVal = status["isActive"];
                if (activeVal is not null && activeVal.GetValueKind() == JsonValueKind.True)
                    isActive = true;
                else if (activeVal is not null && activeVal.GetValueKind() == JsonValueKind.String)
                    isActive = string.Equals(activeVal.GetValue<string>(), "true", StringComparison.OrdinalIgnoreCase);

                // Fall back to status text
                var statusText = status["status"]?.GetValue<string>();
                if (!isActive && statusText is not null)
                    isActive = string.Equals(statusText, "active", StringComparison.OrdinalIgnoreCase)
                            || string.Equals(statusText, "running", StringComparison.OrdinalIgnoreCase);

                currentTask = status["currentTask"]?.GetValue<string>()
                           ?? status["task"]?.GetValue<string>()
                           ?? (isActive ? "Working..." : null);
            }

            // 4. Try to read workspace metadata for richer info
            var (name, description, tags) = await ReadAgentMetadataAsync(id);

            // 5. Fallback to known info if workspace metadata not available
            if (string.IsNullOrWhiteSpace(name) && knownInfo.TryGetValue(id, out var kn))
            {
                name = kn.Name;
                if (string.IsNullOrWhiteSpace(description))
                    description = kn.Description;
                if (tags.Length == 0)
                    tags = kn.Tags;
            }

            if (string.IsNullOrWhiteSpace(model))
            {
                model = id.ToLowerInvariant() switch
                {
                    "iris" or "programmer" or "executor" => "deepseek/deepseek-v4-flash",
                    "reviewer" or "architekt" or "researcher" => "deepseek/deepseek-v4-pro",
                    _ => "deepseek/deepseek-v4-flash"
                };
            }

            // 6. Read goal from workspace files
            var goal = await ReadAgentGoalAsync(id);

            // 7. Calculate progress dynamically
            var progress = CalculateAgentProgress(id, isActive, status);

            // 8. Calculate workload from queue items
            var workload = CalculateAgentWorkload(id, queueItems);

            agents.Add(new DashboardAgentInfo(
                Id: id,
                Name: string.IsNullOrWhiteSpace(name) ? DeriveRole(id) : name,
                Role: DeriveRole(id),
                Model: model,
                IsActive: isActive,
                CurrentTask: currentTask,
                Description: description,
                Tags: tags,
                Progress: progress,
                Workload: workload,
                Goal: goal,
                RoleBadge: DeriveRoleBadge(id),
                StatusLabel: DeriveStatusLabel(isActive, status),
                Elapsed: FormatElapsed(status),
                Think: null,
                Next: DeriveNext(isActive, currentTask)
            ));
        }
        return agents;
    }

    /// <summary>
    /// Loads agent IDs from the OpenClaw config file (openclaw.json).
    /// Falls back to the known list if the config file is unavailable.
    /// </summary>
    private List<string> LoadAgentIdsFromConfig()
    {
        try
        {
            var configPath = configuration.GetValue<string>("AgentConfigPath")
                ?? "/home/node/.openclaw/openclaw.json";

            if (!System.IO.File.Exists(configPath))
                return GetDefaultAgentIds();

            var json = System.IO.File.ReadAllText(configPath);
            using var doc = JsonDocument.Parse(json);
            var root = doc.RootElement;

            if (!root.TryGetProperty("agents", out var agentsEl))
                return GetDefaultAgentIds();
            if (!agentsEl.TryGetProperty("list", out var listEl))
                return GetDefaultAgentIds();

            var ids = new List<string>();
            foreach (var agentEl in listEl.EnumerateArray())
            {
                if (agentEl.TryGetProperty("id", out var idEl))
                {
                    var id = idEl.GetString();
                    if (!string.IsNullOrWhiteSpace(id))
                        ids.Add(id);
                }
            }

            return ids.Count > 0 ? ids : GetDefaultAgentIds();
        }
        catch
        {
            return GetDefaultAgentIds();
        }
    }

    private static List<string> GetDefaultAgentIds()
        => new() { "iris", "programmer", "reviewer", "architekt", "executor", "researcher" };

    /// <summary>
    /// Reads agent metadata from workspace files (IDENTITY.md, SOUL.md).
    /// Returns (Name, Description, Tags) — empty strings/arrays if unavailable.
    /// Tags are not read from files (kept as empty for dynamic agents).
    /// </summary>
    private async Task<(string Name, string Description, string[] Tags)> ReadAgentMetadataAsync(string agentId)
    {
        var name = string.Empty;
        var description = string.Empty;
        var tags = Array.Empty<string>();

        try
        {
            // Try the host-mounted workspace path (used by the API container)
            var workspacePath = "/mnt/workspace-" + agentId;
            var identityPath = Path.Combine(workspacePath, "IDENTITY.md");

            if (System.IO.File.Exists(identityPath))
            {
                var content = await System.IO.File.ReadAllTextAsync(identityPath);
                ParseIdentityContent(content, out name, out description);
            }
            else
            {
                // Fallback: try the OpenClaw workspace path inside the gateway container
                var altPath = "/home/node/.openclaw/workspace-" + agentId + "/IDENTITY.md";
                if (System.IO.File.Exists(altPath))
                {
                    var content = await System.IO.File.ReadAllTextAsync(altPath);
                    ParseIdentityContent(content, out name, out description);
                }
            }

            // If description is still empty, try SOUL.md
            if (string.IsNullOrWhiteSpace(description))
            {
                var soulPath = Path.Combine(workspacePath, "SOUL.md");
                string? soulContent = null;
                if (System.IO.File.Exists(soulPath))
                {
                    soulContent = await System.IO.File.ReadAllTextAsync(soulPath);
                }
                else
                {
                    var altSoulPath = "/home/node/.openclaw/workspace-" + agentId + "/SOUL.md";
                    if (System.IO.File.Exists(altSoulPath))
                    {
                        soulContent = await System.IO.File.ReadAllTextAsync(altSoulPath);
                    }
                }

                if (soulContent is not null)
                {
                    description = ExtractDescriptionFromSoul(soulContent);
                }
            }
        }
        catch
        {
            // Fallback to hardcoded values will handle this
        }

        return (name, description, tags);
    }

    /// <summary>
    /// Parses an IDENTITY.md file to extract Name and a short description (role/creature).
    /// Looks for markdown list items like "- **Name:** ...", "- **Rolle:** ...", etc.
    /// </summary>
    private static void ParseIdentityContent(string content, out string name, out string description)
    {
        name = string.Empty;
        description = string.Empty;

        using var reader = new StringReader(content);
        string? line;
        while ((line = reader.ReadLine()) is not null)
        {
            // Extract name from "- **Name:** ..."
            if (string.IsNullOrWhiteSpace(name))
            {
                var nameMarker = "- **Name:**";
                var idx = line.IndexOf(nameMarker, StringComparison.OrdinalIgnoreCase);
                if (idx >= 0)
                {
                    name = line[(idx + nameMarker.Length)..].Trim();
                    continue;
                }
            }

            // Extract role/theme from "- **Rolle:** ..." or "- **Role:** ..." or "- **Creature:** ..."
            if (string.IsNullOrWhiteSpace(description))
            {
                var descMarkers = new[] { "- **Rolle:**", "- **Role:**", "- **Creature:**" };
                foreach (var marker in descMarkers)
                {
                    var idx = line.IndexOf(marker, StringComparison.OrdinalIgnoreCase);
                    if (idx >= 0)
                    {
                        description = line[(idx + marker.Length)..].Trim();
                        break;
                    }
                }
            }
        }
    }

    /// <summary>
    /// Extracts a short description from SOUL.md (first heading or first paragraph after the "Rolle" section).
    /// Returns the first ~200 characters of meaningful content.
    /// </summary>
    private static string ExtractDescriptionFromSoul(string content)
    {
        // Look for "## Rolle" section and take the first paragraph after it
        using var reader = new StringReader(content);
        string? line;
        var inRoleSection = false;
        var paragraphs = new List<string>();

        while ((line = reader.ReadLine()) is not null)
        {
            var trimmed = line.Trim();

            if (trimmed.StartsWith("## ", StringComparison.OrdinalIgnoreCase))
            {
                if (inRoleSection)
                    break; // We've moved past the "Rolle" section

                if (trimmed.IndexOf("Rolle", StringComparison.OrdinalIgnoreCase) >= 0
                    || trimmed.IndexOf("Role", StringComparison.OrdinalIgnoreCase) >= 0)
                {
                    inRoleSection = true;
                }
                continue;
            }

            if (inRoleSection && !string.IsNullOrWhiteSpace(trimmed))
            {
                paragraphs.Add(trimmed);
                if (paragraphs.Count >= 3)
                    break;
            }
        }

        var summary = string.Join(" ", paragraphs);
        return summary.Length > 200 ? summary[..200] + "…" : summary;
    }

    public async Task<List<MessageEntry>> GetSessionHistoryAsync(
        string sessionKey, int limit = 50, int offset = 0)
    {
        var result = new List<MessageEntry>();
        try
        {
            var toolResult = await InvokeToolAsync("sessions_history", new
            {
                sessionKey, limit, offset,
                includeTools = false
            });
            if (toolResult is null)
                return result;

            var json = toolResult.ToJsonString();
            using var doc = JsonDocument.Parse(json);
            var root = doc.RootElement;

            if (!root.TryGetProperty("details", out var detailsEl))
                return result;
            if (!detailsEl.TryGetProperty("messages", out var messagesEl))
                return result;
            if (messagesEl.ValueKind != JsonValueKind.Array)
                return result;

            foreach (var msg in messagesEl.EnumerateArray())
            {
                if (!msg.TryGetProperty("role", out var roleEl))
                    continue;
                var role = roleEl.GetString() ?? "";
                if (role != "user" && role != "assistant")
                    continue;

                if (!msg.TryGetProperty("content", out var contentEl))
                    continue;
                if (contentEl.ValueKind != JsonValueKind.Array)
                    continue;

                var texts = new List<string>();
                foreach (var block in contentEl.EnumerateArray())
                {
                    if (!block.TryGetProperty("type", out var typeEl))
                        continue;
                    if (typeEl.GetString() != "text")
                        continue;
                    if (!block.TryGetProperty("text", out var textEl))
                        continue;
                    var text = textEl.GetString();
                    if (!string.IsNullOrWhiteSpace(text))
                        texts.Add(text);
                }

                if (texts.Count == 0)
                    continue;

                var content = string.Join(" ", texts).Trim();
                if (string.IsNullOrWhiteSpace(content))
                    continue;
                if (content == "REPLY_SKIP" || content == "ANNOUNCE_SKIP")
                    continue;

                var ts = DateTimeOffset.UtcNow.ToString("o");
                if (msg.TryGetProperty("timestamp", out var tsEl))
                    ts = tsEl.GetString() ?? ts;

                result.Add(new MessageEntry(role, content, ts));
            }
        }
        catch
        {
            // return whatever we collected (may be empty)
        }
        return result;
    }

    /// <summary>
    /// Collects assistant messages from ALL agent sessions (multi-agent operations feed).
    /// Merges, sorts by timestamp descending, and limits the result.
    /// Falls back to an empty list if any agent session is unreachable.
    /// </summary>
    public async Task<List<FeedEntry>> GetAllAgentOperationsAsync(int limit = 30)
    {
        var allEntries = new List<FeedEntry>();
        var agentIds = LoadAgentIdsFromConfig();

        foreach (var agentId in agentIds)
        {
            try
            {
                var sessionKey = $"agent:{agentId}:main";
                var messages = await GetSessionHistoryAsync(sessionKey, Math.Min(limit * 2, 50));
                foreach (var msg in messages)
                {
                    if (!string.Equals(msg.Role, "assistant", StringComparison.OrdinalIgnoreCase))
                        continue;

                    if (string.IsNullOrWhiteSpace(msg.Content))
                        continue;

                    // Parse timestamp
                    var ts = ParseTimestamp(msg.Timestamp);
                    var timeAgo = FormatTimeAgo(ts);

                    // Extract a short agent indicator and action from content
                    var (agent, action) = ExtractAgentAction(msg.Content);

                    // Determine event type based on content heuristics
                    var eventType = DetectEventType(msg.Content);

                    allEntries.Add(new FeedEntry(
                        agent,
                        action,
                        msg.Timestamp,
                        timeAgo,
                        AgentId: agentId,
                        Type: eventType
                    ));
                }
            }
            catch
            {
                // Agent session unreachable — skip; we still have data from other agents
            }
        }

        // Sort descending by timestamp, then limit
        return allEntries
            .OrderByDescending(e => ParseTimestamp(e.Timestamp))
            .Take(Math.Clamp(limit, 1, 100))
            .ToList();
    }

    private static DateTimeOffset ParseTimestamp(string timestamp)
    {
        if (DateTimeOffset.TryParse(timestamp, null, System.Globalization.DateTimeStyles.None, out var dt))
            return dt;
        return DateTimeOffset.UtcNow;
    }

    private static string FormatTimeAgo(DateTimeOffset ts)
    {
        var diff = DateTimeOffset.UtcNow - ts;
        if (diff.TotalMinutes < 1) return "just now";
        if (diff.TotalMinutes < 60) return $"{(int)diff.TotalMinutes}m ago";
        if (diff.TotalHours < 24) return $"{(int)diff.TotalHours}h ago";
        if (diff.TotalDays < 7) return $"{(int)diff.TotalDays}d ago";
        return ts.ToString("MMM dd");
    }

    /// <summary>
    /// Determines a FeedEntry event type from message content heuristics.
    /// </summary>
    private static string DetectEventType(string content)
    {
        if (content.Contains("Subagent Task") || content.Contains("subagent"))
        {
            if (content.Contains("complete") || content.Contains("done") || content.Contains("finished"))
                return "task_complete";
            return "task_start";
        }
        if (content.Contains("Deploy") || content.Contains("deploy") || content.Contains("publish"))
            return "deploy";
        if (content.Contains("System") || content.Contains("system") || content.Contains("health"))
            return "system";
        if (content.Contains("Gestartet") || content.Contains("started") || content.Contains("Session"))
            return "session_start";
        return "chat";
    }

    /// <summary>
    /// Extracts a human-readable agent name and action summary from message content.
    /// </summary>
    private static (string Agent, string Action) ExtractAgentAction(string content)
    {
        // Take first line or first ~80 chars as the action summary
        var firstLine = content.Split('\n', 2)[0].Trim();
        var summary = firstLine.Length > 80 ? firstLine[..80] + "\u2026" : firstLine;

        // Try to identify which agent this came from
        var agent = "Iris";
        foreach (var marker in new[] { "**Agent:**", "**Agent:** ", "*Agent:* ", "Agent:" })
        {
            var idx = content.IndexOf(marker, StringComparison.OrdinalIgnoreCase);
            if (idx >= 0)
            {
                var after = content[(idx + marker.Length)..].TrimStart();
                var end = after.IndexOfAny(['\n', '\r', ',', '.']);
                var found = end > 0 ? after[..end].Trim() : after.Split('\n', 2)[0].Trim();
                if (!string.IsNullOrWhiteSpace(found) && found.Length < 30)
                {
                    agent = found;
                    break;
                }
            }
        }

        // Try to find agent name at the start in brackets like [Agent: Iris]
        if (agent == "Iris")
        {
            var bracketMatch = System.Text.RegularExpressions.Regex.Match(content, @"\[Agent:\s*([^\]]+)\]");
            if (bracketMatch.Success)
                agent = bracketMatch.Groups[1].Value.Trim();
        }

        return (agent, summary);
    }

    public async Task<ChatResponse> SendChatMessageAsync(string agentId, string message)
    {
        try
        {
            var result = await InvokeToolAsync("sessions_send", new { agentId, message });
            if (result is null)
                return new ChatResponse(false, null, "Gateway nicht erreichbar");

            var details = result["details"];
            var ok = (details?["status"]?.GetValue<string>()
                       ?? result["status"]?.GetValue<string>()) == "ok";
            var reply = details?["reply"]?.GetValue<string>()
                ?? result["reply"]?.GetValue<string>()
                ?? result["response"]?.GetValue<string>()
                ?? result["content"]?[0]?["text"]?.GetValue<string>();
            var error = details?["error"]?.GetValue<string>()
                ?? result["error"]?.GetValue<string>();

            return new ChatResponse(ok, reply, error);
        }
        catch (Exception ex)
        {
            return new ChatResponse(false, null, "Fehler: " + ex.Message);
        }
    }

    public async Task<List<QueueItem>> GetQueueAsync()
    {
        try
        {
            var result = await InvokeToolAsync("cron", new { action = "list" });
            if (result is null)
                return new List<QueueItem>();

            var details = result["details"];
            var jobs = details?["jobs"] as JsonArray ?? result.AsArray();
            if (jobs is null)
                return new List<QueueItem>();

            var items = new List<QueueItem>();
            foreach (var j in jobs)
            {
                if (j is null) continue;
                var id = j["id"]?.GetValue<string>() ?? "";
                var name = j["name"]?.GetValue<string>() ?? id;
                var status = j["state"]?["lastStatus"]?.GetValue<string>()
                    ?? j["status"]?.GetValue<string>()
                    ?? "unknown";

                // Calculate waitTime from nextRun if available
                var waitTime = "--";
                var nextRunStr = j["nextRun"]?.GetValue<string>()
                    ?? j["next_run"]?.GetValue<string>()
                    ?? j["scheduledAt"]?.GetValue<string>();
                if (nextRunStr is not null && DateTimeOffset.TryParse(nextRunStr, out var nextRun))
                {
                    var diff = nextRun - DateTimeOffset.UtcNow;
                    if (diff.TotalMinutes < 0)
                        waitTime = "now";
                    else if (diff.TotalMinutes < 1)
                        waitTime = "<1m";
                    else if (diff.TotalMinutes < 60)
                        waitTime = $"{(int)diff.TotalMinutes}m";
                    else if (diff.TotalHours < 24)
                        waitTime = $"{(int)diff.TotalHours}h";
                    else
                        waitTime = $"{(int)diff.TotalDays}d";
                }

                items.Add(new QueueItem(id, name, status, "medium", "cron", waitTime));
            }
            return items;
        }
        catch
        {
            return new List<QueueItem>();
        }
    }

    public async Task<bool> DeleteCronJobAsync(string id)
    {
        try
        {
            var result = await InvokeToolAsync("cron", new { action = "delete", id });
            return result is not null;
        }
        catch
        {
            return false;
        }
    }

    public async Task<DashboardStatus> GetStatusAsync()
    {
        var gatewayOk = false;
        var irisStatus = "Offline";
        var activeAgents = 0;
        var pendingTasks = 0;

        try
        {
            using var pingRequest = new HttpRequestMessage(HttpMethod.Get, "/health");
            using var pingResponse = await httpClient.SendAsync(pingRequest);
            gatewayOk = pingResponse.IsSuccessStatusCode;
        }
        catch { }

        if (gatewayOk)
        {
            try
            {
                var r = await InvokeToolAsync("session_status");
                if (r is not null)
                    irisStatus = r["status"]?.GetValue<string>()
                        ?? r["sessionKey"]?.GetValue<string>()
                        ?? "Active";
            }
            catch { }

            try
            {
                var a = await GetAgentsAsync();
                activeAgents = a.Count(x => x.IsActive);
            }
            catch { }

            try
            {
                var q = await GetQueueAsync();
                pendingTasks = q.Count;
            }
            catch { }
        }

        return new DashboardStatus(gatewayOk, irisStatus, activeAgents, pendingTasks);
    }

    public async Task<AgentModelInfo?> GetAgentModelAsync(string agentId)
    {
        try
        {
            var result = await InvokeToolAsync("session_status", new
            {
                sessionKey = $"agent:{agentId}:main"
            });
            if (result is null)
                return null;

            var model = result["model"]?.GetValue<string>();
            var provider = result["provider"]?.GetValue<string>();

            if (model is null)
                return null;

            return new AgentModelInfo(model, provider ?? "unknown");
        }
        catch
        {
            return null;
        }
    }

    public async Task<bool> SetAgentModelAsync(string agentId, string model)
    {
        try
        {
            var result = await InvokeToolAsync("session_status", new
            {
                sessionKey = $"agent:{agentId}:main",
                model
            });
            return result is not null;
        }
        catch
        {
            return false;
        }
    }

    /// <summary>
    /// Reads the agent's goal from workspace files (goals.md preferred, then AGENTS.md, SOUL.md).
    /// Returns the first meaningful line or null.
    /// </summary>
    private async Task<string?> ReadAgentGoalAsync(string agentId)
    {
        try
        {
            var workspacePath = "/home/node/.openclaw/workspace-" + agentId;

            // 1. Try goals.md first
            var goalsPath = Path.Combine(workspacePath, "goals.md");
            if (System.IO.File.Exists(goalsPath))
            {
                var content = await System.IO.File.ReadAllTextAsync(goalsPath);
                foreach (var line in content.Split('\n'))
                {
                    var trimmed = line.Trim();
                    if (!string.IsNullOrWhiteSpace(trimmed) && !trimmed.StartsWith('#') && !trimmed.StartsWith('-'))
                        return trimmed.Length > 120 ? trimmed[..117] + "..." : trimmed;
                }
            }

            // 2. Try SOUL.md "## Rolle" section
            var soulPath = Path.Combine(workspacePath, "SOUL.md");
            if (System.IO.File.Exists(soulPath))
            {
                var soul = await System.IO.File.ReadAllTextAsync(soulPath);
                using var reader = new StringReader(soul);
                string? line;
                var inRoleSection = false;
                while ((line = reader.ReadLine()) is not null)
                {
                    var trimmed = line.Trim();
                    if (trimmed.StartsWith("## ", StringComparison.OrdinalIgnoreCase))
                    {
                        if (inRoleSection) break;
                        if (trimmed.IndexOf("Rolle", StringComparison.OrdinalIgnoreCase) >= 0
                            || trimmed.IndexOf("Role", StringComparison.OrdinalIgnoreCase) >= 0
                            || trimmed.IndexOf("Oberstes", StringComparison.OrdinalIgnoreCase) >= 0)
                        {
                            inRoleSection = true;
                        }
                        continue;
                    }
                    if (inRoleSection && !string.IsNullOrWhiteSpace(trimmed) && !trimmed.StartsWith('-'))
                    {
                        return trimmed.Length > 120 ? trimmed[..117] + "..." : trimmed;
                    }
                }

                // 3. Look for "## Oberstes Prinzip" as second choice
                inRoleSection = false;
                using var reader2 = new StringReader(soul);
                while ((line = reader2.ReadLine()) is not null)
                {
                    var trimmed = line.Trim();
                    if (trimmed.StartsWith("## ") && trimmed.IndexOf("Prinzip", StringComparison.OrdinalIgnoreCase) >= 0)
                    {
                        inRoleSection = true;
                        continue;
                    }
                    if (inRoleSection && !string.IsNullOrWhiteSpace(trimmed) && !trimmed.StartsWith('-') && !trimmed.StartsWith('#'))
                    {
                        return trimmed.Length > 120 ? trimmed[..117] + "..." : trimmed;
                    }
                }
            }

            // 4. Try AGENTS.md first heading
            var agentsPath = Path.Combine(workspacePath, "AGENTS.md");
            if (System.IO.File.Exists(agentsPath))
            {
                var agentsContent = await System.IO.File.ReadAllTextAsync(agentsPath);
                foreach (var line in agentsContent.Split('\n'))
                {
                    var trimmed = line.Trim();
                    if (trimmed.StartsWith("# "))
                        return trimmed[2..].Trim();
                }
            }
        }
        catch
        {
            // Fallback to hardcoded
        }

        // Fallback: known goals
        return agentId.ToLowerInvariant() switch
        {
            "iris" => "Mission Control — maximale Autonomie bei kontrolliertem Risiko",
            "programmer" => "Nexus Dashboard & Dungeon System",
            "reviewer" => "Zero critical findings before merge",
            "architekt" => "Stabile Zero-Downtime-Deployments",
            "executor" => "Sichere Host-Execution im Allowlist-Rahmen",
            "researcher" => "Verifizierte, strukturierte Recherche-Ergebnisse",
            _ => null
        };
    }

    /// <summary>
    /// Calculates agent progress (0-100) based on session activity.
    /// Active agents with recent activity get higher scores.
    /// Idle agents or inactive sessions get lower scores.
    /// </summary>
    private static int CalculateAgentProgress(string agentId, bool isActive, JsonNode? status)
    {
        if (!isActive)
            return 0;

        // Base progress from active status
        var progress = 50;

        // Boost for agents that have a current task
        var hasTask = status?["currentTask"]?.GetValue<string>() is not null
                   || status?["task"]?.GetValue<string>() is not null;
        if (hasTask)
            progress += 20;

        // Check for last activity timestamp — more recent = higher progress
        var lastActivity = status?["lastActivity"]?.GetValue<string>()
                        ?? status?["lastMessage"]?.GetValue<string>();
        if (lastActivity is not null && DateTimeOffset.TryParse(lastActivity, out var lastTs))
        {
            var minutesSinceActivity = (DateTimeOffset.UtcNow - lastTs).TotalMinutes;
            if (minutesSinceActivity < 1)
                progress += 25; // actively working
            else if (minutesSinceActivity < 5)
                progress += 15;
            else if (minutesSinceActivity < 15)
                progress += 10;
            else
                progress -= 10; // stale
        }
        else if (hasTask)
        {
            // Has task but no timestamp — assume actively working
            progress += 15;
        }

        return Math.Clamp(progress, 0, 100);
    }

    /// <summary>
    /// Calculates agent workload (0-100) based on queue items and active status.
    /// More queued tasks or active sessions = higher workload.
    /// </summary>
    private static int CalculateAgentWorkload(string agentId, List<QueueItem> queueItems)
    {
        if (queueItems.Count == 0)
            return 0;

        // Calculate workload based on queue density per agent
        var agentQueued = queueItems.Count(q =>
            q.Name.Contains(agentId, StringComparison.OrdinalIgnoreCase)
            || q.Id.Contains(agentId, StringComparison.OrdinalIgnoreCase));

        // Base workload from total queue pressure
        var totalQueuePressure = Math.Min(queueItems.Count * 10, 60);

        // Agent-specific queue items add extra weight
        var agentPressure = agentQueued * 25;

        return Math.Clamp(totalQueuePressure + agentPressure, 0, 100);
    }

    /// <summary>
    /// Fetches the most recent assistant activity (last N messages) for a specific agent.
    /// Returns entries with timestamp and truncated content text.
    /// Falls back to an empty list if the session is unreachable.
    /// </summary>
    public async Task<List<AgentActivityEntry>> GetAgentActivityAsync(string agentId, int limit = 5)
    {
        var entries = new List<AgentActivityEntry>();
        try
        {
            var sessionKey = $"agent:{agentId}:main";
            var messages = await GetSessionHistoryAsync(sessionKey, Math.Clamp(limit * 2, 1, 100));
            foreach (var msg in messages)
            {
                if (!string.Equals(msg.Role, "assistant", StringComparison.OrdinalIgnoreCase))
                    continue;
                if (string.IsNullOrWhiteSpace(msg.Content))
                    continue;
                if (msg.Content == "REPLY_SKIP" || msg.Content == "ANNOUNCE_SKIP")
                    continue;

                // Truncate content to first 200 chars for compact display
                var text = msg.Content.Length > 200
                    ? msg.Content[..200] + "…"
                    : msg.Content;
                var ts = ParseTimestamp(msg.Timestamp);
                var timeAgo = FormatTimeAgo(ts);

                entries.Add(new AgentActivityEntry(timeAgo, text));
            }
        }
        catch
        {
            // Return empty list if gateway is unreachable
        }
        return entries.Take(Math.Clamp(limit, 1, 20)).ToList();
    }

    /// <summary>
    /// Returns the list of available models by reading from the OpenClaw config,
    /// with fallback to hardcoded list.
    /// </summary>
    public List<ModelOption> GetAvailableModels()
    {
        try
        {
            var configPath = configuration.GetValue<string>("AgentConfigPath")
                ?? "/home/node/.openclaw/openclaw.json";

            if (!System.IO.File.Exists(configPath))
                return GetDefaultModels();

            var json = System.IO.File.ReadAllText(configPath);
            using var doc = JsonDocument.Parse(json);
            var root = doc.RootElement;

            // Read models from agents.defaults.models
            if (!root.TryGetProperty("agents", out var agentsEl))
                return GetDefaultModels();
            if (!agentsEl.TryGetProperty("defaults", out var defaultsEl))
                return GetDefaultModels();
            if (!defaultsEl.TryGetProperty("models", out var modelsEl))
                return GetDefaultModels();

            var models = new List<ModelOption>();
            foreach (var modelProp in modelsEl.EnumerateObject())
            {
                var modelId = modelProp.Name;
                var modelObj = modelProp.Value;

                var name = modelId; // fallback: use model ID as name
                var provider = ExtractProvider(modelId);

                // Check for alias in the model object
                if (modelObj.TryGetProperty("alias", out var aliasEl))
                {
                    var alias = aliasEl.GetString();
                    if (!string.IsNullOrWhiteSpace(alias))
                        name = alias;
                }

                models.Add(new ModelOption(modelId, name, provider));
            }

            return models.Count > 0 ? models : GetDefaultModels();
        }
        catch
        {
            return GetDefaultModels();
        }
    }

    private static List<ModelOption> GetDefaultModels()
        => new()
        {
            new ModelOption("openai/gpt-5.4", "GPT-5.4", "openai"),
            new ModelOption("openai/gpt-5.5", "GPT-5.5", "openai"),
            new ModelOption("deepseek/deepseek-v4-flash", "DeepSeek V4 Flash", "deepseek"),
            new ModelOption("deepseek/deepseek-v4-pro", "DeepSeek V4 Pro", "deepseek")
        };

    private static string ExtractProvider(string modelId)
    {
        var slash = modelId.IndexOf('/');
        return slash > 0 ? modelId[..slash] : "unknown";
    }

    private static string DeriveRoleBadge(string agentId) => agentId.ToLowerInvariant() switch
    {
        "iris" => "badge-purple",
        "programmer" or "developer" => "badge-blue",
        "reviewer" => "badge-amber",
        "architekt" => "badge-cyan",
        "executor" => "badge-rose",
        "researcher" => "badge-green",
        _ => "badge-slate"
    };

    private static string DeriveStatusLabel(bool isActive, JsonNode? status)
    {
        if (!isActive) return "Bereit";
        var statusText = status?["status"]?.GetValue<string>()?.ToLowerInvariant();
        return statusText switch
        {
            "thinking" or "think" => "Plant",
            "blocked" or "block" => "Blockiert",
            _ => "Arbeitet"
        };
    }

    private static string? FormatElapsed(JsonNode? status)
    {
        var lastActivity = status?["lastActivity"]?.GetValue<string>()
                        ?? status?["lastMessage"]?.GetValue<string>();
        if (lastActivity is null) return null;
        if (!DateTimeOffset.TryParse(lastActivity, out var ts)) return null;
        var diff = DateTimeOffset.UtcNow - ts;
        if (diff.TotalSeconds < 60) return $"{(int)diff.TotalSeconds}s";
        if (diff.TotalMinutes < 60) return $"{(int)diff.TotalMinutes}m";
        if (diff.TotalHours < 24) return $"{(int)diff.TotalHours}h";
        return $"{(int)diff.TotalDays}d";
    }

    private static string DeriveNext(bool isActive, string? currentTask)
    {
        if (!isActive) return "Standby";
        if (!string.IsNullOrWhiteSpace(currentTask) && currentTask != "Working...")
            return currentTask.Length > 60 ? currentTask[..60] + "…" : currentTask;
        return "Aufgabe ausführen";
    }

    private static string DeriveRole(string agentId) => agentId.ToLowerInvariant() switch
    {
        "iris" => "Chief of Staff",
        "programmer" => "Full-Stack Developer",
        "reviewer" => "Code Quality Assurance",
        "architekt" => "Infrastructure Architect",
        "executor" => "Host Executor",
        "researcher" => "Research & Analysis",
        "main" => "Assistant",
        _ => "Custom"
    };
}
