using System.Net; using AutopilotMonitor.Functions.Helpers; using AutopilotMonitor.Shared.DataAccess; using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Http; using Microsoft.Extensions.Logging; namespace AutopilotMonitor.Functions.Functions.Metrics { /// /// Functions for retrieving rule telemetry stats. /// Tenant-facing: per-tenant rule effectiveness metrics. /// Global admin: cross-tenant rule stats and adoption summaries. /// public class RuleStatsFunction { private readonly ILogger _logger; private readonly IMetricsRepository _metricsRepo; private readonly IHardwareRejectionNotificationTracker _notificationTracker; public RuleStatsFunction( ILogger logger, IMetricsRepository metricsRepo, IHardwareRejectionNotificationTracker notificationTracker) { _logger = logger; _metricsRepo = metricsRepo; _notificationTracker = notificationTracker; } /// /// GET /api/metrics/rule-stats?startDate=...&endDate=...&ruleType=analyze /// Returns rule stats for the caller's tenant (MemberRead). /// [Function("GetRuleStats")] public async Task GetTenantRuleStats( [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "metrics/rule-stats")] HttpRequestData req) { try { string tenantId = TenantHelper.GetTenantId(req); var startDate = req.Query["startDate"] ?? DateTime.UtcNow.AddDays(-30).ToString("yyyy-MM-dd"); var endDate = req.Query["endDate"] ?? DateTime.UtcNow.ToString("yyyy-MM-dd"); var ruleType = req.Query["ruleType"]; _logger.LogInformation("Rule stats requested for tenant {TenantId} ({StartDate} to {EndDate})", tenantId, startDate, endDate); var entries = await _metricsRepo.GetRuleStatsAsync(tenantId, startDate, endDate, ruleType); // Aggregate across dates per ruleId var aggregated = entries .GroupBy(e => e.RuleId) .Select(g => new { ruleId = g.Key, ruleType = g.First().RuleType, ruleTitle = g.First().RuleTitle, category = g.First().Category, severity = g.First().Severity, fireCount = g.Sum(e => e.FireCount), evaluationCount = g.Sum(e => e.EvaluationCount), sessionsEvaluated = g.Sum(e => e.SessionsEvaluated), hitRate = g.Sum(e => e.EvaluationCount) > 0 ? Math.Round(100.0 * g.Sum(e => e.FireCount) / g.Sum(e => e.EvaluationCount), 1) : 0.0, avgConfidenceScore = g.Sum(e => e.FireCount) > 0 ? Math.Round((double)g.Sum(e => e.ConfidenceScoreSum) / g.Sum(e => e.FireCount), 1) : 0.0, trend = g.OrderBy(e => e.Date).Select(e => new { date = e.Date, fireCount = e.FireCount, evaluationCount = e.EvaluationCount }) }) .OrderByDescending(r => r.fireCount) .ToList(); var totalEvaluations = aggregated.Sum(r => r.evaluationCount); var totalFires = aggregated.Sum(r => r.fireCount); var result = new { rules = aggregated, // F3 PR6: active regression episodes (tracker rows) — the rules-page badge // and MCP get_rule_stats read them from here. Empty list = nothing regressed. regressions = await _notificationTracker.GetRuleRegressionsAsync(tenantId), summary = new { totalEvaluations, totalFires, overallHitRate = totalEvaluations > 0 ? Math.Round(100.0 * totalFires / totalEvaluations, 1) : 0.0, topRuleByFireCount = aggregated.FirstOrDefault()?.ruleId, period = new { start = startDate, end = endDate } } }; var response = req.CreateResponse(HttpStatusCode.OK); await response.WriteAsJsonAsync(result); return response; } catch (Exception ex) { _logger.LogError(ex, "Error fetching tenant rule stats"); var errorResponse = req.CreateResponse(HttpStatusCode.InternalServerError); await errorResponse.WriteAsJsonAsync(new { success = false, message = "Failed to fetch rule stats" }); return errorResponse; } } /// /// GET /api/global/metrics/rule-stats?startDate=...&endDate=...&ruleType=analyze&tenantId=... /// Returns cross-tenant global rule stats (Global Admin only). /// When tenantId is provided, returns tenant-specific stats instead of global aggregates. /// [Function("GetGlobalRuleStats")] public async Task GetGlobalRuleStats( [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "global/metrics/rule-stats")] HttpRequestData req) { try { var startDate = req.Query["startDate"] ?? DateTime.UtcNow.AddDays(-30).ToString("yyyy-MM-dd"); var endDate = req.Query["endDate"] ?? DateTime.UtcNow.ToString("yyyy-MM-dd"); var ruleType = req.Query["ruleType"]; var tenantId = req.Query["tenantId"]; // If tenantId specified, return tenant-specific stats; otherwise global aggregates var queryTenantId = !string.IsNullOrEmpty(tenantId) ? tenantId : "global"; _logger.LogInformation("Global rule stats requested for {Scope} ({StartDate} to {EndDate})", queryTenantId, startDate, endDate); var entries = await _metricsRepo.GetRuleStatsAsync(queryTenantId, startDate, endDate, ruleType); var aggregated = entries .GroupBy(e => e.RuleId) .Select(g => new { ruleId = g.Key, ruleType = g.First().RuleType, ruleTitle = g.First().RuleTitle, category = g.First().Category, severity = g.First().Severity, fireCount = g.Sum(e => e.FireCount), evaluationCount = g.Sum(e => e.EvaluationCount), sessionsEvaluated = g.Sum(e => e.SessionsEvaluated), hitRate = g.Sum(e => e.EvaluationCount) > 0 ? Math.Round(100.0 * g.Sum(e => e.FireCount) / g.Sum(e => e.EvaluationCount), 1) : 0.0, avgConfidenceScore = g.Sum(e => e.FireCount) > 0 ? Math.Round((double)g.Sum(e => e.ConfidenceScoreSum) / g.Sum(e => e.FireCount), 1) : 0.0, trend = g.OrderBy(e => e.Date).Select(e => new { date = e.Date, fireCount = e.FireCount, evaluationCount = e.EvaluationCount }) }) .OrderByDescending(r => r.fireCount) .ToList(); var totalEvaluations = aggregated.Sum(r => r.evaluationCount); var totalFires = aggregated.Sum(r => r.fireCount); var result = new { rules = aggregated, // Regression episodes are tenant-scoped: present when drilling into one // tenant, empty for the cross-tenant "global" aggregate (no episode there). regressions = queryTenantId != "global" ? await _notificationTracker.GetRuleRegressionsAsync(queryTenantId) : new List(), summary = new { totalEvaluations, totalFires, overallHitRate = totalEvaluations > 0 ? Math.Round(100.0 * totalFires / totalEvaluations, 1) : 0.0, topRuleByFireCount = aggregated.FirstOrDefault()?.ruleId, uniqueRules = aggregated.Count, period = new { start = startDate, end = endDate } } }; var response = req.CreateResponse(HttpStatusCode.OK); await response.WriteAsJsonAsync(result); return response; } catch (Exception ex) { _logger.LogError(ex, "Error fetching global rule stats"); var errorResponse = req.CreateResponse(HttpStatusCode.InternalServerError); await errorResponse.WriteAsJsonAsync(new { success = false, message = "Failed to fetch global rule stats" }); return errorResponse; } } } }