# TASK 3.2: Implement EfMonitorSpecRepository

## Summary

Implement the `IMonitorSpecRepository` interface using Entity Framework Core for database access. The implementation lives in the Data project alongside the DbContext and entities it operates on.

## Dependencies

- TASK 1.1 (MonitorSpec entity)
- TASK 1.3 (Migration applied)
- TASK 3.1 (IMonitorSpecRepository interface)

## Files to Create

### 1. `backend/Kds.Ai.Data/Repositories/EfMonitorSpecRepository.cs`

```csharp
using Kds.Ai.Data.Entities;
using Kds.Ai.Data.Enums;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace Kds.Ai.Data.Repositories;

/// <summary>
/// Entity Framework Core implementation of IMonitorSpecRepository
/// </summary>
public sealed class EfMonitorSpecRepository : IMonitorSpecRepository
{
    private readonly KdsAiDbContext _dbContext;
    private readonly ILogger<EfMonitorSpecRepository> _logger;
    
    public EfMonitorSpecRepository(
        KdsAiDbContext dbContext,
        ILogger<EfMonitorSpecRepository> logger)
    {
        _dbContext = dbContext;
        _logger = logger;
    }
    
    public async Task<MonitorSpec?> GetByIdAsync(Guid id, CancellationToken ct = default)
    {
        return await _dbContext.MonitorSpecs
            .FirstOrDefaultAsync(x => x.Id == id, ct);
    }
    
    public async Task<MonitorSpec?> GetByNameAsync(string name, CancellationToken ct = default)
    {
        return await _dbContext.MonitorSpecs
            .FirstOrDefaultAsync(x => x.Name == name, ct);
    }
    
    public async Task<IReadOnlyList<MonitorSpec>> GetByStateAsync(
        MonitorState state, 
        CancellationToken ct = default)
    {
        return await _dbContext.MonitorSpecs
            .Where(x => x.State == state)
            .OrderBy(x => x.Name)
            .ToListAsync(ct);
    }
    
    public async Task<IReadOnlyList<MonitorSpec>> GetActiveAsync(CancellationToken ct = default)
    {
        return await GetByStateAsync(MonitorState.Active, ct);
    }
    
    public async Task<IReadOnlyList<MonitorSpec>> GetAllAsync(CancellationToken ct = default)
    {
        return await _dbContext.MonitorSpecs
            .OrderBy(x => x.Name)
            .ToListAsync(ct);
    }
    
    public async Task<MonitorSpec> CreateAsync(MonitorSpec spec, CancellationToken ct = default)
    {
        if (spec.Id == Guid.Empty)
        {
            spec.Id = Guid.NewGuid();
        }
        
        spec.CreatedAt = DateTimeOffset.UtcNow;
        
        _dbContext.MonitorSpecs.Add(spec);
        await _dbContext.SaveChangesAsync(ct);
        
        _logger.LogInformation(
            "Created MonitorSpec {Id} '{Name}' in state {State}",
            spec.Id, spec.Name, spec.State);
        
        return spec;
    }
    
    public async Task<MonitorSpec> UpdateAsync(MonitorSpec spec, CancellationToken ct = default)
    {
        spec.UpdatedAt = DateTimeOffset.UtcNow;
        
        _dbContext.MonitorSpecs.Update(spec);
        await _dbContext.SaveChangesAsync(ct);
        
        _logger.LogInformation("Updated MonitorSpec {Id}", spec.Id);
        
        return spec;
    }
    
    public async Task UpdateLastRunAsync(Guid id, DateTimeOffset lastRunAt, CancellationToken ct = default)
    {
        var affected = await _dbContext.MonitorSpecs
            .Where(x => x.Id == id)
            .ExecuteUpdateAsync(setters => setters
                .SetProperty(x => x.LastRunAt, lastRunAt)
                .SetProperty(x => x.UpdatedAt, DateTimeOffset.UtcNow), ct);
        
        if (affected == 0)
        {
            _logger.LogWarning("MonitorSpec {Id} not found for LastRunAt update", id);
        }
    }
    
    public async Task SetStateAsync(Guid id, MonitorState state, CancellationToken ct = default)
    {
        var affected = await _dbContext.MonitorSpecs
            .Where(x => x.Id == id)
            .ExecuteUpdateAsync(setters => setters
                .SetProperty(x => x.State, state)
                .SetProperty(x => x.UpdatedAt, DateTimeOffset.UtcNow), ct);
        
        if (affected == 0)
        {
            _logger.LogWarning("MonitorSpec {Id} not found for state transition", id);
        }
        else
        {
            _logger.LogInformation("MonitorSpec {Id} transitioned to {State}", id, state);
        }
    }
    
    public async Task<bool> DeleteAsync(Guid id, CancellationToken ct = default)
    {
        var affected = await _dbContext.MonitorSpecs
            .Where(x => x.Id == id)
            .ExecuteDeleteAsync(ct);
        
        if (affected > 0)
        {
            _logger.LogInformation("Deleted MonitorSpec {Id}", id);
            return true;
        }
        
        _logger.LogWarning("MonitorSpec {Id} not found for deletion", id);
        return false;
    }
    
    public async Task<bool> ExistsByNameAsync(string name, CancellationToken ct = default)
    {
        return await _dbContext.MonitorSpecs
            .AnyAsync(x => x.Name == name, ct);
    }
}
```

## Files to Modify

### 2. `backend/Kds.Ai.Data/DependencyInjectionExtensions.cs`

Add repository registration to the existing `AddDataModule` method:

```csharp
using Kds.Ai.Data.Repositories;

// Inside AddDataModule, after the AddDbContext call:

// Register repositories
builder.Services.AddScoped<IMonitorSpecRepository, EfMonitorSpecRepository>();
```

The full method should look like:

```csharp
public static IHostApplicationBuilder AddDataModule(this IHostApplicationBuilder builder, string connectionName = "kdsaidb")
{
    builder.Services.AddDbContext<KdsAiDbContext>(options =>
    {
        var connectionString = builder.Configuration.GetConnectionString(connectionName) ?? "Server=localhost;Database=dummy;";
        options.UseMySQL(connectionString);
    });
    
    // Register repositories
    builder.Services.AddScoped<IMonitorSpecRepository, EfMonitorSpecRepository>();
    
    return builder;
}
```

## Implementation Details

### Why Kds.Ai.Data?

The EF Core repository implementation belongs in the Data project because:

- It directly depends on `KdsAiDbContext` defined in the same project
- It uses only types from the Data project (entities, enums)
- No external dependencies beyond what the Data project already has (`Microsoft.EntityFrameworkCore`, `Microsoft.Extensions.Logging`)
- DI registration in `AddDataModule` ensures the repository is available to any project that calls `AddDataModule`, without requiring them to know about the implementation details
- Keeps the AnomalyDetector project focused on domain logic, not data access plumbing

### ExecuteUpdateAsync / ExecuteDeleteAsync

- Uses EF Core 7+ bulk update/delete operations
- More efficient than loading entity then saving
- Returns affected row count for existence checking

### Timestamps

- `CreatedAt` set on create
- `UpdatedAt` set on update operations
- UTC times used consistently

### Logging

- Info level for successful operations
- Warning level for not-found scenarios

### Ordering

- Results ordered by `Name` for predictable iteration

## Acceptance Criteria

- [ ] `EfMonitorSpecRepository` class created in `Kds.Ai.Data/Repositories/` (sealed)
- [ ] Namespace is `Kds.Ai.Data.Repositories`
- [ ] All interface methods implemented
- [ ] Uses `MonitorState` enum (not `bool IsEnabled`)
- [ ] No references to `StoreGuid` or `MonitorType`
- [ ] Uses `ExecuteUpdateAsync` for partial updates
- [ ] Uses `ExecuteDeleteAsync` for bulk delete
- [ ] Timestamps managed correctly
- [ ] Logging integrated
- [ ] Repository registered in `AddDataModule` in `Kds.Ai.Data/DependencyInjectionExtensions.cs`
- [ ] No repository registration needed in AnomalyDetector's DI
- [ ] Solution builds without errors

## Testing Considerations

```csharp
// Example usage in tests - resolve from any project that calls AddDataModule
var repo = serviceProvider.GetRequiredService<IMonitorSpecRepository>();

// Create
var spec = await repo.CreateAsync(new MonitorSpec
{
    Name = "Prep Time Monitor - Downtown",
    SystemPrompt = "You are monitoring prep times...",
    State = MonitorState.Active
});

// Query
var active = await repo.GetActiveAsync();
Assert.Contains(active, s => s.Name == "Prep Time Monitor - Downtown");

// State transition
await repo.SetStateAsync(spec.Id, MonitorState.Paused);

// Delete
var deleted = await repo.DeleteAsync(spec.Id);
Assert.True(deleted);
```

## Notes

- The implementation assumes a scoped `KdsAiDbContext` from DI (registered by `AddDataModule`)
- No explicit transactions - rely on EF Core's default behavior
- Consider adding concurrency token if needed
- Consumer projects (e.g., AnomalyDetector) inject `IMonitorSpecRepository` - they don't need to know it's EF Core behind the scenes
