using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using LittleShop.Data; using LittleShop.DTOs; using LittleShop.Models; namespace LittleShop.Services; public class BotMetricsService : IBotMetricsService { private readonly LittleShopContext _context; private readonly ILogger _logger; public BotMetricsService(LittleShopContext context, ILogger logger) { _context = context; _logger = logger; } // Metrics Methods public async Task RecordMetricAsync(Guid botId, CreateBotMetricDto dto) { var metric = new BotMetric { Id = Guid.NewGuid(), BotId = botId, MetricType = dto.MetricType, Value = dto.Value, Category = dto.Category, Description = dto.Description, Metadata = JsonSerializer.Serialize(dto.Metadata), RecordedAt = DateTime.UtcNow }; _context.BotMetrics.Add(metric); await _context.SaveChangesAsync(); return MapMetricToDto(metric); } public async Task RecordMetricsBatchAsync(Guid botId, BotMetricsBatchDto dto) { try { var metrics = dto.Metrics.Select(m => new BotMetric { Id = Guid.NewGuid(), BotId = botId, MetricType = m.MetricType, Value = m.Value, Category = m.Category, Description = m.Description, Metadata = JsonSerializer.Serialize(m.Metadata), RecordedAt = DateTime.UtcNow }).ToList(); _context.BotMetrics.AddRange(metrics); await _context.SaveChangesAsync(); _logger.LogInformation("Recorded {Count} metrics for bot {BotId}", metrics.Count, botId); return true; } catch (Exception ex) { _logger.LogError(ex, "Failed to record metrics batch for bot {BotId}", botId); return false; } } public async Task> GetBotMetricsAsync(Guid botId, DateTime? startDate = null, DateTime? endDate = null) { var query = _context.BotMetrics.Where(m => m.BotId == botId); if (startDate.HasValue) query = query.Where(m => m.RecordedAt >= startDate.Value); if (endDate.HasValue) query = query.Where(m => m.RecordedAt <= endDate.Value); var metrics = await query .OrderByDescending(m => m.RecordedAt) .Take(1000) // Limit to prevent large results .ToListAsync(); return metrics.Select(MapMetricToDto); } public async Task GetMetricsSummaryAsync(Guid botId, DateTime? startDate = null, DateTime? endDate = null) { var bot = await _context.Bots.FindAsync(botId); if (bot == null) return new BotMetricsSummaryDto { BotId = botId }; var start = startDate ?? DateTime.UtcNow.AddDays(-30); var end = endDate ?? DateTime.UtcNow; var metrics = await _context.BotMetrics .Where(m => m.BotId == botId && m.RecordedAt >= start && m.RecordedAt <= end) .ToListAsync(); var sessions = await _context.BotSessions .Where(s => s.BotId == botId && s.StartedAt >= start && s.StartedAt <= end) .ToListAsync(); var summary = new BotMetricsSummaryDto { BotId = botId, BotName = bot.Name, PeriodStart = start, PeriodEnd = end, TotalSessions = sessions.Count, UniqueSessions = sessions.Select(s => s.SessionIdentifier).Distinct().Count(), TotalOrders = sessions.Sum(s => s.OrderCount), TotalRevenue = sessions.Sum(s => s.TotalSpent), TotalMessages = sessions.Sum(s => s.MessageCount), TotalErrors = (int)metrics.Where(m => m.MetricType == Enums.MetricType.Error).Sum(m => m.Value) }; // Calculate average response time var responseTimeMetrics = metrics.Where(m => m.MetricType == Enums.MetricType.ResponseTime).ToList(); if (responseTimeMetrics.Any()) summary.AverageResponseTime = responseTimeMetrics.Average(m => m.Value); // Calculate uptime percentage var uptimeMetrics = metrics.Where(m => m.MetricType == Enums.MetricType.Uptime).ToList(); if (uptimeMetrics.Any()) { var totalPossibleUptime = (end - start).TotalMinutes; var actualUptime = uptimeMetrics.Sum(m => m.Value); summary.UptimePercentage = (actualUptime / (decimal)totalPossibleUptime) * 100; } // Group metrics by type summary.MetricsByType = metrics .GroupBy(m => m.MetricType.ToString()) .ToDictionary(g => g.Key, g => g.Sum(m => m.Value)); // Generate time series data (daily aggregation) summary.TimeSeries = GenerateTimeSeries(metrics, start, end); return summary; } // Session Methods public async Task StartSessionAsync(Guid botId, CreateBotSessionDto dto) { var session = new BotSession { Id = Guid.NewGuid(), BotId = botId, SessionIdentifier = dto.SessionIdentifier, Platform = dto.Platform, StartedAt = DateTime.UtcNow, LastActivityAt = DateTime.UtcNow, Language = dto.Language, Country = dto.Country, IsAnonymous = dto.IsAnonymous, Metadata = JsonSerializer.Serialize(dto.Metadata), OrderCount = 0, MessageCount = 0, TotalSpent = 0 }; _context.BotSessions.Add(session); await _context.SaveChangesAsync(); _logger.LogInformation("Started session {SessionId} for bot {BotId}", session.Id, botId); return MapSessionToDto(session); } public async Task UpdateSessionAsync(Guid sessionId, UpdateBotSessionDto dto) { var session = await _context.BotSessions.FindAsync(sessionId); if (session == null) return false; session.LastActivityAt = DateTime.UtcNow; if (dto.OrderCount.HasValue) session.OrderCount = dto.OrderCount.Value; if (dto.MessageCount.HasValue) session.MessageCount = dto.MessageCount.Value; if (dto.TotalSpent.HasValue) session.TotalSpent = dto.TotalSpent.Value; if (dto.EndSession.HasValue && dto.EndSession.Value) session.EndedAt = DateTime.UtcNow; if (dto.Metadata != null) session.Metadata = JsonSerializer.Serialize(dto.Metadata); await _context.SaveChangesAsync(); return true; } public async Task GetSessionAsync(Guid sessionId) { var session = await _context.BotSessions.FindAsync(sessionId); return session != null ? MapSessionToDto(session) : null; } public async Task> GetBotSessionsAsync(Guid botId, bool activeOnly = false) { var query = _context.BotSessions.Where(s => s.BotId == botId); if (activeOnly) query = query.Where(s => !s.EndedAt.HasValue); var sessions = await query .OrderByDescending(s => s.StartedAt) .Take(100) .ToListAsync(); return sessions.Select(MapSessionToDto); } public async Task GetSessionSummaryAsync(Guid botId, DateTime? startDate = null, DateTime? endDate = null) { var query = _context.BotSessions.Where(s => s.BotId == botId); if (startDate.HasValue) query = query.Where(s => s.StartedAt >= startDate.Value); if (endDate.HasValue) query = query.Where(s => s.StartedAt <= endDate.Value); var sessions = await query.ToListAsync(); var summary = new BotSessionSummaryDto { TotalSessions = sessions.Count, ActiveSessions = sessions.Count(s => !s.EndedAt.HasValue), CompletedSessions = sessions.Count(s => s.EndedAt.HasValue) }; if (sessions.Any()) { var completedSessions = sessions.Where(s => s.EndedAt.HasValue).ToList(); if (completedSessions.Any()) { summary.AverageSessionDuration = (decimal)completedSessions .Average(s => (s.EndedAt!.Value - s.StartedAt).TotalMinutes); } summary.AverageOrdersPerSession = (decimal)sessions.Average(s => s.OrderCount); summary.AverageSpendPerSession = sessions.Average(s => s.TotalSpent); summary.SessionsByPlatform = sessions .GroupBy(s => s.Platform) .ToDictionary(g => g.Key, g => g.Count()); summary.SessionsByCountry = sessions .Where(s => !string.IsNullOrEmpty(s.Country)) .GroupBy(s => s.Country) .ToDictionary(g => g.Key, g => g.Count()); summary.SessionsByLanguage = sessions .GroupBy(s => s.Language) .ToDictionary(g => g.Key, g => g.Count()); } return summary; } public async Task EndSessionAsync(Guid sessionId) { var session = await _context.BotSessions.FindAsync(sessionId); if (session == null || session.EndedAt.HasValue) return false; session.EndedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); _logger.LogInformation("Ended session {SessionId}", sessionId); return true; } public async Task CleanupInactiveSessionsAsync(int inactiveMinutes = 30) { var cutoffTime = DateTime.UtcNow.AddMinutes(-inactiveMinutes); var inactiveSessions = await _context.BotSessions .Where(s => !s.EndedAt.HasValue && s.LastActivityAt < cutoffTime) .ToListAsync(); foreach (var session in inactiveSessions) { session.EndedAt = DateTime.UtcNow; } await _context.SaveChangesAsync(); _logger.LogInformation("Cleaned up {Count} inactive sessions", inactiveSessions.Count); return inactiveSessions.Count; } // Helper Methods private BotMetricDto MapMetricToDto(BotMetric metric) { var metadata = new Dictionary(); try { metadata = JsonSerializer.Deserialize>(metric.Metadata) ?? new Dictionary(); } catch { } return new BotMetricDto { Id = metric.Id, BotId = metric.BotId, MetricType = metric.MetricType, Value = metric.Value, Category = metric.Category, Description = metric.Description, RecordedAt = metric.RecordedAt, Metadata = metadata }; } private BotSessionDto MapSessionToDto(BotSession session) { var metadata = new Dictionary(); try { metadata = JsonSerializer.Deserialize>(session.Metadata) ?? new Dictionary(); } catch { } return new BotSessionDto { Id = session.Id, BotId = session.BotId, SessionIdentifier = session.SessionIdentifier, Platform = session.Platform, StartedAt = session.StartedAt, LastActivityAt = session.LastActivityAt, EndedAt = session.EndedAt, OrderCount = session.OrderCount, MessageCount = session.MessageCount, TotalSpent = session.TotalSpent, Language = session.Language, Country = session.Country, IsAnonymous = session.IsAnonymous, Metadata = metadata }; } private List GenerateTimeSeries(List metrics, DateTime start, DateTime end) { var dataPoints = new List(); var currentDate = start.Date; while (currentDate <= end.Date) { var dayMetrics = metrics.Where(m => m.RecordedAt.Date == currentDate).ToList(); if (dayMetrics.Any()) { dataPoints.Add(new TimeSeriesDataPoint { Timestamp = currentDate, Label = currentDate.ToString("yyyy-MM-dd"), Value = dayMetrics.Sum(m => m.Value) }); } currentDate = currentDate.AddDays(1); } return dataPoints; } }