using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using AutopilotMonitor.Shared.Pagination;
namespace AutopilotMonitor.Shared.DataAccess
{
///
/// Repository for operational events (OpsEvents table).
/// Stores vital infrastructure events visible to Global Admins in the Ops dashboard.
///
public interface IOpsEventRepository
{
Task SaveOpsEventAsync(OpsEventEntry entry);
///
/// Returns all matching ops events in the given UTC window.
/// is optional — when provided, scopes to a single PartitionKey for an
/// indexed lookup. Sorted newest-first. No row cap; for unbounded
/// windows on busy installations, prefer .
/// narrows on non-key columns server-side.
///
Task> GetOpsEventsAsync(
string? category = null, DateTime? dateFrom = null, DateTime? dateTo = null,
OpsEventQueryFilters? filters = null);
///
/// Reads a single page of ops events. The returned
/// carries the underlying store's opaque continuation token; null
/// when this page was the last. Items in each page are sorted
/// newest-first.
///
Task> GetOpsEventsPageAsync(
string? category, DateTime? dateFrom, DateTime? dateTo, int pageSize, string? continuation,
OpsEventQueryFilters? filters = null);
Task DeleteOpsEventsOlderThanAsync(DateTime cutoff);
}
///
/// Categories for operational events.
///
public static class OpsEventCategory
{
public const string Consent = "Consent";
public const string Maintenance = "Maintenance";
public const string Security = "Security";
public const string Tenant = "Tenant";
public const string Agent = "Agent";
public const string Sla = "SLA";
/// Platform infrastructure alerts relayed from Azure Monitor (ops alert webhook).
public const string Platform = "Platform";
///
/// Every category = every OpsEvents PartitionKey. The cross-category paged read fans out
/// over exactly this list, so a category missing here is INVISIBLE to every paged reader
/// (MCP, the ops dashboard) while still being written — Platform was missing that way.
/// OpsEventCategoryCoverageTests reflects over the constants above and fails if a new one
/// is not listed here.
///
public static readonly string[] All = { Consent, Maintenance, Security, Tenant, Agent, Sla, Platform };
}
///
/// Severity levels for operational events.
///
public static class OpsEventSeverity
{
public const string Info = "Info";
public const string Warning = "Warning";
public const string Error = "Error";
public const string Critical = "Critical";
/// Canonical severities, ascending by .
public static readonly string[] All = { Info, Warning, Error, Critical };
///
/// Ordinal severity rank (Info=0 … Critical=3), -1 for anything outside the vocabulary.
/// Case-SENSITIVE on purpose: written severities always come from the constants above, and
/// the ops-alert dispatch has always compared them verbatim. Query input goes through
/// first.
///
public static int Rank(string? severity) => severity switch
{
Info => 0,
Warning => 1,
Error => 2,
Critical => 3,
_ => -1,
};
///
/// Maps caller-supplied text ("warning", "ERROR") onto the canonical constant. Returns false
/// for anything outside the vocabulary so an endpoint can 400 instead of silently returning
/// an empty page — Table Storage eq is case-sensitive, so an unnormalized "warning"
/// would match nothing at all.
///
public static bool TryNormalize(string? value, out string canonical)
{
foreach (var s in All)
{
if (string.Equals(s, value, StringComparison.OrdinalIgnoreCase))
{
canonical = s;
return true;
}
}
canonical = string.Empty;
return false;
}
/// Canonical severities at or above (empty for an unknown value).
public static IReadOnlyList AtOrAbove(string minSeverity)
{
var min = Rank(minSeverity);
if (min < 0) return new string[0];
var result = new List();
foreach (var s in All)
{
if (Rank(s) >= min) result.Add(s);
}
return result;
}
}
///
/// Optional exact-match field filters for the ops-events reads. Every field maps to a
/// SERVER-SIDE OData clause (see TableOpsEventRepository.BuildFilter) — the point is that the
/// store returns only matching rows, so an operator asking for one event type does not pull a
/// whole category over the wire. Mirrors AuditLogQueryFilters.
///
public class OpsEventQueryFilters
{
/// Exact match on the EventType column (e.g. "AgentEmergencyBreak"). Case-sensitive — Table Storage cannot fold case.
public string? EventType { get; set; }
/// Exact match on the Severity column. Callers should pass a canonical value.
public string? Severity { get; set; }
/// Threshold match: every severity at or above this one (Info < Warning < Error < Critical). Same vocabulary the ops-alert rules use.
public string? MinSeverity { get; set; }
/// True when no filter field is set — callers can skip the filter plumbing entirely.
public bool IsEmpty =>
string.IsNullOrEmpty(EventType) &&
string.IsNullOrEmpty(Severity) &&
string.IsNullOrEmpty(MinSeverity);
}
public class OpsEventEntry
{
public string Id { get; set; } = string.Empty;
public string Category { get; set; } = string.Empty;
public string EventType { get; set; } = string.Empty;
public string Severity { get; set; } = OpsEventSeverity.Info;
public string? TenantId { get; set; }
public string? UserId { get; set; }
public string Message { get; set; } = string.Empty;
public string? Details { get; set; }
public DateTime Timestamp { get; set; }
}
}