using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
///
/// L19-2: 타이머·알람 핸들러. "timer" 프리픽스로 사용합니다.
///
/// 예: timer → 사용법 + 실행 중인 타이머 목록
/// timer 30 → 30초 타이머 (Enter로 시작)
/// timer 5m → 5분 타이머
/// timer 1h30m → 1시간 30분 타이머
/// timer 2h → 2시간 타이머
/// timer 10m30s → 10분 30초 타이머
/// timer stop → 모든 타이머 취소
/// timer stop → 특정 타이머 취소
/// Enter → 타이머 시작 (또는 중단).
///
public class TimerHandler : IActionHandler
{
public string? Prefix => "timer";
public PluginMetadata Metadata => new(
"Timer",
"타이머·알람 — 초/분/시간 단위 백그라운드 타이머",
"1.0",
"AX");
// 타이머 레코드
private record TimerEntry(int Id, string Label, TimeSpan Duration, DateTime StartAt, CancellationTokenSource Cts);
// 정적 타이머 레지스트리
private static readonly List _timers = [];
private static readonly object _lock = new();
private static int _nextId = 1;
public Task> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
var items = new List();
if (string.IsNullOrWhiteSpace(q))
{
items.Add(new LauncherItem("타이머·알람",
"timer 30 / timer 5m / timer 1h30m / timer stop",
null, null, Symbol: "\uE916"));
items.Add(new LauncherItem("사용법", "timer <시간> · 예: 30(초) / 5m / 1h30m / 2h", null, null, Symbol: "\uE916"));
AddRunningTimers(items);
return Task.FromResult>(items);
}
var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries);
var sub = parts[0].ToLowerInvariant();
// stop 명령
if (sub is "stop" or "cancel" or "취소")
{
lock (_lock)
{
if (_timers.Count == 0)
{
items.Add(new LauncherItem("실행 중인 타이머 없음", "취소할 타이머가 없습니다",
null, null, Symbol: "\uE916"));
return Task.FromResult>(items);
}
// 특정 ID 취소
if (parts.Length >= 2 && int.TryParse(parts[1], out var stopId))
{
var t = _timers.FirstOrDefault(x => x.Id == stopId);
if (t != null)
items.Add(new LauncherItem($"타이머 #{t.Id} '{t.Label}' 취소",
$"Enter로 취소합니다", null, ("stop", stopId.ToString()), Symbol: "\uE916"));
else
items.Add(new LauncherItem($"타이머 #{stopId}를 찾을 수 없습니다",
"timer 명령으로 목록을 확인하세요", null, null, Symbol: "\uE783"));
return Task.FromResult>(items);
}
// 전체 취소
items.Add(new LauncherItem($"모든 타이머 취소 ({_timers.Count}개)",
"Enter로 모두 취소합니다", null, ("stop_all", ""), Symbol: "\uE916"));
foreach (var t in _timers)
items.Add(new LauncherItem($" #{t.Id} {t.Label}",
$"시작: {t.StartAt:HH:mm:ss} 남은: {Remaining(t)}",
null, ("stop", t.Id.ToString()), Symbol: "\uE916"));
}
return Task.FromResult>(items);
}
// 시간 파싱 시도
if (TryParseTime(sub, out var duration) && duration > TimeSpan.Zero)
{
var label = FormatDuration(duration);
var endTime = DateTime.Now.Add(duration);
items.Add(new LauncherItem($"⏱ {label} 타이머 시작",
$"완료: {endTime:HH:mm:ss} · Enter로 시작",
null, ("start", DurationToSeconds(duration).ToString()), Symbol: "\uE916"));
items.Add(new LauncherItem($"완료 예정", $"{endTime:HH:mm:ss}", null, null, Symbol: "\uE916"));
items.Add(new LauncherItem($"경과 시간", label, null, null, Symbol: "\uE916"));
// 실행 중 타이머 표시
AddRunningTimers(items);
}
else
{
items.Add(new LauncherItem($"형식 오류: '{q}'",
"예: timer 30 / timer 5m / timer 1h30m / timer stop",
null, null, Symbol: "\uE783"));
}
return Task.FromResult>(items);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
switch (item.Data)
{
case ("start", string secStr) when long.TryParse(secStr, out var sec):
{
var duration = TimeSpan.FromSeconds(sec);
var label = FormatDuration(duration);
var cts = new CancellationTokenSource();
int id;
lock (_lock)
{
id = _nextId++;
_timers.Add(new TimerEntry(id, label, duration, DateTime.Now, cts));
}
NotificationService.Notify("Timer", $"⏱ #{id} {label} 타이머 시작됩니다.");
_ = RunTimerAsync(id, label, duration, cts.Token);
break;
}
case ("stop", string idStr) when int.TryParse(idStr, out var stopId):
{
TimerEntry? entry = null;
lock (_lock)
{
entry = _timers.FirstOrDefault(t => t.Id == stopId);
if (entry != null) _timers.Remove(entry);
}
if (entry != null)
{
entry.Cts.Cancel();
NotificationService.Notify("Timer", $"⏹ #{entry.Id} '{entry.Label}' 타이머가 취소되었습니다.");
}
break;
}
case ("stop_all", _):
{
List all;
lock (_lock)
{
all = [.._timers];
_timers.Clear();
}
foreach (var t in all) t.Cts.Cancel();
NotificationService.Notify("Timer", $"⏹ 타이머 {all.Count}개 모두 취소됐습니다.");
break;
}
}
return Task.CompletedTask;
}
// ── 타이머 실행 ──────────────────────────────────────────────────────────
private static async Task RunTimerAsync(int id, string label, TimeSpan duration, CancellationToken token)
{
try
{
await Task.Delay(duration, token);
// 완료 — 레지스트리에서 제거
lock (_lock) { _timers.RemoveAll(t => t.Id == id); }
NotificationService.Notify("⏰ 타이머 완료", $"#{id} {label} 타이머가 종료되었습니다!");
}
catch (OperationCanceledException)
{
// 취소됨 — 이미 처리됨
}
}
// ── 파싱 헬퍼 ────────────────────────────────────────────────────────────
private static bool TryParseTime(string s, out TimeSpan ts)
{
ts = TimeSpan.Zero;
s = s.ToLowerInvariant().Trim();
// 숫자만 → 초
if (long.TryParse(s, out var sec))
{
ts = TimeSpan.FromSeconds(sec);
return true;
}
// 복합 형식: 1h30m20s
long hours = 0, minutes = 0, seconds = 0;
var rem = s;
bool found = false;
if (TryExtractUnit(ref rem, 'h', out var h)) { hours = h; found = true; }
if (TryExtractUnit(ref rem, 'm', out var m)) { minutes = m; found = true; }
if (TryExtractUnit(ref rem, 's', out var sv)){ seconds = sv; found = true; }
if (found && rem.Length == 0)
{
ts = TimeSpan.FromSeconds(hours * 3600 + minutes * 60 + seconds);
return true;
}
return false;
}
private static bool TryExtractUnit(ref string s, char unit, out long value)
{
value = 0;
var idx = s.IndexOf(unit);
if (idx <= 0) return false;
var numStr = s[..idx];
if (!long.TryParse(numStr, out value)) return false;
s = s[(idx + 1)..];
return true;
}
private static string FormatDuration(TimeSpan ts)
{
if (ts.TotalSeconds < 60) return $"{(long)ts.TotalSeconds}초";
if (ts.TotalMinutes < 60)
{
var mins = (long)ts.TotalMinutes;
var secs = (long)(ts.TotalSeconds - mins * 60);
return secs > 0 ? $"{mins}분 {secs}초" : $"{mins}분";
}
var h = (long)ts.TotalHours;
var m = (long)(ts.TotalMinutes - h * 60);
var s = (long)(ts.TotalSeconds - h * 3600 - m * 60);
var result = $"{h}시간";
if (m > 0) result += $" {m}분";
if (s > 0) result += $" {s}초";
return result;
}
private static long DurationToSeconds(TimeSpan ts) => (long)ts.TotalSeconds;
private static string Remaining(TimerEntry t)
{
var elapsed = DateTime.Now - t.StartAt;
var left = t.Duration - elapsed;
if (left <= TimeSpan.Zero) return "완료";
return FormatDuration(left);
}
private static void AddRunningTimers(List items)
{
List running;
lock (_lock) { running = [.._timers]; }
if (running.Count == 0) return;
items.Add(new LauncherItem($"── 실행 중인 타이머 {running.Count}개 ──", "", null, null, Symbol: "\uE916"));
foreach (var t in running)
{
var left = Remaining(t);
items.Add(new LauncherItem($"#{t.Id} {t.Label}", $"남은 시간: {left} · 시작: {t.StartAt:HH:mm:ss}",
null, ("stop", t.Id.ToString()), Symbol: "\uE916"));
}
}
}