using System.Security.Cryptography; using DysonNetwork.Shared.Models; using DysonNetwork.Shared.Proto; using Grpc.Net.Client; using Microsoft.EntityFrameworkCore; namespace DysonNetwork.Pass.Rewind; public class AccountRewindService( IHttpClientFactory httpClientFactory, AppDatabase db, Account.AccountService accounts, PassRewindService passRewindSrv ) { private static string CapitalizeFirstLetter(string str) { if (string.IsNullOrEmpty(str)) return str; // Capitalize the first character and append the rest of the string in lowercase return char.ToUpper(str[0]) + str[1..].ToLower(); } private RewindService.RewindServiceClient CreateRewindServiceClient(string serviceId) { var httpClient = httpClientFactory.CreateClient( $"{nameof(AccountRewindService)}+{CapitalizeFirstLetter(serviceId)}" ); var channel = GrpcChannel.ForAddress($"https://_grpc.{serviceId}", new GrpcChannelOptions { HttpClient = httpClient, }); return new RewindService.RewindServiceClient(channel); } private async Task CreateRewindPoint(Guid accountId) { const int currentYear = 2025; var rewindRequest = new RequestRewindEvent { AccountId = accountId.ToString(), Year = currentYear }; var rewindEventTasks = new List> { passRewindSrv.CreateRewindEvent(accountId, currentYear), CreateRewindServiceClient("sphere").GetRewindEventAsync(rewindRequest).ResponseAsync }; var rewindEvents = await Task.WhenAll(rewindEventTasks); var rewindData = rewindEvents.ToDictionary>( rewindEvent => rewindEvent.ServiceId, rewindEvent => GrpcTypeHelper.ConvertByteStringToObject>(rewindEvent.Data) ?? new Dictionary() ); var point = new SnRewindPoint { SchemaVersion = 1, AccountId = accountId, Data = rewindData.ToDictionary(kvp => kvp.Key, object? (kvp) => kvp.Value) }; db.RewindPoints.Add(point); await db.SaveChangesAsync(); var account = await accounts.GetAccount(accountId); if (account is not null) point.Account = account; // Fill the data return point; } public async Task GetOrCreateRewindPoint(Guid accountId) { var currentYear = DateTime.UtcNow.Year; var existingRewind = await db.RewindPoints .Where(p => p.AccountId == accountId && p.Year == currentYear) .Include(p => p.Account) .ThenInclude(p => p.Profile) .OrderBy(p => p.CreatedAt) .FirstOrDefaultAsync(); if (existingRewind is not null) return existingRewind; return await CreateRewindPoint(accountId); } public async Task GetPublicRewindPoint(string code) { var point = await db.RewindPoints .Where(p => p.SharableCode == code) .Include(p => p.Account) .ThenInclude(p => p.Profile) .OrderBy(p => p.CreatedAt) .FirstOrDefaultAsync(); return point; } public async Task SetRewindPointPublic(Guid accountId, int year) { var point = await db.RewindPoints .Where(p => p.AccountId == accountId && p.Year == year) .Include(p => p.Account) .ThenInclude(p => p.Profile) .OrderBy(p => p.CreatedAt) .FirstOrDefaultAsync(); if (point is null) throw new InvalidOperationException("No rewind point was found."); point.SharableCode = _GenerateRandomString(16); db.RewindPoints.Update(point); await db.SaveChangesAsync(); return point; } public async Task SetRewindPointPrivate(Guid accountId, int year) { var point = await db.RewindPoints .Where(p => p.AccountId == accountId && p.Year == year) .Include(p => p.Account) .ThenInclude(p => p.Profile) .OrderBy(p => p.CreatedAt) .FirstOrDefaultAsync(); if (point is null) throw new InvalidOperationException("No rewind point was found."); point.SharableCode = null; db.RewindPoints.Update(point); await db.SaveChangesAsync(); return point; } private static string _GenerateRandomString(int length) { const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; var result = new char[length]; using var rng = RandomNumberGenerator.Create(); for (var i = 0; i < length; i++) { var bytes = new byte[1]; rng.GetBytes(bytes); result[i] = chars[bytes[0] % chars.Length]; } return new string(result); } }