using DysonNetwork.Shared.Proto; using Google.Protobuf.WellKnownTypes; using Grpc.Core; using Microsoft.EntityFrameworkCore; using NodaTime; using NodaTime.Serialization.Protobuf; namespace DysonNetwork.Pass.Account; public class AccountServiceGrpc( AppDatabase db, IClock clock, ILogger logger ) : Shared.Proto.AccountService.AccountServiceBase { private readonly AppDatabase _db = db ?? throw new ArgumentNullException(nameof(db)); private readonly IClock _clock = clock ?? throw new ArgumentNullException(nameof(clock)); private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); public override async Task GetAccount(GetAccountRequest request, ServerCallContext context) { if (!Guid.TryParse(request.Id, out var accountId)) throw new RpcException(new Grpc.Core.Status(StatusCode.InvalidArgument, "Invalid account ID format")); var account = await _db.Accounts .AsNoTracking() .Include(a => a.Profile) .FirstOrDefaultAsync(a => a.Id == accountId); if (account == null) throw new RpcException(new Grpc.Core.Status(StatusCode.NotFound, $"Account {request.Id} not found")); return account.ToProtoValue(); } public override async Task CreateAccount(CreateAccountRequest request, ServerCallContext context) { // Map protobuf request to domain model var account = new Account { Name = request.Name, Nick = request.Nick, Language = request.Language, IsSuperuser = request.IsSuperuser, ActivatedAt = request.Profile != null ? null : _clock.GetCurrentInstant(), Profile = new AccountProfile { FirstName = request.Profile?.FirstName, LastName = request.Profile?.LastName, // Initialize other profile fields as needed } }; // Add to database _db.Accounts.Add(account); await _db.SaveChangesAsync(); _logger.LogInformation("Created new account with ID {AccountId}", account.Id); return account.ToProtoValue(); } public override async Task UpdateAccount(UpdateAccountRequest request, ServerCallContext context) { if (!Guid.TryParse(request.Id, out var accountId)) throw new RpcException(new Grpc.Core.Status(StatusCode.InvalidArgument, "Invalid account ID format")); var account = await _db.Accounts.FindAsync(accountId); if (account == null) throw new RpcException(new Grpc.Core.Status(StatusCode.NotFound, $"Account {request.Id} not found")); // Update fields if they are provided in the request if (request.Name != null) account.Name = request.Name; if (request.Nick != null) account.Nick = request.Nick; if (request.Language != null) account.Language = request.Language; if (request.IsSuperuser != null) account.IsSuperuser = request.IsSuperuser.Value; await _db.SaveChangesAsync(); return account.ToProtoValue(); } public override async Task DeleteAccount(DeleteAccountRequest request, ServerCallContext context) { if (!Guid.TryParse(request.Id, out var accountId)) throw new RpcException(new Grpc.Core.Status(StatusCode.InvalidArgument, "Invalid account ID format")); var account = await _db.Accounts.FindAsync(accountId); if (account == null) throw new RpcException(new Grpc.Core.Status(StatusCode.NotFound, $"Account {request.Id} not found")); _db.Accounts.Remove(account); await _db.SaveChangesAsync(); return new Empty(); } public override async Task ListAccounts(ListAccountsRequest request, ServerCallContext context) { var query = _db.Accounts.AsNoTracking(); // Apply filters if provided if (!string.IsNullOrEmpty(request.Filter)) { // Implement filtering logic based on request.Filter // This is a simplified example query = query.Where(a => a.Name.Contains(request.Filter) || a.Nick.Contains(request.Filter)); } // Apply ordering query = request.OrderBy switch { "name" => query.OrderBy(a => a.Name), "name_desc" => query.OrderByDescending(a => a.Name), _ => query.OrderBy(a => a.Id) }; // Get total count for pagination var totalCount = await query.CountAsync(); // Apply pagination var accounts = await query .Skip(request.PageSize * (request.PageToken != null ? int.Parse(request.PageToken) : 0)) .Take(request.PageSize) .Include(a => a.Profile) .ToListAsync(); var response = new ListAccountsResponse { TotalSize = totalCount, NextPageToken = (accounts.Count == request.PageSize) ? ((request.PageToken != null ? int.Parse(request.PageToken) : 0) + 1).ToString() : "" }; response.Accounts.AddRange(accounts.Select(x => x.ToProtoValue())); return response; } // Implement other service methods following the same pattern... // Profile operations public override async Task GetProfile(GetProfileRequest request, ServerCallContext context) { if (!Guid.TryParse(request.AccountId, out var accountId)) throw new RpcException(new Grpc.Core.Status(StatusCode.InvalidArgument, "Invalid account ID format")); var profile = await _db.AccountProfiles .AsNoTracking() .FirstOrDefaultAsync(p => p.AccountId == accountId); if (profile == null) throw new RpcException(new Grpc.Core.Status(StatusCode.NotFound, $"Profile for account {request.AccountId} not found")); return profile.ToProtoValue(); } public override async Task UpdateProfile(UpdateProfileRequest request, ServerCallContext context) { if (!Guid.TryParse(request.AccountId, out var accountId)) throw new RpcException(new Grpc.Core.Status(StatusCode.InvalidArgument, "Invalid account ID format")); var profile = await _db.AccountProfiles .FirstOrDefaultAsync(p => p.AccountId == accountId); if (profile == null) throw new RpcException(new Grpc.Core.Status(StatusCode.NotFound, $"Profile for account {request.AccountId} not found")); // Update only the fields specified in the field mask if (request.UpdateMask == null || request.UpdateMask.Paths.Contains("first_name")) profile.FirstName = request.Profile.FirstName; if (request.UpdateMask == null || request.UpdateMask.Paths.Contains("last_name")) profile.LastName = request.Profile.LastName; // Update other fields similarly... await _db.SaveChangesAsync(); return profile.ToProtoValue(); } // Contact operations public override async Task AddContact(AddContactRequest request, ServerCallContext context) { if (!Guid.TryParse(request.AccountId, out var accountId)) throw new RpcException(new Grpc.Core.Status(StatusCode.InvalidArgument, "Invalid account ID format")); var contact = new AccountContact { AccountId = accountId, Type = (AccountContactType)request.Type, Content = request.Content, IsPrimary = request.IsPrimary, VerifiedAt = null }; _db.AccountContacts.Add(contact); await _db.SaveChangesAsync(); return contact.ToProtoValue(); } public override async Task RemoveContact(RemoveContactRequest request, ServerCallContext context) { if (!Guid.TryParse(request.AccountId, out var accountId)) throw new RpcException(new Grpc.Core.Status(StatusCode.InvalidArgument, "Invalid account ID format")); if (!Guid.TryParse(request.Id, out var contactId)) throw new RpcException(new Grpc.Core.Status(StatusCode.InvalidArgument, "Invalid contact ID format")); var contact = await _db.AccountContacts.FirstOrDefaultAsync(c => c.Id == contactId && c.AccountId == accountId); if (contact == null) throw new RpcException(new Grpc.Core.Status(StatusCode.NotFound, "Contact not found.")); _db.AccountContacts.Remove(contact); await _db.SaveChangesAsync(); return new Empty(); } public override async Task ListContacts(ListContactsRequest request, ServerCallContext context) { if (!Guid.TryParse(request.AccountId, out var accountId)) throw new RpcException(new Grpc.Core.Status(StatusCode.InvalidArgument, "Invalid account ID format")); var query = _db.AccountContacts.AsNoTracking().Where(c => c.AccountId == accountId); if (request.VerifiedOnly) query = query.Where(c => c.VerifiedAt != null); var contacts = await query.ToListAsync(); var response = new ListContactsResponse(); response.Contacts.AddRange(contacts.Select(c => c.ToProtoValue())); return response; } public override async Task VerifyContact(VerifyContactRequest request, ServerCallContext context) { // This is a placeholder implementation. In a real-world scenario, you would // have a more robust verification mechanism (e.g., sending a code to the // user's email or phone). if (!Guid.TryParse(request.AccountId, out var accountId)) throw new RpcException(new Grpc.Core.Status(StatusCode.InvalidArgument, "Invalid account ID format")); if (!Guid.TryParse(request.Id, out var contactId)) throw new RpcException(new Grpc.Core.Status(StatusCode.InvalidArgument, "Invalid contact ID format")); var contact = await _db.AccountContacts.FirstOrDefaultAsync(c => c.Id == contactId && c.AccountId == accountId); if (contact == null) throw new RpcException(new Grpc.Core.Status(StatusCode.NotFound, "Contact not found.")); contact.VerifiedAt = _clock.GetCurrentInstant(); await _db.SaveChangesAsync(); return contact.ToProtoValue(); } // Badge operations public override async Task AddBadge(AddBadgeRequest request, ServerCallContext context) { if (!Guid.TryParse(request.AccountId, out var accountId)) throw new RpcException(new Grpc.Core.Status(StatusCode.InvalidArgument, "Invalid account ID format")); var badge = new AccountBadge { AccountId = accountId, Type = request.Type, Label = request.Label, Caption = request.Caption, ActivatedAt = _clock.GetCurrentInstant(), ExpiredAt = request.ExpiredAt?.ToInstant(), Meta = request.Meta.ToDictionary(kvp => kvp.Key, kvp => (object)kvp.Value) }; _db.Badges.Add(badge); await _db.SaveChangesAsync(); return badge.ToProtoValue(); } public override async Task RemoveBadge(RemoveBadgeRequest request, ServerCallContext context) { if (!Guid.TryParse(request.AccountId, out var accountId)) throw new RpcException(new Grpc.Core.Status(StatusCode.InvalidArgument, "Invalid account ID format")); if (!Guid.TryParse(request.Id, out var badgeId)) throw new RpcException(new Grpc.Core.Status(StatusCode.InvalidArgument, "Invalid badge ID format")); var badge = await _db.Badges.FirstOrDefaultAsync(b => b.Id == badgeId && b.AccountId == accountId); if (badge == null) throw new RpcException(new Grpc.Core.Status(StatusCode.NotFound, "Badge not found.")); _db.Badges.Remove(badge); await _db.SaveChangesAsync(); return new Empty(); } public override async Task ListBadges(ListBadgesRequest request, ServerCallContext context) { if (!Guid.TryParse(request.AccountId, out var accountId)) throw new RpcException(new Grpc.Core.Status(StatusCode.InvalidArgument, "Invalid account ID format")); var query = _db.Badges.AsNoTracking().Where(b => b.AccountId == accountId); if (request.ActiveOnly) query = query.Where(b => b.ExpiredAt == null || b.ExpiredAt > _clock.GetCurrentInstant()); var badges = await query.ToListAsync(); var response = new ListBadgesResponse(); response.Badges.AddRange(badges.Select(b => b.ToProtoValue())); return response; } public override async Task SetActiveBadge(SetActiveBadgeRequest request, ServerCallContext context) { if (!Guid.TryParse(request.AccountId, out var accountId)) throw new RpcException(new Grpc.Core.Status(StatusCode.InvalidArgument, "Invalid account ID format")); var profile = await _db.AccountProfiles.FirstOrDefaultAsync(p => p.AccountId == accountId); if (profile == null) throw new RpcException(new Grpc.Core.Status(StatusCode.NotFound, "Profile not found.")); if (!string.IsNullOrEmpty(request.BadgeId) && !Guid.TryParse(request.BadgeId, out var badgeId)) throw new RpcException(new Grpc.Core.Status(StatusCode.InvalidArgument, "Invalid badge ID format")); await _db.SaveChangesAsync(); return profile.ToProtoValue(); } }