using Microsoft.AspNetCore.Mvc; using StellaOps.Notifier.Worker.Correlation; namespace StellaOps.Notifier.WebService.Endpoints; /// /// API endpoints for quiet hours calendar management. /// public static class QuietHoursEndpoints { /// /// Maps quiet hours endpoints. /// public static IEndpointRouteBuilder MapQuietHoursEndpoints(this IEndpointRouteBuilder app) { var group = app.MapGroup("/api/v2/quiet-hours") .WithTags("QuietHours") .WithOpenApi(); group.MapGet("/calendars", ListCalendarsAsync) .WithName("ListQuietHoursCalendars") .WithSummary("List all quiet hours calendars") .WithDescription("Returns all quiet hours calendars for the tenant."); group.MapGet("/calendars/{calendarId}", GetCalendarAsync) .WithName("GetQuietHoursCalendar") .WithSummary("Get a quiet hours calendar") .WithDescription("Returns a specific quiet hours calendar by ID."); group.MapPost("/calendars", CreateCalendarAsync) .WithName("CreateQuietHoursCalendar") .WithSummary("Create a quiet hours calendar") .WithDescription("Creates a new quiet hours calendar with schedules."); group.MapPut("/calendars/{calendarId}", UpdateCalendarAsync) .WithName("UpdateQuietHoursCalendar") .WithSummary("Update a quiet hours calendar") .WithDescription("Updates an existing quiet hours calendar."); group.MapDelete("/calendars/{calendarId}", DeleteCalendarAsync) .WithName("DeleteQuietHoursCalendar") .WithSummary("Delete a quiet hours calendar") .WithDescription("Deletes a quiet hours calendar."); group.MapPost("/evaluate", EvaluateAsync) .WithName("EvaluateQuietHours") .WithSummary("Evaluate quiet hours") .WithDescription("Checks if quiet hours are currently active for an event kind."); return app; } private static async Task ListCalendarsAsync( [FromHeader(Name = "X-Tenant-Id")] string? tenantId, [FromServices] IQuietHoursCalendarService calendarService, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(tenantId)) { return Results.BadRequest(new { error = "X-Tenant-Id header is required." }); } var calendars = await calendarService.ListCalendarsAsync(tenantId, cancellationToken); return Results.Ok(calendars.Select(MapToApiResponse)); } private static async Task GetCalendarAsync( string calendarId, [FromHeader(Name = "X-Tenant-Id")] string? tenantId, [FromServices] IQuietHoursCalendarService calendarService, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(tenantId)) { return Results.BadRequest(new { error = "X-Tenant-Id header is required." }); } var calendar = await calendarService.GetCalendarAsync(tenantId, calendarId, cancellationToken); if (calendar is null) { return Results.NotFound(new { error = $"Calendar '{calendarId}' not found." }); } return Results.Ok(MapToApiResponse(calendar)); } private static async Task CreateCalendarAsync( [FromBody] QuietHoursCalendarApiRequest request, [FromHeader(Name = "X-Tenant-Id")] string? tenantIdHeader, [FromHeader(Name = "X-Actor")] string? actor, [FromServices] IQuietHoursCalendarService calendarService, CancellationToken cancellationToken) { var tenantId = request.TenantId ?? tenantIdHeader; if (string.IsNullOrWhiteSpace(tenantId)) { return Results.BadRequest(new { error = "Tenant ID is required via X-Tenant-Id header or request body." }); } if (string.IsNullOrWhiteSpace(request.Name)) { return Results.BadRequest(new { error = "Calendar name is required." }); } if (request.Schedules is null || request.Schedules.Count == 0) { return Results.BadRequest(new { error = "At least one schedule is required." }); } var calendarId = request.CalendarId ?? Guid.NewGuid().ToString("N")[..16]; var calendar = new QuietHoursCalendar { CalendarId = calendarId, TenantId = tenantId, Name = request.Name, Description = request.Description, Enabled = request.Enabled ?? true, Priority = request.Priority ?? 100, Schedules = request.Schedules.Select(MapToScheduleEntry).ToList(), ExcludedEventKinds = request.ExcludedEventKinds, IncludedEventKinds = request.IncludedEventKinds }; var created = await calendarService.UpsertCalendarAsync(calendar, actor, cancellationToken); return Results.Created($"/api/v2/quiet-hours/calendars/{created.CalendarId}", MapToApiResponse(created)); } private static async Task UpdateCalendarAsync( string calendarId, [FromBody] QuietHoursCalendarApiRequest request, [FromHeader(Name = "X-Tenant-Id")] string? tenantIdHeader, [FromHeader(Name = "X-Actor")] string? actor, [FromServices] IQuietHoursCalendarService calendarService, CancellationToken cancellationToken) { var tenantId = request.TenantId ?? tenantIdHeader; if (string.IsNullOrWhiteSpace(tenantId)) { return Results.BadRequest(new { error = "Tenant ID is required via X-Tenant-Id header or request body." }); } var existing = await calendarService.GetCalendarAsync(tenantId, calendarId, cancellationToken); if (existing is null) { return Results.NotFound(new { error = $"Calendar '{calendarId}' not found." }); } var calendar = new QuietHoursCalendar { CalendarId = calendarId, TenantId = tenantId, Name = request.Name ?? existing.Name, Description = request.Description ?? existing.Description, Enabled = request.Enabled ?? existing.Enabled, Priority = request.Priority ?? existing.Priority, Schedules = request.Schedules?.Select(MapToScheduleEntry).ToList() ?? existing.Schedules, ExcludedEventKinds = request.ExcludedEventKinds ?? existing.ExcludedEventKinds, IncludedEventKinds = request.IncludedEventKinds ?? existing.IncludedEventKinds, CreatedAt = existing.CreatedAt, CreatedBy = existing.CreatedBy }; var updated = await calendarService.UpsertCalendarAsync(calendar, actor, cancellationToken); return Results.Ok(MapToApiResponse(updated)); } private static async Task DeleteCalendarAsync( string calendarId, [FromHeader(Name = "X-Tenant-Id")] string? tenantId, [FromHeader(Name = "X-Actor")] string? actor, [FromServices] IQuietHoursCalendarService calendarService, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(tenantId)) { return Results.BadRequest(new { error = "X-Tenant-Id header is required." }); } var deleted = await calendarService.DeleteCalendarAsync(tenantId, calendarId, actor, cancellationToken); if (!deleted) { return Results.NotFound(new { error = $"Calendar '{calendarId}' not found." }); } return Results.NoContent(); } private static async Task EvaluateAsync( [FromBody] QuietHoursEvaluateApiRequest request, [FromHeader(Name = "X-Tenant-Id")] string? tenantIdHeader, [FromServices] IQuietHoursCalendarService calendarService, CancellationToken cancellationToken) { var tenantId = request.TenantId ?? tenantIdHeader; if (string.IsNullOrWhiteSpace(tenantId)) { return Results.BadRequest(new { error = "Tenant ID is required via X-Tenant-Id header or request body." }); } if (string.IsNullOrWhiteSpace(request.EventKind)) { return Results.BadRequest(new { error = "Event kind is required." }); } var result = await calendarService.EvaluateAsync( tenantId, request.EventKind, request.EvaluationTime, cancellationToken); return Results.Ok(new QuietHoursEvaluateApiResponse { IsActive = result.IsActive, MatchedCalendarId = result.MatchedCalendarId, MatchedCalendarName = result.MatchedCalendarName, MatchedScheduleName = result.MatchedScheduleName, EndsAt = result.EndsAt, Reason = result.Reason }); } private static QuietHoursScheduleEntry MapToScheduleEntry(QuietHoursScheduleApiRequest request) => new() { Name = request.Name ?? "Unnamed Schedule", StartTime = request.StartTime ?? "00:00", EndTime = request.EndTime ?? "00:00", DaysOfWeek = request.DaysOfWeek, Timezone = request.Timezone, Enabled = request.Enabled ?? true }; private static QuietHoursCalendarApiResponse MapToApiResponse(QuietHoursCalendar calendar) => new() { CalendarId = calendar.CalendarId, TenantId = calendar.TenantId, Name = calendar.Name, Description = calendar.Description, Enabled = calendar.Enabled, Priority = calendar.Priority, Schedules = calendar.Schedules.Select(s => new QuietHoursScheduleApiResponse { Name = s.Name, StartTime = s.StartTime, EndTime = s.EndTime, DaysOfWeek = s.DaysOfWeek?.ToList(), Timezone = s.Timezone, Enabled = s.Enabled }).ToList(), ExcludedEventKinds = calendar.ExcludedEventKinds?.ToList(), IncludedEventKinds = calendar.IncludedEventKinds?.ToList(), CreatedAt = calendar.CreatedAt, CreatedBy = calendar.CreatedBy, UpdatedAt = calendar.UpdatedAt, UpdatedBy = calendar.UpdatedBy }; } #region API Request/Response Models /// /// Request to create or update a quiet hours calendar. /// public sealed class QuietHoursCalendarApiRequest { public string? CalendarId { get; set; } public string? TenantId { get; set; } public string? Name { get; set; } public string? Description { get; set; } public bool? Enabled { get; set; } public int? Priority { get; set; } public List? Schedules { get; set; } public List? ExcludedEventKinds { get; set; } public List? IncludedEventKinds { get; set; } } /// /// Schedule entry in a quiet hours calendar request. /// public sealed class QuietHoursScheduleApiRequest { public string? Name { get; set; } public string? StartTime { get; set; } public string? EndTime { get; set; } public List? DaysOfWeek { get; set; } public string? Timezone { get; set; } public bool? Enabled { get; set; } } /// /// Request to evaluate quiet hours. /// public sealed class QuietHoursEvaluateApiRequest { public string? TenantId { get; set; } public string? EventKind { get; set; } public DateTimeOffset? EvaluationTime { get; set; } } /// /// Response for a quiet hours calendar. /// public sealed class QuietHoursCalendarApiResponse { public required string CalendarId { get; set; } public required string TenantId { get; set; } public required string Name { get; set; } public string? Description { get; set; } public required bool Enabled { get; set; } public required int Priority { get; set; } public required List Schedules { get; set; } public List? ExcludedEventKinds { get; set; } public List? IncludedEventKinds { get; set; } public DateTimeOffset CreatedAt { get; set; } public string? CreatedBy { get; set; } public DateTimeOffset UpdatedAt { get; set; } public string? UpdatedBy { get; set; } } /// /// Schedule entry in a quiet hours calendar response. /// public sealed class QuietHoursScheduleApiResponse { public required string Name { get; set; } public required string StartTime { get; set; } public required string EndTime { get; set; } public List? DaysOfWeek { get; set; } public string? Timezone { get; set; } public required bool Enabled { get; set; } } /// /// Response for quiet hours evaluation. /// public sealed class QuietHoursEvaluateApiResponse { public required bool IsActive { get; set; } public string? MatchedCalendarId { get; set; } public string? MatchedCalendarName { get; set; } public string? MatchedScheduleName { get; set; } public DateTimeOffset? EndsAt { get; set; } public string? Reason { get; set; } } #endregion