using System.Text.Json; using InnovEnergy.App.Backend.DataTypes; using InnovEnergy.Lib.Mailer; using MailKit.Net.Smtp; using MailKit.Security; using MimeKit; namespace InnovEnergy.App.Backend.Services; public static class ReportEmailService { /// /// Sends the weekly report as a nicely formatted HTML email. /// Uses MailKit directly (same config as existing Mailer library) but with HTML support. /// public static async Task SendReportEmailAsync(WeeklyReportResponse report, string recipientEmail) { var subject = $"Weekly Energy Report — {report.InstallationName} ({report.PeriodStart} to {report.PeriodEnd})"; var html = BuildHtmlEmail(report); var config = await ReadMailerConfig(); var from = new MailboxAddress(config.SenderName, config.SenderAddress); var to = new MailboxAddress(recipientEmail, recipientEmail); var msg = new MimeMessage { From = { from }, To = { to }, Subject = subject, Body = new TextPart("html") { Text = html } }; Console.WriteLine($"[ReportEmailService] SMTP: {config.SmtpUsername}@{config.SmtpServerUrl}:{config.SmtpPort}"); using var smtp = new SmtpClient(); await smtp.ConnectAsync(config.SmtpServerUrl, config.SmtpPort, SecureSocketOptions.StartTls); await smtp.AuthenticateAsync(config.SmtpUsername, config.SmtpPassword); await smtp.SendAsync(msg); await smtp.DisconnectAsync(true); Console.WriteLine($"[ReportEmailService] Report sent to {recipientEmail}"); } private static async Task ReadMailerConfig() { await using var fileStream = File.OpenRead(MailerConfig.DefaultFile); var config = await JsonSerializer.DeserializeAsync(fileStream); return config ?? throw new InvalidOperationException("Failed to read MailerConfig.json"); } // ── HTML email template ───────────────────────────────────────────── public static string BuildHtmlEmail(WeeklyReportResponse r) { var cur = r.CurrentWeek; var prev = r.PreviousWeek; // Parse AI insight into
  • bullet points (split on newlines, strip leading "- " or "1. ") var insightLines = r.AiInsight .Split('\n', StringSplitOptions.RemoveEmptyEntries) .Select(l => System.Text.RegularExpressions.Regex.Replace(l.Trim(), @"^[\d]+[.)]\s*|^[-*]\s*", "")) .Where(l => l.Length > 0) .ToList(); var insightHtml = insightLines.Count > 1 ? "
      " + string.Join("", insightLines.Select(l => $"
    • {FormatInsightLine(l)}
    • ")) + "
    " : $"

    {FormatInsightLine(r.AiInsight)}

    "; // Daily rows var dailyRows = ""; foreach (var d in r.DailyData) { var dayName = DateTime.Parse(d.Date).ToString("ddd"); var isCurrentWeek = string.Compare(d.Date, r.PeriodStart, StringComparison.Ordinal) >= 0; var bgColor = isCurrentWeek ? "#ffffff" : "#f9f9f9"; dailyRows += $@" {dayName} {d.Date} {d.PvProduction:F1} {d.LoadConsumption:F1} {d.GridImport:F1} {d.GridExport:F1} {d.BatteryCharged:F1}/{d.BatteryDischarged:F1} "; } // Week-over-week comparison rows var comparisonHtml = prev != null ? $@" PV Production {cur.TotalPvProduction:F1} kWh {prev.TotalPvProduction:F1} kWh {FormatChange(r.PvChangePercent)} Consumption {cur.TotalConsumption:F1} kWh {prev.TotalConsumption:F1} kWh {FormatChange(r.ConsumptionChangePercent)} Grid Import {cur.TotalGridImport:F1} kWh {prev.TotalGridImport:F1} kWh {FormatChange(r.GridImportChangePercent)} Grid Export {cur.TotalGridExport:F1} kWh {prev.TotalGridExport:F1} kWh — Battery In/Out {cur.TotalBatteryCharged:F1}/{cur.TotalBatteryDischarged:F1} kWh {prev.TotalBatteryCharged:F1}/{prev.TotalBatteryDischarged:F1} kWh — " : $@" PV Production{cur.TotalPvProduction:F1} kWh Consumption{cur.TotalConsumption:F1} kWh Grid Import{cur.TotalGridImport:F1} kWh Grid Export{cur.TotalGridExport:F1} kWh Battery In/Out{cur.TotalBatteryCharged:F1}/{cur.TotalBatteryDischarged:F1} kWh"; var comparisonHeaders = prev != null ? @"This Week Last Week Change" : @"This Week"; return $@"
    Weekly Performance Report
    {r.InstallationName}
    {r.PeriodStart} — {r.PeriodEnd}
    Weekly Insights
    {insightHtml}
    Weekly Summary
    {comparisonHeaders} {comparisonHtml}
    Metric
    Your Savings This Week
    {SavingsBox("Solar Energy Used", $"{r.CurrentWeek.TotalPvProduction - r.CurrentWeek.TotalGridExport:F1} kWh", "stayed at home", "#27ae60")} {SavingsBox("Est. Money Saved", $"~{(r.CurrentWeek.TotalPvProduction - r.CurrentWeek.TotalGridExport) * 0.27:F1} CHF", "at 0.27 CHF/kWh", "#2980b9")} {SavingsBox("Solar Coverage", $"{r.SelfSufficiencyPercent:F0}%", "from solar", "#8e44ad")} {SavingsBox("Battery Eff.", $"{r.BatteryEfficiencyPercent:F0}%", "out vs in", "#e67e22")}
    Daily Breakdown (kWh)
    {dailyRows}
    Day PV Load Grid In Grid Out Batt In/Out
    Generated by Inesco Energy Monitor Platform · Powered by Mistral AI
    "; } private static string SavingsBox(string label, string value, string subtitle, string color) => $@"
    {value}
    {label}
    {subtitle}
    "; // Bolds "Title" before first colon, and numbers+units in the rest private static string FormatInsightLine(string line) { var colonIdx = line.IndexOf(':'); string result; if (colonIdx > 0) { var title = line[..colonIdx]; var rest = line[colonIdx..]; // includes the colon result = $"{title}{rest}"; } else { result = line; } // Bold numbers followed by units result = System.Text.RegularExpressions.Regex.Replace( result, @"(\d+[\d,.]*\s*(?:kWh|CHF|%|days?))", "$1"); return result; } private static string FormatChange(double pct) => pct == 0 ? "—" : pct > 0 ? $"+{pct:F1}%" : $"{pct:F1}%"; private static string ChangeColor(double pct) => pct > 0 ? "#27ae60" : pct < 0 ? "#e74c3c" : "#888"; }