using ClawdDotNet.Core.Config;
using ClawdDotNet.Core.Scheduling;
using Microsoft.Extensions.Logging;
namespace ClawdDotNet.Core.Tasks;
///
/// Überführt die alten Scheduler-Konfigurationen ins Taskboard — der letzte Schritt, um
/// die Alt-Scheduler abzulösen: alles Periodische ist danach ein Task.
///
/// - Ein scheduler (Agent nach Cron) → ein Task mit when: cron und Assignee
/// @new:<agent> (frischer Lauf, wie der alte AgentScheduler).
/// - Jeder toolJobs-Eintrag (Poll) → ein Task vom Typ tool_job.
///
/// Einmalig und nicht-destruktiv: Existiert der Task (stabile Id) schon, wird er
/// übersprungen — spätere Änderungen an der Task-Datei bleiben erhalten.
///
public sealed class SchedulerTaskMigration
{
private readonly TaskboardService _board;
private readonly ITaskRepository _repo;
private readonly ILogger _logger;
public SchedulerTaskMigration(TaskboardService board, ITaskRepository repo, ILoggerFactory loggerFactory)
{
_board = board;
_repo = repo;
_logger = loggerFactory.CreateLogger("ClawdDotNet.Core.Tasks.SchedulerMigration");
}
public async Task RunAsync(IEnumerable agents, CancellationToken ct)
{
// IANA-Schreibweise, nicht TimeZoneInfo.Local.Id: Unter Windows lieferte das
// "W. Europe Standard Time", unter Linux "Europe/Berlin". Die erzeugten
// Task-Dateien wandern zwischen Rechnern — sie brauchen die Form, die überall
// gilt.
var localTz = TimeZones.LocalIanaId;
var created = 0;
foreach (var agent in agents)
{
if (agent.Scheduler is { } scheduler && !string.IsNullOrWhiteSpace(scheduler.Cron))
created += await MigrateSchedulerAsync(agent, scheduler, localTz, ct);
foreach (var job in agent.ToolJobs)
created += await MigrateToolJobAsync(agent, job, localTz, ct);
}
if (created > 0)
_logger.LogInformation("Scheduler-Migration: {Count} Task(s) aus Alt-Konfiguration angelegt", created);
return created;
}
private async Task MigrateSchedulerAsync(
AgentConfig agent, SchedulerConfig scheduler, string tz, CancellationToken ct)
{
var id = $"sched-{agent.AgentId}";
if (await _repo.GetAsync(id, ct) is not null)
return 0;
await _board.CreateAsync(new TaskItem
{
Id = id,
Title = $"Geplanter Lauf: {Display(agent)}",
Body = scheduler.TaskMessage,
Status = TaskItemStatus.Todo,
Type = TaskItemType.Work,
Assignee = $"@new:{agent.AgentId}", // frischer Lauf, wie der alte AgentScheduler
When = new TaskWhen { Kind = TaskWhenKind.Cron, Value = scheduler.Cron, TimeZone = tz }
}, ct);
return 1;
}
private async Task MigrateToolJobAsync(
AgentConfig agent, ToolJobConfig job, string tz, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(job.Cron) || string.IsNullOrWhiteSpace(job.ToolName))
return 0;
var id = $"tj-{agent.AgentId}-{job.JobId}";
if (await _repo.GetAsync(id, ct) is not null)
return 0;
await _board.CreateAsync(new TaskItem
{
Id = id,
Title = $"Poll: {job.ToolName}/{job.JobTypeId} → {Display(agent)}",
Body = $"Wiederkehrender {job.ToolName}-Poll ({job.JobTypeId}). Weckt {Display(agent)} bei einem Ereignis.",
// Deaktivierte Jobs kommen als backlog — der Scanner nimmt nur todo/backlog...
// backlog wird aber nicht geclaimt, also ruht der Poll, bis jemand ihn aktiviert.
Status = job.Enabled ? TaskItemStatus.Todo : TaskItemStatus.Backlog,
Type = TaskItemType.ToolJob,
ToolName = job.ToolName,
JobTypeId = job.JobTypeId,
Assignee = $"@{agent.AgentId}", // Zielagent zum Wecken
When = new TaskWhen { Kind = TaskWhenKind.Cron, Value = job.Cron, TimeZone = tz }
}, ct);
return 1;
}
private static string Display(AgentConfig agent)
=> string.IsNullOrWhiteSpace(agent.DisplayName) ? agent.AgentId : agent.DisplayName;
}