using System.Diagnostics;
using System.Linq;
using System.Windows;
using AxCopilot.Models;
namespace AxCopilot.Services;
///
/// 예약 작업을 백그라운드에서 점검하고 조건이 맞으면 액션을 실행합니다.
/// 활성 일정이 없을 때는 타이머를 돌리지 않아 유휴 CPU 사용을 줄입니다.
///
public sealed class SchedulerService : IDisposable
{
private readonly SettingsService _settings;
private Timer? _timer;
private bool _disposed;
public SchedulerService(SettingsService settings)
{
_settings = settings;
}
public void Start()
{
if (_disposed || _timer != null) return;
_timer = new Timer(OnTick, null, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30));
LogService.Info("SchedulerService 시작");
}
public void Stop()
{
if (_timer == null) return;
_timer.Dispose();
_timer = null;
LogService.Info("SchedulerService 중지");
}
public void Refresh()
{
if (_disposed) return;
if (HasEnabledSchedules())
{
Start();
return;
}
Stop();
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
Stop();
}
private void OnTick(object? _)
{
try
{
if (!HasEnabledSchedules())
{
Stop();
return;
}
var now = DateTime.Now;
var schedules = _settings.Settings.Schedules;
var dirty = false;
foreach (var entry in schedules.ToList())
{
if (!ShouldFire(entry, now)) continue;
LogService.Info($"스케줄 실행: '{entry.Name}' ({entry.TriggerType} {entry.TriggerTime})");
ExecuteAction(entry);
entry.LastRun = now;
dirty = true;
if (entry.TriggerType == "once")
entry.Enabled = false;
}
if (dirty)
{
_settings.Save();
Refresh();
}
}
catch (Exception ex)
{
LogService.Error($"SchedulerService 오류: {ex.Message}");
}
}
private bool HasEnabledSchedules() =>
_settings.Settings.Schedules.Any(entry => entry.Enabled);
private static bool ShouldFire(ScheduleEntry entry, DateTime now)
{
if (!entry.Enabled) return false;
if (!TimeSpan.TryParse(entry.TriggerTime, out var triggerTime)) return false;
var targetDt = now.Date + triggerTime;
if (Math.Abs((now - targetDt).TotalMinutes) > 1.0) return false;
if (entry.TriggerType != "once" &&
entry.LastRun.HasValue &&
entry.LastRun.Value.Date == now.Date)
{
return false;
}
var typeMatch = entry.TriggerType switch
{
"daily" => true,
"weekdays" => now.DayOfWeek >= DayOfWeek.Monday &&
now.DayOfWeek <= DayOfWeek.Friday,
"weekly" => entry.WeekDays.Count > 0 &&
entry.WeekDays.Contains((int)now.DayOfWeek),
"once" => !entry.LastRun.HasValue &&
entry.TriggerDate != null &&
DateTime.TryParse(entry.TriggerDate, out var d) &&
now.Date == d.Date,
_ => false
};
if (!typeMatch) return false;
if (!string.IsNullOrWhiteSpace(entry.ConditionProcess))
{
var procName = entry.ConditionProcess.Trim()
.Replace(".exe", "", StringComparison.OrdinalIgnoreCase);
var isRunning = Process.GetProcessesByName(procName).Length > 0;
if (entry.ConditionProcessMustRun && !isRunning) return false;
if (!entry.ConditionProcessMustRun && isRunning) return false;
}
return true;
}
private static void ExecuteAction(ScheduleEntry entry)
{
try
{
switch (entry.ActionType)
{
case "app":
if (!string.IsNullOrWhiteSpace(entry.ActionTarget))
{
Process.Start(new ProcessStartInfo
{
FileName = entry.ActionTarget,
Arguments = entry.ActionArgs ?? "",
UseShellExecute = true
});
}
break;
case "notification":
var msg = string.IsNullOrWhiteSpace(entry.ActionTarget)
? entry.Name
: entry.ActionTarget;
Application.Current?.Dispatcher.Invoke(() =>
NotificationService.Notify($"[일정] {entry.Name}", msg));
break;
}
}
catch (Exception ex)
{
LogService.Warn($"스케줄 액션 실행 실패 '{entry.Name}': {ex.Message}");
}
}
public static DateTime? ComputeNextRun(ScheduleEntry entry)
{
if (!entry.Enabled) return null;
if (!TimeSpan.TryParse(entry.TriggerTime, out var t)) return null;
var now = DateTime.Now;
return entry.TriggerType switch
{
"once" when
!entry.LastRun.HasValue &&
entry.TriggerDate != null &&
DateTime.TryParse(entry.TriggerDate, out var d) &&
(now.Date + t) >= now =>
d.Date + t,
"daily" => NextOccurrence(now, t, _ => true),
"weekdays" => NextOccurrence(now, t, dt =>
dt.DayOfWeek >= DayOfWeek.Monday && dt.DayOfWeek <= DayOfWeek.Friday),
"weekly" when entry.WeekDays.Count > 0 => NextOccurrence(now, t, dt =>
entry.WeekDays.Contains((int)dt.DayOfWeek)),
_ => null
};
}
private static DateTime? NextOccurrence(DateTime now, TimeSpan t, Func dayFilter)
{
for (var i = 0; i <= 7; i++)
{
var candidate = now.Date.AddDays(i) + t;
if (candidate > now && dayFilter(candidate))
return candidate;
}
return null;
}
public static string TriggerLabel(ScheduleEntry e) => e.TriggerType switch
{
"daily" => "매일",
"weekdays" => "주중(월~금)",
"weekly" => WeekDayLabel(e.WeekDays),
"once" => $"한번({e.TriggerDate})",
_ => e.TriggerType
};
private static readonly string[] DayShort = ["일", "월", "화", "수", "목", "금", "토"];
private static string WeekDayLabel(List days) =>
days.Count == 0
? "매주(요일 미지정)"
: "매주 " + string.Join(", ", days.OrderBy(d => d).Select(d => DayShort[d]));
}