using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Dapr.Client;
using FirebaseAdmin;
using FirebaseAdmin.Messaging;
using GB5Shared.DTO.PushNotification;
using GB5Shared.DaprCache;
using GB5Shared.DTO.Framework.Login;
using GB5Shared.Vault;
using Google.Apis.Auth.OAuth2;
namespace GB5Shared.PushNotification
{
public class PushNotificationDAL : IPushNotificationDAL
{
/// Vault KV v2 path holding the Firebase service-account JSON. Set via SetSecretEndpoint.
private const string FirebaseServiceAccountVaultKey = "push-notification/firebase-service-account";
private static readonly SemaphoreSlim _initLock = new(1, 1);
private static bool _firebaseInitialized = false;
private readonly DaprClient _daprClient;
private readonly IVaultService _vaultService;
public PushNotificationDAL(IVaultService vaultService)
{
_daprClient = new DaprClientBuilder().Build();
_vaultService = vaultService;
}
#region Save User Token
///
/// Saves a user's device token in the Dapr state store with a 24-hour TTL.
///
/// User login details.
/// Firebase device token.
/// Status message indicating success or failure.
public async Task SaveUserToken(LoginDTO loginDTO, string token)
{
try
{
string tokenCacheKey = $"UserToken:{loginDTO.UserId}:{loginDTO.ServerId}:{loginDTO.DatabaseName}:{loginDTO.LoginEventLogId}";
var tokenEntry = new UserTokenCache
{
Token = token,
UserId = loginDTO.UserId,
ServerId = loginDTO.ServerId,
DatabaseName = loginDTO.DatabaseName,
LoginEventLogId = loginDTO.LoginEventLogId,
Expiration = DateTime.UtcNow.AddHours(24)
};
var ttlMetadata = new Dictionary { { "ttlInSeconds", "86400" } }; // 24 hours
await _daprClient.SaveStateAsync("pushnotification", tokenCacheKey, tokenEntry, metadata: ttlMetadata);
// Track all user tokens for easy lookup later
string userKeysKey = $"UserKeys:{loginDTO.UserId}";
var existingUserKeys = await _daprClient.GetStateAsync("pushnotification", userKeysKey)
?? new UserKeysCache();
if (!existingUserKeys.Keys.Contains(tokenCacheKey))
{
existingUserKeys.Keys.Add(tokenCacheKey);
await _daprClient.SaveStateAsync("pushnotification", userKeysKey, existingUserKeys);
}
return "User token saved successfully.";
}
catch (Exception)
{
throw;
}
}
#endregion
#region Subscribe to Topic
///
/// Subscribes a user’s device token to a specific Firebase topic (e.g., “DEV”, “Payroll”).
///
/// Device token.
/// Topic name.
/// Success or error message.
public async Task SubscribeUserToTopic(string token, string topic)
{
try
{
await FirebaseInitializedAsync();
var tokens = new List { token };
var result = await FirebaseMessaging.DefaultInstance.SubscribeToTopicAsync(tokens, topic);
if (result.FailureCount > 0)
throw new Exception($"Subscription failed. Success: {result.SuccessCount}, Failure: {result.FailureCount}");
return $"User subscribed to topic '{topic}' successfully.";
}
catch (Exception)
{
throw;
}
}
#endregion
#region Send Notification to Topic
///
/// Sends a push notification to all users subscribed to a specific Firebase topic.
///
/// The notification data including topic, title, and body.
///
/// A message indicating success with the Firebase message ID, or an error message on failure.
///
public async Task SendNotificationToTopic(PushNotificationDTO pushNotificationDTO)
{
try
{
// Ensure Firebase app is initialized before sending
await FirebaseInitializedAsync();
var message = new Message
{
Topic = pushNotificationDTO.NotificationTopic,
//Notification = new Notification
//{
// Title = pushNotificationDTO.NotificationTitle,
// Body = pushNotificationDTO.NotificationBody
//}
};
// Send the message using Firebase Cloud Messaging
string messageId = await FirebaseMessaging.DefaultInstance.SendAsync(message);
return $"Notification sent successfully to topic '{pushNotificationDTO.NotificationTopic}'. Message ID: {messageId}";
}
catch (Exception)
{
throw;
}
}
#endregion
#region Send Notification to Users (Single or Multiple)
///
/// Sends a push notification to one or multiple users based on their stored tokens.
///
///
/// The notification data including user IDs, title, and body.
///
///
/// A message summarizing how many notifications were successfully sent or skipped.
///
public async Task SendNotificationToUsers(PushNotificationDTO PushNotificationDTO)
{
try
{
await FirebaseInitializedAsync();
int totalSent = 0;
foreach (int userId in PushNotificationDTO.UserIds)
{
string userKeysKey = $"UserKeys:{userId}";
var userTokenKeys = await _daprClient.GetStateAsync("pushnotification", userKeysKey);
if (userTokenKeys == null || userTokenKeys.Keys.Count == 0)
continue;
foreach (var tokenKey in userTokenKeys.Keys)
{
var tokenEntry = await _daprClient.GetStateAsync("pushnotification", tokenKey);
if (tokenEntry == null || tokenEntry.Expiration <= DateTime.UtcNow)
continue;
var message = new FirebaseAdmin.Messaging.Message
{
Token = tokenEntry.Token,
Notification = new FirebaseAdmin.Messaging.Notification
{
Title = PushNotificationDTO.NotificationTitle,
Body = PushNotificationDTO.NotificationBody
}
};
await FirebaseMessaging.DefaultInstance.SendAsync(message);
totalSent++;
}
}
return totalSent > 0
? $"Notification sent to {totalSent} device(s)."
: "No valid tokens found for the provided user(s).";
}
catch (Exception)
{
throw;
}
}
#endregion
///
/// Lazily initializes the default from the service-account
/// JSON stored in Vault at . Never hardcode
/// the credential here — see for the only supported access path.
///
private async Task FirebaseInitializedAsync()
{
if (_firebaseInitialized)
return;
await _initLock.WaitAsync().ConfigureAwait(false);
try
{
if (_firebaseInitialized)
return;
string jsonCredentials = await _vaultService.GetSecretAsync(FirebaseServiceAccountVaultKey)
.ConfigureAwait(false);
FirebaseApp.Create(new AppOptions
{
Credential = GoogleCredential.FromJson(jsonCredentials)
});
_firebaseInitialized = true;
}
finally
{
_initLock.Release();
}
}
}
}