# TASK 4.3: Add AlertEvent Emission Logic on Anomaly Detection

## Summary

Create the alert emitter service that converts anomaly detection results into `AlertEvent` entities and persists them to the database.

## Dependencies

- TASK 1.1 (MonitorSpec for context)
- TASK 2.1 (AnomalyDetectionResult as input)
- Existing `AlertEvent` entity from Kds.Ai.Data

## Files to Create

### 1. `backend/Kds.Ai.AnomalyDetector/Alerts/IAlertEmitter.cs`

```csharp
using Kds.Ai.AnomalyDetector.Agents;
using Kds.Ai.Data.Entities;

namespace Kds.Ai.AnomalyDetector.Alerts;

/// <summary>
/// Service for emitting alerts based on anomaly detection results
/// </summary>
public interface IAlertEmitter
{
    /// <summary>
    /// Emit an alert for a detected anomaly
    /// </summary>
    /// <param name="spec">The monitor spec that detected the anomaly</param>
    /// <param name="result">The detection result</param>
    /// <param name="ct">Cancellation token</param>
    /// <returns>The created alert event (Id == Guid.Empty if suppressed)</returns>
    Task<AlertEvent> EmitAsync(
        MonitorSpec spec,
        AnomalyDetectionResult result,
        CancellationToken ct = default);
    
    /// <summary>
    /// Check if an alert should be suppressed due to deduplication
    /// </summary>
    /// <param name="spec">The monitor spec</param>
    /// <param name="result">The detection result</param>
    /// <param name="ct">Cancellation token</param>
    /// <returns>True if alert should be suppressed</returns>
    Task<bool> ShouldSuppressAsync(
        MonitorSpec spec,
        AnomalyDetectionResult result,
        CancellationToken ct = default);
}
```

### 2. `backend/Kds.Ai.AnomalyDetector/Alerts/AlertEmitter.cs`

```csharp
using System.Security.Cryptography;
using System.Text;
using Kds.Ai.AnomalyDetector.Agents;
using Kds.Ai.Data;
using Kds.Ai.Data.Entities;
using Kds.Ai.Data.Enums;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace Kds.Ai.AnomalyDetector.Alerts;

internal sealed class AlertEmitter(
    KdsAiDbContext dbContext,
    ILogger<AlertEmitter> logger) : IAlertEmitter
{
    // Deduplication window - suppress similar alerts within this period
    private static readonly TimeSpan DedupeWindow = TimeSpan.FromHours(4);
    
    public async Task<AlertEvent> EmitAsync(
        MonitorSpec spec,
        AnomalyDetectionResult result,
        CancellationToken ct = default)
    {
        if (!result.AnomalyDetected)
        {
            throw new InvalidOperationException("Cannot emit alert for non-anomaly result");
        }
        
        // Check deduplication
        if (await ShouldSuppressAsync(spec, result, ct))
        {
            logger.LogInformation(
                "Suppressing duplicate alert for monitor '{MonitorName}'",
                spec.Name);
            
            // Return a "suppressed" marker alert (not saved)
            return new AlertEvent
            {
                Id = Guid.Empty, // Indicates not saved
                Type = EventType.AiAnomaly,
                Severity = result.Severity ?? EventSeverity.Low,
                Timestamp = DateTimeOffset.UtcNow,
                Name = $"[SUPPRESSED] {BuildAlertName(spec, result)}",
                Summary = "Alert suppressed due to deduplication",
                Content = result.DetailedAnalysis ?? ""
            };
        }
        
        var alert = new AlertEvent
        {
            Id = Guid.NewGuid(),
            Type = EventType.AiAnomaly,
            Severity = result.Severity ?? EventSeverity.Low,
            Timestamp = DateTimeOffset.UtcNow,
            Name = BuildAlertName(spec, result),
            Summary = BuildAlertSummary(spec, result),
            Content = BuildAlertContent(spec, result)
        };
        
        dbContext.Events.Add(alert);
        await dbContext.SaveChangesAsync(ct);
        
        logger.LogInformation(
            "Emitted {Severity} alert {AlertId}: {Name}",
            alert.Severity, alert.Id, alert.Name);
        
        return alert;
    }
    
    public async Task<bool> ShouldSuppressAsync(
        MonitorSpec spec,
        AnomalyDetectionResult result,
        CancellationToken ct = default)
    {
        // Check for recent similar alerts from the same monitor
        var cutoff = DateTimeOffset.UtcNow - DedupeWindow;
        var alertPrefix = $"[{spec.Name}]";
        
        var recentSimilar = await dbContext.Events
            .Where(e => e.Type == EventType.AiAnomaly)
            .Where(e => e.Timestamp > cutoff)
            .Where(e => e.Name.StartsWith(alertPrefix))
            .AnyAsync(ct);
        
        return recentSimilar;
    }
    
    private static string BuildAlertName(MonitorSpec spec, AnomalyDetectionResult result)
    {
        // Format: [MonitorName] - Summary
        var summaryPart = result.Summary?.Length > 60
            ? result.Summary[..60] + "..."
            : result.Summary ?? "Anomaly Detected";
        
        return $"[{spec.Name}] {summaryPart}";
    }
    
    private static string BuildAlertSummary(MonitorSpec spec, AnomalyDetectionResult result)
    {
        if (!string.IsNullOrEmpty(result.Summary))
        {
            return result.Summary;
        }
        
        var findingCount = result.Findings.Count;
        return findingCount switch
        {
            0 => "Anomaly detected in monitoring analysis",
            1 => result.Findings[0].Description,
            _ => $"{findingCount} anomalies detected by {spec.Name}"
        };
    }
    
    private static string BuildAlertContent(MonitorSpec spec, AnomalyDetectionResult result)
    {
        var sb = new StringBuilder();
        
        // Header
        sb.AppendLine($"# {spec.Name} - Alert");
        sb.AppendLine();
        sb.AppendLine($"**Monitor:** {spec.Name}");
        sb.AppendLine($"**Severity:** {result.Severity}");
        sb.AppendLine($"**Generated:** {DateTimeOffset.UtcNow:yyyy-MM-dd HH:mm:ss} UTC");
        sb.AppendLine();
        
        // Summary
        if (!string.IsNullOrEmpty(result.Summary))
        {
            sb.AppendLine("## Summary");
            sb.AppendLine();
            sb.AppendLine(result.Summary);
            sb.AppendLine();
        }
        
        // Findings
        if (result.Findings.Count > 0)
        {
            sb.AppendLine("## Findings");
            sb.AppendLine();
            
            foreach (var finding in result.Findings)
            {
                sb.AppendLine($"### {finding.Description}");
                sb.AppendLine();
                
                sb.AppendLine($"- **Severity:** {finding.Severity}");
                
                if (finding.AffectedPeriod is not null)
                    sb.AppendLine($"- **Period:** {finding.AffectedPeriod}");
                
                if (finding.Evidence is not null)
                    sb.AppendLine($"- **Evidence:** {finding.Evidence}");
                
                sb.AppendLine();
            }
        }
        
        // Detailed Analysis
        if (!string.IsNullOrEmpty(result.DetailedAnalysis))
        {
            sb.AppendLine("## Detailed Analysis");
            sb.AppendLine();
            sb.AppendLine(result.DetailedAnalysis);
            sb.AppendLine();
        }
        
        // Warnings
        if (result.Warnings.Count > 0)
        {
            sb.AppendLine("## Warnings");
            sb.AppendLine();
            foreach (var warning in result.Warnings)
            {
                sb.AppendLine($"- {warning}");
            }
            sb.AppendLine();
        }
        
        return sb.ToString();
    }
    
    private static string GenerateFingerprint(MonitorSpec spec, AnomalyDetectionResult result)
    {
        // Create a fingerprint based on monitor name and severity
        var input = $"{spec.Name}|{result.Severity}|" +
                   string.Join(",", result.Findings.Select(f => f.Description).Distinct());
        
        var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(input));
        return Convert.ToHexString(bytes)[..16];
    }
}
```

## Files to Modify

### 3. `backend/Kds.Ai.AnomalyDetector/DependencyInjectionExtensions.cs`

Add emitter registration:

```csharp
// Register alert emitter
builder.Services.AddScoped<IAlertEmitter, AlertEmitter>();
```

## Alert Content Structure

The generated alert content follows this Markdown structure:

```markdown
# Prep Time Monitor - Downtown - Alert

**Monitor:** Prep Time Monitor - Downtown
**Severity:** High
**Generated:** 2025-01-15 14:30:00 UTC

## Summary

Prep times 45% above baseline during lunch rush (11:00-14:00)

## Findings

### Significant increase in average prep time during peak hours

- **Severity:** High
- **Period:** 2025-01-15 12:00-13:00
- **Evidence:** Average 450s vs baseline 310s (45% increase, 2.8 standard deviations above mean)

## Detailed Analysis

[LLM-generated detailed analysis...]

## Warnings

- Sample size for 13:00 hour was below minimum threshold
```

## Deduplication Logic

The deduplication prevents alert fatigue by:
1. Checking for recent alerts with the same monitor name prefix within the dedupe window (4 hours)
2. Suppressing if a similar alert exists
3. The fingerprint method is available for future enhanced similarity matching

### Changes from Old Design

- Dedup uses `spec.Name` prefix instead of `spec.MonitorType` + `spec.StoreGuid[..8]`
- `BuildAlertName` uses `[MonitorName] Summary` format instead of `[MonitorType] Store: xxx - Summary`
- `BuildAlertContent` uses `spec.Name` instead of `spec.StoreGuid` and `spec.MonitorType`
- Findings use `Description`, `Severity`, `AffectedPeriod`, `Evidence` instead of `Type`, `DeviationPercent`, `ZScore`, `Confidence`
- Class is `internal sealed` with primary constructor

## Acceptance Criteria

- [ ] `IAlertEmitter` interface created
- [ ] `AlertEmitter` implementation created (`internal sealed` with primary constructor)
- [ ] Alert name format: `[MonitorName] Summary`
- [ ] Alert content in Markdown format
- [ ] Deduplication logic uses `spec.Name` prefix
- [ ] Suppressed alerts return marker with `Id = Guid.Empty`
- [ ] No references to `MonitorType`, `StoreGuid`, `Type` (on findings), `DeviationPercent`, `ZScore`, `Confidence`
- [ ] Logging integrated
- [ ] Emitter registered in DI
- [ ] Solution builds without errors

## Notes

- The dedupe window (4 hours) can be made configurable
- Consider adding alert channels (email, Slack) in future
- May want to add alert acknowledgment/resolution tracking
- Fingerprinting could be more sophisticated (e.g., LSH for similarity)
