# TASK 5.1: Create RunAnomalyDetectorCommand in CLI

## Summary

Create the CLI command that triggers anomaly detection runs, enabling manual testing and ad-hoc execution from the command line. The command identifies monitors by **name** or **spec ID** (not store GUID).

## Dependencies

- TASK 4.1 (IMonitorRunOrchestrator)
- TASK 3.1 (IMonitorSpecRepository)
- TASK 3.3 (MonitorSpecSeeder for auto-creation)

## Files to Create

### 1. `backend/Kds.Ai.Cli/Commands/AnomalyDetector/RunAnomalyDetectorCommand.cs`

```csharp
using ConsoleAppFramework;
using Kds.Ai.AnomalyDetector.Orchestration;
using Kds.Ai.AnomalyDetector.Seeding;
using Kds.Ai.Data.Entities;
using Kds.Ai.Data.Enums;
using Kds.Ai.Data.Repositories;

namespace Kds.Ai.Cli.Commands.AnomalyDetector;

/// <summary>
/// CLI commands for running anomaly detection
/// </summary>
[RegisterCommands("anomaly")]
public class RunAnomalyDetectorCommand(
    IMonitorRunOrchestrator orchestrator,
    IMonitorSpecRepository specRepository,
    MonitorSpecSeeder seeder)
{
    
    /// <summary>
    /// Run anomaly detection for a specific monitor spec
    /// </summary>
    /// <param name="name">-n, Monitor spec name</param>
    /// <param name="specId">--spec-id, Monitor spec ID</param>
    /// <param name="analysisTime">-t|--time, Analysis time in ISO format</param>
    /// <param name="dryRun">-d|--dry-run, Run without saving results or emitting alerts</param>
    /// <param name="verbose">-v|--verbose, Show detailed output</param>
    [Command("run")]
    public async Task Run(
        string? name = null,
        Guid? specId = null,
        string? analysisTime = null,
        bool dryRun = false,
        bool verbose = false)
    {
        if (name is null && specId is null)
        {
            Console.WriteLine("ERROR: Specify either --name (-n) or --spec-id");
            return;
        }
        
        // Resolve spec
        var spec = specId.HasValue
            ? await specRepository.GetByIdAsync(specId.Value)
            : await specRepository.GetByNameAsync(name!);
        
        if (spec is null)
        {
            Console.WriteLine(specId.HasValue
                ? $"ERROR: Monitor spec {specId} not found"
                : $"ERROR: Monitor spec '{name}' not found");
            Console.WriteLine("Use 'anomaly list' to see available monitors or 'anomaly create' to create one.");
            return;
        }
        
        // Parse analysis time
        var time = ParseAnalysisTime(analysisTime);
        
        Console.WriteLine($"Monitor: {spec.Name} ({spec.Id})");
        Console.WriteLine($"State: {spec.State}");
        Console.WriteLine($"Analysis time: {time:yyyy-MM-dd HH:mm:ss} UTC");
        Console.WriteLine();
        
        // Configure options
        var options = new RunOptions
        {
            DryRun = dryRun,
            Progress = verbose ? new ConsoleProgress() : null
        };
        
        if (dryRun)
        {
            Console.WriteLine("[DRY RUN MODE - No changes will be saved]");
            Console.WriteLine();
        }
        
        // Execute
        try
        {
            var run = await orchestrator.ExecuteAsync(spec, time, options);
            
            // Output results
            Console.WriteLine();
            Console.WriteLine("=== Results ===");
            Console.WriteLine($"Run ID: {run.Id}");
            Console.WriteLine($"Status: {run.Status}");
            Console.WriteLine($"Duration: {run.DurationMs}ms");
            Console.WriteLine($"Anomalies Detected: {run.AnomaliesDetected}");
            
            if (run.TokensUsed.HasValue)
            {
                Console.WriteLine($"Tokens Used: {run.TokensUsed}");
            }
            
            if (run.AlertEvents.Count > 0)
            {
                Console.WriteLine($"Alerts Emitted: {run.AlertEvents.Count}");
                foreach (var alert in run.AlertEvents)
                {
                    Console.WriteLine($"  - {alert.Id} ({alert.Severity})");
                }
            }
            
            if (!string.IsNullOrEmpty(run.ErrorMessage))
            {
                Console.WriteLine($"Error: {run.ErrorMessage}");
            }
            
            if (verbose && !string.IsNullOrEmpty(run.ResultSummary))
            {
                Console.WriteLine();
                Console.WriteLine("=== Result Summary ===");
                Console.WriteLine(run.ResultSummary);
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"ERROR: {ex.Message}");
            if (verbose)
            {
                Console.WriteLine(ex.ToString());
            }
        }
    }
    
    /// <summary>
    /// List monitor specs
    /// </summary>
    /// <param name="state">--state, Filter by state: draft, active, paused, archived, all</param>
    [Command("list")]
    public async Task List(string state = "all")
    {
        IReadOnlyList<MonitorSpec> specs;
        
        if (state.Equals("all", StringComparison.OrdinalIgnoreCase))
        {
            specs = await specRepository.GetAllAsync();
        }
        else
        {
            var stateEnum = Enum.Parse<MonitorState>(state, ignoreCase: true);
            specs = await specRepository.GetByStateAsync(stateEnum);
        }
        
        if (specs.Count == 0)
        {
            Console.WriteLine("No monitor specs found.");
            return;
        }
        
        Console.WriteLine($"Found {specs.Count} monitor specs:");
        Console.WriteLine();
        
        foreach (var spec in specs)
        {
            var lastRun = spec.LastRunAt?.ToString("yyyy-MM-dd HH:mm") ?? "Never";
            
            Console.WriteLine($"  ID: {spec.Id}");
            Console.WriteLine($"  Name: {spec.Name}");
            Console.WriteLine($"  State: {spec.State}");
            Console.WriteLine($"  Last Run: {lastRun}");
            Console.WriteLine();
        }
    }
    
    /// <summary>
    /// Create a new monitor spec
    /// </summary>
    /// <param name="name">-n|--name, Monitor name [required]</param>
    /// <param name="promptFile">-p|--prompt-file, Path to a text file containing the system prompt</param>
    /// <param name="useExample">--example, Use the built-in example prompt</param>
    [Command("create")]
    public async Task Create(
        string name,
        string? promptFile = null,
        bool useExample = false)
    {
        var exists = await specRepository.ExistsByNameAsync(name);
        
        if (exists)
        {
            Console.WriteLine($"ERROR: A monitor with name '{name}' already exists.");
            return;
        }
        
        string systemPrompt;
        
        if (promptFile is not null)
        {
            if (!File.Exists(promptFile))
            {
                Console.WriteLine($"ERROR: Prompt file not found: {promptFile}");
                return;
            }
            systemPrompt = await File.ReadAllTextAsync(promptFile);
        }
        else if (useExample)
        {
            systemPrompt = ExamplePrompts.PrepTimeAnomaly;
        }
        else
        {
            Console.WriteLine("ERROR: Specify either --prompt-file (-p) or --example");
            return;
        }
        
        var spec = await seeder.EnsureAsync(name, systemPrompt);
        
        Console.WriteLine("Monitor spec created successfully:");
        Console.WriteLine($"  ID: {spec.Id}");
        Console.WriteLine($"  Name: {spec.Name}");
        Console.WriteLine($"  State: {spec.State}");
        Console.WriteLine();
        Console.WriteLine("Use 'anomaly set-state --spec-id <id> --state active' to activate it.");
    }
    
    /// <summary>
    /// Change the state of a monitor spec
    /// </summary>
    /// <param name="specId">--spec-id, Monitor spec ID [required]</param>
    /// <param name="state">--state, New state: draft, active, paused, archived [required]</param>
    [Command("set-state")]
    public async Task SetState(Guid specId, string state)
    {
        var spec = await specRepository.GetByIdAsync(specId);
        if (spec is null)
        {
            Console.WriteLine($"ERROR: Monitor spec {specId} not found");
            return;
        }
        
        var newState = Enum.Parse<MonitorState>(state, ignoreCase: true);
        await specRepository.SetStateAsync(specId, newState);
        Console.WriteLine($"Monitor '{spec.Name}' state changed to {newState}");
    }
    
    private static DateTimeOffset ParseAnalysisTime(string? timeStr)
    {
        if (string.IsNullOrEmpty(timeStr))
            return DateTimeOffset.UtcNow;
        
        // Support relative times
        if (timeStr.StartsWith("-"))
        {
            if (int.TryParse(timeStr[1..^1], out var value))
            {
                return timeStr[^1] switch
                {
                    'h' => DateTimeOffset.UtcNow.AddHours(-value),
                    'd' => DateTimeOffset.UtcNow.AddDays(-value),
                    'm' => DateTimeOffset.UtcNow.AddMinutes(-value),
                    _ => throw new ArgumentException($"Unknown time unit: {timeStr}")
                };
            }
        }
        
        if (DateTimeOffset.TryParse(timeStr, out var result))
            return result;
        
        throw new ArgumentException($"Invalid time format: {timeStr}. Use ISO format (e.g., 2025-01-15T14:00:00Z) or relative (-2h, -1d)");
    }
    
    /// <summary>
    /// Console progress reporter
    /// </summary>
    private sealed class ConsoleProgress : IProgress<RunProgress>
    {
        public void Report(RunProgress value)
        {
            var prefix = value.Phase switch
            {
                RunPhase.Completed => "[DONE]",
                RunPhase.Failed => "[FAIL]",
                _ => "[....]"
            };
            
            Console.WriteLine($"{prefix} {value.Phase}: {value.Message}");
        }
    }
}
```

## Command Usage Examples

```bash
# Run anomaly detection for a named monitor
dotnet run --project Kds.Ai.Cli -- anomaly run -n "Prep Time Monitor - Downtown"

# Run by spec ID
dotnet run --project Kds.Ai.Cli -- anomaly run --spec-id "abc123-..."

# Dry run with verbose output
dotnet run --project Kds.Ai.Cli -- anomaly run -n "Prep Time Monitor" -d -v

# Run for a specific historical time
dotnet run --project Kds.Ai.Cli -- anomaly run -n "Prep Time Monitor" -t "2025-01-15T14:00:00Z"

# Run with relative time (2 hours ago)
dotnet run --project Kds.Ai.Cli -- anomaly run -n "Prep Time Monitor" -t "-2h"

# List all monitor specs
dotnet run --project Kds.Ai.Cli -- anomaly list

# List only active specs
dotnet run --project Kds.Ai.Cli -- anomaly list --state active

# Create a new monitor with example prompt
dotnet run --project Kds.Ai.Cli -- anomaly create -n "Prep Time Monitor - Downtown" --example

# Create a monitor from a prompt file
dotnet run --project Kds.Ai.Cli -- anomaly create -n "Custom Monitor" -p ./prompts/my-prompt.txt

# Change monitor state to active
dotnet run --project Kds.Ai.Cli -- anomaly set-state --spec-id "abc123-..." --state active
```

## Help Output

```
$ dotnet run --project Kds.Ai.Cli -- anomaly --help

anomaly
  Commands for anomaly detection

Commands:
  run          Run anomaly detection for a specific monitor spec
  list         List monitor specs
  create       Create a new monitor spec
  set-state    Change the state of a monitor spec

$ dotnet run --project Kds.Ai.Cli -- anomaly run --help

anomaly run
  Run anomaly detection for a specific monitor spec

Options:
  -n, --name      Monitor spec name
  --spec-id       Monitor spec ID
  -t, --time      Analysis time in ISO format or relative (-2h, -1d)
  -d, --dry-run   Run without saving results or emitting alerts
  -v, --verbose   Show detailed output
```

## Changes from Old Design

- `run` command takes `--name` or `--spec-id` instead of `--store` (MonitorSpec has no StoreGuid)
- No `--create` flag on `run` - use `anomaly create` instead
- `list` filters by `--state` (MonitorState enum) instead of `--store` (StoreGuid)
- `create` takes `--prompt-file` or `--example` instead of `--store` + auto-creating from `PrepTimeMonitorSpecFactory`
- New `set-state` command replaces `toggle --enable/--disable` (MonitorState has 4 states, not boolean)
- Uses `run.AlertEvents` collection instead of `run.AlertEventId`
- No references to `PrepTimeMonitorSpecFactory`, `StoreGuid`, `MonitorType`, `IsEnabled`

## Acceptance Criteria

- [ ] `RunAnomalyDetectorCommand` class created
- [ ] `[RegisterCommands("anomaly")]` attribute applied
- [ ] `run` command accepts `--name` or `--spec-id` (not `--store`)
- [ ] `list` command filters by `--state` (not `--store`)
- [ ] `create` command accepts `--prompt-file` or `--example`
- [ ] `set-state` command transitions MonitorState
- [ ] Dry-run mode prevents persistence
- [ ] Verbose mode shows progress
- [ ] Relative time parsing (`-2h`, `-1d`) works
- [ ] Uses `run.AlertEvents` (collection, not single FK)
- [ ] No references to `StoreGuid`, `MonitorType`, `IsEnabled`, `PrepTimeMonitorSpecFactory`
- [ ] Help text displays correctly
- [ ] Solution builds without errors

## Notes

- ConsoleAppFramework handles argument parsing and help generation
- Parameter names automatically become options (e.g., `name` becomes `--name`)
- Aliases are defined in XML doc comments (e.g., `-n|--name` in `<param>` tag)
- No `[Option]` attributes needed - they are deprecated in ConsoleAppFramework v5
- Progress reporting only shown in verbose mode to reduce noise
- Monitors are created in `Draft` state by default, must be activated with `set-state`
