using System.Text;
using ClawdDotNet.Core.Config;
using ClawdDotNet.Core.Engine;
using ClawdDotNet.Core.State;
using ClawdDotNet.Core.Tools;
using Microsoft.Extensions.Logging;

namespace ClawdDotNet.Core.Tasks;

/// <summary>
/// Verbindet den Scanner mit der Engine: übersetzt den Assignee einer fälligen Aufgabe in
/// einen konkreten Lauf.
///
/// - <c>@new</c> / <c>@new:&lt;agent&gt;</c> → <see cref="AgentEngine.RunAsync"/>: frischer
///   Lauf ohne Historie.
/// - <c>@&lt;agent&gt;</c> → <see cref="AgentEngine.ChatAsync"/>: der Agent mit seinem
///   bestehenden Kontext. Das Agent-Gate der Engine serialisiert solche Läufe (B2).
///
/// Ist der Ziel-Agent nicht auflösbar (unbekannt, oder bloßes <c>@new</c> bei mehreren
/// Agenten), scheitert der Dispatch mit einer klaren Meldung, statt einen falschen Agenten
/// zu raten.
/// </summary>
public sealed class EngineTaskDispatcher : ITaskDispatcher
{
    private readonly AgentEngine _engine;
    private readonly Func<IReadOnlyList<AgentConfig>> _agents;
    private readonly string _instanceId;
    private readonly ToolRegistry _toolRegistry;
    private readonly IStateStore _stateStore;
    private readonly ILogger _logger;

    public EngineTaskDispatcher(
        AgentEngine engine,
        Func<IReadOnlyList<AgentConfig>> agents,
        string instanceId,
        ToolRegistry toolRegistry,
        IStateStore stateStore,
        ILoggerFactory loggerFactory)
    {
        _engine = engine;
        _agents = agents;
        _instanceId = instanceId;
        _toolRegistry = toolRegistry;
        _stateStore = stateStore;
        _logger = loggerFactory.CreateLogger("ClawdDotNet.Core.Tasks.Dispatcher");
    }

    public async Task<bool> DispatchAsync(TaskItem task, CancellationToken ct)
    {
        // Poll-Tasks (tool_job) ticken ein Tool statt einen Agenten direkt zu starten.
        if (task.Type == TaskItemType.ToolJob)
            return await DispatchToolJobAsync(task, ct);

        var agents = _agents();
        var message = BuildMessage(task);
        var kind = TaskAssignee.KindOf(task.Assignee);

        AgentRunResult result;
        switch (kind)
        {
            case TaskAssigneeKind.New:
            {
                var config = ResolveFreshAgent(agents, TaskAssignee.AgentId(task.Assignee));
                if (config is null)
                {
                    _logger.LogWarning(
                        "Aufgabe {TaskId}: Assignee '{Assignee}' nicht auflösbar (frischer Lauf braucht einen eindeutigen Agenten)",
                        task.Id, task.Assignee);
                    return false;
                }
                result = await _engine.RunAsync(
                    config, message, _instanceId, ct, source: ChatSource.Task, taskId: task.Id);
                break;
            }
            case TaskAssigneeKind.Agent:
            {
                var agentId = TaskAssignee.AgentId(task.Assignee);
                var config = agents.FirstOrDefault(a => a.AgentId == agentId);
                if (config is null)
                {
                    _logger.LogWarning(
                        "Aufgabe {TaskId}: Agent '{AgentId}' nicht gefunden", task.Id, agentId);
                    return false;
                }
                result = await _engine.ChatAsync(
                    config, message, _instanceId, ct, source: ChatSource.Task, taskId: task.Id);
                break;
            }
            default:
                // @human wird vom Scanner gar nicht erst angestoßen.
                return false;
        }

        return result.Status == AgentRunStatus.Completed;
    }

    /// <summary>
    /// Tickt einen <see cref="IToolJobProvider"/> (Poll) und weckt den Ziel-Agenten nur,
    /// wenn der Tick etwas meldet. Der Tick selbst gilt als erfolgreich, auch wenn nichts
    /// zu tun war — ein Poll ohne Fund ist kein Fehler.
    /// </summary>
    private async Task<bool> DispatchToolJobAsync(TaskItem task, CancellationToken ct)
    {
        if (_toolRegistry.Get(task.ToolName) is not IToolJobProvider provider)
        {
            _logger.LogWarning(
                "Tool-Job {TaskId}: Tool '{Tool}' ist kein IToolJobProvider oder nicht registriert",
                task.Id, task.ToolName);
            return false;
        }

        // Zielagent (zum Wecken) und dessen Tool-Konfiguration/Workspace.
        var agentId = TaskAssignee.AgentId(task.Assignee);
        var config = _agents().FirstOrDefault(a => a.AgentId == agentId);
        var toolConfig = config is not null && config.Tools.TryGetValue(task.ToolName, out var cfg)
            ? (IReadOnlyDictionary<string, object?>)cfg.AsReadOnly()
            : new Dictionary<string, object?>().AsReadOnly();

        var result = await provider.ExecuteJobAsync(
            task.JobTypeId, toolConfig, _stateStore, _logger, ct, agentId, config?.WorkspacePath);

        if (!result.ShouldWakeAgent || string.IsNullOrWhiteSpace(result.WakeMessage))
            return true; // Poll lief, nichts zu wecken

        if (config is null)
        {
            _logger.LogWarning(
                "Tool-Job {TaskId} wollte Agent '{AgentId}' wecken, der aber nicht gefunden wurde",
                task.Id, agentId);
            return true;
        }

        // Der Provider entscheidet je Tick, ob mit Kontext (ChatAsync) oder zustandslos.
        if (result.UseChatContext)
            await _engine.ChatAsync(config, result.WakeMessage, _instanceId, ct, source: ChatSource.Job, taskId: task.Id);
        else
            await _engine.RunAsync(config, result.WakeMessage, _instanceId, ct, source: ChatSource.Job, taskId: task.Id);

        return true;
    }

    private static AgentConfig? ResolveFreshAgent(IReadOnlyList<AgentConfig> agents, string agentId)
    {
        if (!string.IsNullOrWhiteSpace(agentId))
            return agents.FirstOrDefault(a => a.AgentId == agentId);

        // Bloßes @new ohne Agent: nur eindeutig, wenn die Instanz genau einen Agenten hat.
        return agents.Count == 1 ? agents[0] : null;
    }

    private static string BuildMessage(TaskItem task)
    {
        var sb = new StringBuilder();
        sb.AppendLine($"[Aufgabe {task.Id}] {task.Title}");

        if (!string.IsNullOrWhiteSpace(task.Body))
        {
            sb.AppendLine();
            sb.AppendLine(task.Body.Trim());
        }

        if (!string.IsNullOrWhiteSpace(task.Acceptance))
        {
            sb.AppendLine();
            sb.AppendLine("Abnahmekriterien (das Ergebnis wird daran gemessen):");
            sb.AppendLine(task.Acceptance.Trim());
        }

        return sb.ToString().TrimEnd();
    }
}
