using System.Net.Http.Headers; using System.Text.Json; using ProfileManager.Models; namespace ProfileManager.Services; public sealed class GiteaApiClient { private readonly HttpClient _httpClient; private readonly string _baseUrl; private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true }; public GiteaApiClient(string? baseUrl = null, string? apiToken = null) { _baseUrl = (baseUrl ?? Environment.GetEnvironmentVariable("GITEA_API_URL") ?? "https://git.webcore-consulting.dev/api/v1") .TrimEnd('/'); var handler = new SocketsHttpHandler { PooledConnectionLifetime = TimeSpan.FromMinutes(2), ConnectTimeout = TimeSpan.FromSeconds(10) }; _httpClient = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(15) }; _httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("NickiProfileBot", "1.0")); _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); var token = apiToken ?? Environment.GetEnvironmentVariable("GITEA_TOKEN"); if (!string.IsNullOrWhiteSpace(token)) { _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); } } public async Task> GetHeatmapAsync(string username, CancellationToken cancellationToken = default) { var url = $"{_baseUrl}/users/{Uri.EscapeDataString(username)}/heatmap"; Console.WriteLine($"[API] Fetching heatmap from: {url}"); try { using var response = await _httpClient.GetAsync(url, cancellationToken); if (response.IsSuccessStatusCode) { var json = await response.Content.ReadAsStringAsync(cancellationToken); var items = JsonSerializer.Deserialize>(json, JsonOptions); if (items != null && items.Count > 0) { Console.WriteLine($"[API] Successfully retrieved {items.Count} heatmap data points."); return items; } } Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine($"[API-WARN] Heatmap request failed (Status: {response.StatusCode}). Switching to mock fallback."); Console.ResetColor(); } catch (Exception ex) { Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine($"[API-WARN] Network error connecting to Gitea ({ex.Message}). Using realistic fallback data."); Console.ResetColor(); } return GenerateMockHeatmap(); } public async Task> GetReposAsync(string username, CancellationToken cancellationToken = default) { var url = $"{_baseUrl}/users/{Uri.EscapeDataString(username)}/repos?limit=100&sort=updated"; Console.WriteLine($"[API] Fetching repositories from: {url}"); try { using var response = await _httpClient.GetAsync(url, cancellationToken); if (response.IsSuccessStatusCode) { var json = await response.Content.ReadAsStringAsync(cancellationToken); var repos = JsonSerializer.Deserialize>(json, JsonOptions); if (repos != null && repos.Count > 0) { Console.WriteLine($"[API] Successfully retrieved {repos.Count} repositories."); return repos; } } Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine($"[API-WARN] Repositories request failed (Status: {response.StatusCode}). Switching to mock fallback."); Console.ResetColor(); } catch (Exception ex) { Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine($"[API-WARN] Network error connecting to Gitea ({ex.Message}). Using realistic fallback data."); Console.ResetColor(); } return GenerateMockRepos(); } private static List GenerateMockHeatmap() { var items = new List(); var now = DateTimeOffset.UtcNow; var random = new Random(42); // Fixed seed for reproducible fallback for (int i = 0; i < 365; i++) { var day = now.AddDays(-i); // Simulate realistic developer habits (more activity on weekdays and active streaks) bool isWeekend = day.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday; int chance = isWeekend ? 40 : 75; if (random.Next(100) < chance) { int commits = random.Next(1, 14); items.Add(new HeatmapItem { Timestamp = day.ToUnixTimeSeconds(), Contributions = commits }); } } return items; } private static List GenerateMockRepos() { var now = DateTime.UtcNow; return [ new GiteaRepo { Name = "Markdown-Editor-Pro", FullName = "NickiCloud/Markdown-Editor-Pro", Description = "Moderner, performanter Markdown-Editor mit Live-Vorschau, Syntax-Highlighting & LaTeX-Support auf Avalonia UI.", HtmlUrl = "https://git.webcore-consulting.dev/NickiCloud/Markdown-Editor-Pro", Language = "C#", StarsCount = 7, ForksCount = 1, UpdatedAt = now.AddHours(-3) }, new GiteaRepo { Name = "CloudDrop", FullName = "NickiCloud/CloudDrop", Description = "Leichtgewichtiger, selbstgehosteter File-Sharing-Dienst mit temporären Download-Links und verschlüsselter Ablage.", HtmlUrl = "https://git.webcore-consulting.dev/NickiCloud/CloudDrop", Language = "C#", StarsCount = 5, ForksCount = 0, UpdatedAt = now.AddDays(-2) }, new GiteaRepo { Name = "KasseApp", FullName = "NickiCloud/KasseApp", Description = "Intuitive Kassen- und Abrechnungssoftware für Vereine und Events mit Offline-First SQLite-Datenbank.", HtmlUrl = "https://git.webcore-consulting.dev/NickiCloud/KasseApp", Language = "C#", StarsCount = 4, ForksCount = 0, UpdatedAt = now.AddDays(-5) }, new GiteaRepo { Name = "DrinkReminder", FullName = "NickiCloud/DrinkReminder", Description = "Smarter Tray-Assistent für Desktop & Mobile zur Erinnerung an regelmäßige Flüssigkeitszufuhr.", HtmlUrl = "https://git.webcore-consulting.dev/NickiCloud/DrinkReminder", Language = "C#", StarsCount = 3, ForksCount = 0, UpdatedAt = now.AddDays(-12) }, new GiteaRepo { Name = "Selfhosted-Infra", FullName = "NickiCloud/Selfhosted-Infra", Description = "Docker-Compose & Konfigurationen für Debian, Gitea, Mailcow, Nginx Reverse Proxy, UFW & Fail2ban.", HtmlUrl = "https://git.webcore-consulting.dev/NickiCloud/Selfhosted-Infra", Language = "Shell", StarsCount = 6, ForksCount = 0, UpdatedAt = now.AddDays(-18) }, new GiteaRepo { Name = "WebCore-Portal", FullName = "NickiCloud/WebCore-Portal", Description = "Interaktives Service-Portal mit MudBlazor Komponenten und JWT-Authentifizierung.", HtmlUrl = "https://git.webcore-consulting.dev/NickiCloud/WebCore-Portal", Language = "HTML", StarsCount = 2, ForksCount = 0, UpdatedAt = now.AddDays(-25) } ]; } }