using LSFE.Infrastructure.Dto.Maintenance; using LSFE.Infrastructure.Dto.Planning; using LSFE.Infrastructure.Entities.Maintenance; using LSFE.MobApp.Contracts; using LSFE.MobApp.Entities.Account; using LSFE.MobApp.Entities.Location; using LSFE.MobApp.Mapper; using LSFE.MobApp.Models; using LSFE.MobApp.Services.Authentication; using LSFE.MobApp.Services.Coverage; using Microsoft.Extensions.Configuration; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; namespace LSFE.MobApp.Services { public class SyncService : ISyncService { #region Api Client Factory private readonly IApiClientFactory _apiClientFactory; private readonly IApiClient _doctorApiClient; private readonly IApiClient _productTransApiClient; private readonly IApiClient _inventoryTransApiClient; private readonly IApiClient _mrRawPlanApiClient; private readonly IApiClient _userApiClient; #endregion #region Local Repository private readonly IRepository _institutionRepository; private readonly IRepository _attendanceRepository; private readonly IRepository _doctorRepository; private readonly IRepository _productTransRepository; private readonly IRepository _inventoryTransRepository; private readonly IRepository _mrRawPlanRepository; private readonly IRepository _userRepository; #endregion #region Helper private readonly IFileDownloadService _fileDownloadService; #endregion private readonly IConfiguration _configuration; private readonly AddressService _addressService; public SyncService( IApiClientFactory apiClientFactory, IRepository institutionRepository, IRepository doctor, IRepository attendanceRepository, IRepository productTransRepository, IRepository inventoryTransRepository, IRepository mrRawPlanRepository, IRepository userRepository, IConfiguration configuration, IFileDownloadService fileDownloadService, AddressService addressService) { #region Api Constructor _configuration = configuration; _apiClientFactory = apiClientFactory; _doctorApiClient = apiClientFactory.CreateClient("DoctorMgmt/GetDoctorCoverageToday"); _productTransApiClient = apiClientFactory.CreateClient("Plan/GetDoctorProductToday"); _inventoryTransApiClient = apiClientFactory.CreateClient("Plan/GetDoctorSampleToday"); _mrRawPlanApiClient = apiClientFactory.CreateClient("Plan/GetMRRawPlans"); _userApiClient = apiClientFactory.CreateClient(_configuration["ApiSettings:Login"] ?? "AnonAccount/Login/"); #endregion #region Local Constructor _institutionRepository = institutionRepository; _doctorRepository = doctor; _productTransRepository = productTransRepository; _inventoryTransRepository = inventoryTransRepository; _attendanceRepository = attendanceRepository; _mrRawPlanRepository = mrRawPlanRepository; _userRepository = userRepository; #endregion #region Helper _fileDownloadService = fileDownloadService; _addressService = addressService; #endregion } #region Sync Pull public async Task SyncPullUsersAsync(User users) { try { var result = await _userRepository.SaveAsync(users); if (result > 0) { return true; } return false; } catch (Exception ex) { Debug.WriteLine($"Sync error: {ex.Message}"); return false; } } public async Task SyncPullMRRawPlansAsync() { try { var cred =await UserCred.GetUserCred(); if (cred.Token == null || cred.Token.Length == 0 || cred.Token == "N/A") { var userResponse = await _userApiClient.CreateAsync(cred); cred.Token = userResponse.Token; } var serverPlans = await _mrRawPlanApiClient.GetWithQueryAsync(new Dictionary { { "UserId", cred.UserId }, { "UserName", cred.UserName }, { "Token", cred.Token } }); if (serverPlans == null || !serverPlans.Any()) { Debug.WriteLine("No data received from server"); return false; } // Get existing local data var localPlans = await _mrRawPlanRepository.GetAllAsync(); int newCount = 0; int updatedCount = 0; foreach (var serverPlan in serverPlans) { try { // Find existing local record by InstitutionId var existingLocal = await _mrRawPlanRepository. FirstOrDefaultAsync(x => x.MRRawPlanId == serverPlan.MRRawPlanId); if (existingLocal == null) { // Create new local record var newPlan = ApiEntityMapper.MapMRRawPlanFromApi(serverPlan); var result = await _mrRawPlanRepository.SaveAsync(newPlan); if (result > 0) { newCount++; } else { Debug.WriteLine($"❌ Failed to save: {newPlan.MRRawPlanId}"); } } else { // Update existing record ApiEntityMapper.UpdateMRRawPlanFromApi(existingLocal, serverPlan); var result = await _mrRawPlanRepository.UpdateAsync(existingLocal); if (result > 0) { updatedCount++; Debug.WriteLine($"✅ Updated: {existingLocal.MRRawPlanId}"); } else { Debug.WriteLine($"❌ Failed to update: {existingLocal.MRRawPlanId}"); } } } catch (Exception itemEx) { Debug.WriteLine($"Error processing mrRawPlan {serverPlan.MRRawPlanId}: {itemEx.Message}"); } } Debug.WriteLine($"Sync completed: {newCount} new, {updatedCount} updated"); return true; } catch (Exception ex) { Debug.WriteLine($"Sync error: {ex.Message}"); return false; } } public async Task SyncPullProductsAsync() { try { var cred = await UserCred.GetUserCred(); if (cred.Token == null || cred.Token.Length == 0 || cred.Token == "N/A") { var userResponse = await _userApiClient.CreateAsync(cred); cred.Token = userResponse.Token; } var serverProducts = await _productTransApiClient.GetWithQueryAsync(new Dictionary { { "UserId", cred.UserId }, { "UserName", cred.UserName }, { "Token", cred.Token } }); if (serverProducts == null || !serverProducts.Any()) { Debug.WriteLine("No data received from server"); return false; } // Get existing local data var localProducts = await _productTransRepository.GetAllAsync(); int newCount = 0; int updatedCount = 0; foreach (var serverProduct in serverProducts) { try { // Find existing local record by InstitutionId var existingLocal = await _productTransRepository. FirstOrDefaultAsync(x => x.ProductTransId == serverProduct.ProductTransId); if (existingLocal == null) { // Create new local record var newProduct = ApiEntityMapper.MapProductTransFromApi(serverProduct); var result = await _productTransRepository.SaveAsync(newProduct); if (result > 0) { newCount++; Debug.WriteLine($"✅ Successfully created: {newProduct.ProductName}"); } else { Debug.WriteLine($"❌ Failed to save: {newProduct.ProductName}"); } } else { // Update existing record ApiEntityMapper.UpdateProductTransFromApi(existingLocal, serverProduct); var result = await _productTransRepository.UpdateAsync(existingLocal); if (result > 0) { updatedCount++; Debug.WriteLine($"✅ Updated: {existingLocal.ProductName}"); } else { Debug.WriteLine($"❌ Failed to update: {existingLocal.ProductName}"); } } } catch (Exception itemEx) { Debug.WriteLine($"Error processing institution {serverProduct.ProductId}: {itemEx.Message}"); } } Debug.WriteLine($"Sync completed: {newCount} new, {updatedCount} updated"); return true; } catch (Exception ex) { Debug.WriteLine($"Sync error: {ex.Message}"); return false; } } public async Task SyncPullSamplesAsync() { try { var cred = await UserCred.GetUserCred(); if (cred.Token == null || cred.Token.Length == 0 || cred.Token == "N/A") { var userResponse = await _userApiClient.CreateAsync(cred); cred.Token = userResponse.Token; } var serverInventoryTrans = await _inventoryTransApiClient.GetWithQueryAsync(new Dictionary { { "UserId", cred.UserId }, { "UserName", cred.UserName }, { "Token", cred.Token } }); if (serverInventoryTrans == null || !serverInventoryTrans.Any()) { Debug.WriteLine("No data received from server"); return false; } // Get existing local data var localInventoryTrans = await _inventoryTransRepository.GetAllAsync(); int newCount = 0; int updatedCount = 0; foreach (var serverInventoryTran in serverInventoryTrans) { try { // Find existing local record by InventoryTransId var existingLocal = await _inventoryTransRepository. FirstOrDefaultAsync(x => x.InventoryTransId == serverInventoryTran.InventoryTransId); if (existingLocal == null) { // Create new local record var newProduct = ApiEntityMapper.MapInventoryTransFromApi(serverInventoryTran); var result = await _inventoryTransRepository.SaveAsync(newProduct); if (result > 0) { newCount++; } else { Debug.WriteLine($"❌ Failed to save: {newProduct.Description}"); } } else { // Update existing record ApiEntityMapper.UpdateInventoryTransFromApi(existingLocal, serverInventoryTran); var result = await _inventoryTransRepository.UpdateAsync(existingLocal); if (result > 0) { updatedCount++; } else { Debug.WriteLine($"❌ Failed to update: {existingLocal.Description}"); } } } catch (Exception itemEx) { Debug.WriteLine($"Error processing inventory {serverInventoryTran.InventoryTransId}: {itemEx.Message}"); } } //Debug.WriteLine($"Sync completed: {newCount} new, {updatedCount} updated"); return true; } catch (Exception ex) { Debug.WriteLine($"Sync error: {ex.Message}"); return false; } } public async Task SyncPullInstitutionsAsync() { try { // Use the exact API entity name var apiClient = _apiClientFactory.CreateClient("Maintenance/institutions"); var serverInstitutions = await apiClient.GetAllAsync(); if (serverInstitutions == null || !serverInstitutions.Any()) { Debug.WriteLine("No data received from server"); return false; } Debug.WriteLine($"Received {serverInstitutions.Count} institutions from API"); // Get existing local data var localInstitutions = await _institutionRepository.GetAllAsync(); Debug.WriteLine($"Found {localInstitutions.Count} existing local institutions"); int newCount = 0; int updatedCount = 0; foreach (var serverInstitution in serverInstitutions) { try { // Find existing local record by InstitutionId var existingLocal = await _institutionRepository. FirstOrDefaultAsync(x => x.InstitutionId == serverInstitution.InstitutionId); if (existingLocal == null) { // Create new local record var newInstitution = ApiEntityMapper.MapInstutionFromApi(serverInstitution); Debug.WriteLine($"Creating new institution: {newInstitution.InstitutionName} (ServerID: {newInstitution.InstitutionId})"); var result = await _institutionRepository.SaveAsync(newInstitution); if (result > 0) { newCount++; Debug.WriteLine($"✅ Successfully created: {newInstitution.InstitutionName}"); } else { Debug.WriteLine($"❌ Failed to save: {newInstitution.InstitutionName}"); } } else { // Update existing record ApiEntityMapper.UpdateInstutionFromApi(existingLocal, serverInstitution); var result = await _institutionRepository.UpdateAsync(existingLocal); if (result > 0) { updatedCount++; Debug.WriteLine($"✅ Updated: {existingLocal.InstitutionName}"); } else { Debug.WriteLine($"❌ Failed to update: {existingLocal.InstitutionName}"); } } } catch (Exception itemEx) { Debug.WriteLine($"Error processing institution {serverInstitution.InstitutionId}: {itemEx.Message}"); } } Debug.WriteLine($"Sync completed: {newCount} new, {updatedCount} updated"); return true; } catch (Exception ex) { Debug.WriteLine($"Sync error: {ex.Message}"); return false; } } public async Task SyncPullDoctorsAsync() { try { var cred= await UserCred.GetUserCred(); if (cred.Token == null || cred.Token.Length == 0 || cred.Token == "N/A") { var userResponse = await _userApiClient.CreateAsync(cred); cred.Token = userResponse.Token; await SecureStorage.SetAsync("auth_token", userResponse.Token ?? "N/A"); } var serverDoctors = await _doctorApiClient.GetWithQueryAsync(new Dictionary { { "UserId", cred.UserId }, { "UserName", cred.UserName }, { "Token", cred.Token } }); if (serverDoctors == null || !serverDoctors.Any()) { Debug.WriteLine("No data received from server"); return false; } // Get existing local data and update new await _doctorRepository.TruncateAsync(); var localDoctor = await _doctorRepository.GetAllAsync(); int newCount = 0; int updatedCount = 0; foreach (var serverDoc in serverDoctors) { try { // Find existing local record by InstitutionId var existingLocal = await _doctorRepository. FirstOrDefaultAsync(x => x.DoctorId == serverDoc.DoctorId); if (existingLocal == null) { // Create new local record var newDoctor = ApiEntityMapper.MapDoctorFromApi(serverDoc); Debug.WriteLine($"Creating new institution: {newDoctor.FirstName} (ServerID: {newDoctor.DoctorId})"); var result = await _doctorRepository.SaveAsync(newDoctor); if (result > 0) { newCount++; Debug.WriteLine($"✅ Successfully created: {newDoctor.FirstName}"); } else { Debug.WriteLine($"❌ Failed to save: {newDoctor.FirstName}"); } } else { // Update existing record ApiEntityMapper.UpdateDoctorFromApi(existingLocal, serverDoc); var result = await _doctorRepository.UpdateAsync(existingLocal); if (result > 0) { updatedCount++; Debug.WriteLine($"✅ Updated: {existingLocal.FirstName}"); } else { Debug.WriteLine($"❌ Failed to update: {existingLocal.FirstName}"); } } } catch (Exception itemEx) { Debug.WriteLine($"Error processing institution {serverDoc.InstitutionId}: {itemEx.Message}"); } } Debug.WriteLine($"Sync completed: {newCount} new, {updatedCount} updated"); return true; } catch (Exception ex) { Debug.WriteLine($"Sync error: {ex.Message}"); return false; } } public async Task SyncPullCoveragePlanTodayAsync() { await SyncPullDoctorsAsync(); await SyncPullProductsAsync(); await SyncPullSamplesAsync(); await SyncPullMRRawPlansAsync(); await DownloadAllMaterials(); return await SyncPullInstitutionsAsync(); } #endregion #region Sync Post public async Task SyncPostAttendanceAsync() { try { var today = DateTime.Today; var localAttendance = await _attendanceRepository.GetAllAsync(); var _todayAttendance = localAttendance .Where(a => a.TimeIn.HasValue && a.TimeIn.Value.Date == today) .OrderByDescending(a => a.TimeIn) .FirstOrDefault(); // Add null check here if (_todayAttendance == null) { Debug.WriteLine("No attendance record found for today"); return false; } var cred = await UserCred.GetUserCred(); if (cred.Token == null || cred.Token.Length == 0 || cred.Token == "N/A") { var userResponse = await _userApiClient.CreateAsync(cred); cred.Token = userResponse.Token; } var apiClient = _apiClientFactory. CreateClient(_configuration["ApiSettings:Endpoints:PostAttendance"]); var serverAttendance = await apiClient.CreateAttendanceAsync(_todayAttendance); if (!serverAttendance.Success) { Debug.WriteLine("No data received from server"); return false; } Debug.WriteLine($"Sync completed"); return true; } catch (Exception ex) { Debug.WriteLine($"Sync error: {ex.Message}"); return false; } } public async Task SyncPostCoverageTodayAsync(Guid mrRawPlanId) { try { // 1. Fetch the local record var localActualCoveragePlan = await _mrRawPlanRepository .FirstOrDefaultAsync(a => a.MRRawPlanId == mrRawPlanId); // Validate the record exists if (localActualCoveragePlan == null) { Debug.WriteLine($"[SYNC] No coverage plan found for ID: {mrRawPlanId}"); return false; } // Check if already synced (avoid redundant API calls) if (localActualCoveragePlan.IsSync) { Debug.WriteLine($"[SYNC] Coverage plan {mrRawPlanId} already synced. Skipping."); return true; // Already synced is a success } // 4. Validate required data before sending to API if (!ValidateCoveragePlan(localActualCoveragePlan)) { Debug.WriteLine($"[SYNC] Coverage plan {mrRawPlanId} validation failed"); return false; } if (localActualCoveragePlan.ForAddressTranslation) { var (address, isHumanReadable) = await _addressService. GetAddressAsync(double.Parse(localActualCoveragePlan.Latitude), double.Parse(localActualCoveragePlan.Longitude)); localActualCoveragePlan.Location = address; } // 5. Create API client var apiClient = _apiClientFactory .CreateClient( _configuration["ApiSettings:Endpoints:PostActualCoveragePlan"]); Debug.WriteLine($"[SYNC] Sending coverage plan {mrRawPlanId} to server..."); // Send to server var serverResponse = await apiClient.CreateCoveragePlanAsync(localActualCoveragePlan); // Validate server response if (serverResponse == null) { Debug.WriteLine($"[SYNC] Null response from server for {mrRawPlanId}"); return false; } if (!serverResponse.Success) { Debug.WriteLine($"[SYNC] Server returned failure for {mrRawPlanId}. " + $"Message: {serverResponse.Message ?? "No message"}"); return false; } localActualCoveragePlan.IsSync = true; localActualCoveragePlan.LastSyncDate = DateTime.Now; int updateResult = await _mrRawPlanRepository.UpdateAsync(localActualCoveragePlan); if (updateResult <= 0) { Debug.WriteLine($"[SYNC] Failed to update local IsSync flag for {mrRawPlanId}"); // Note: Data is on server but local flag update failed // Consider how to handle this edge case return false; } Debug.WriteLine($"[SYNC] ✓ Coverage plan {mrRawPlanId} synced successfully"); return true; } catch (HttpRequestException httpEx) { // Network-specific errors Debug.WriteLine($"[SYNC] Network error for {mrRawPlanId}: {httpEx.Message}"); return false; } catch (TimeoutException timeoutEx) { // Timeout errors Debug.WriteLine($"[SYNC] Timeout error for {mrRawPlanId}: {timeoutEx.Message}"); return false; } catch (Exception ex) { // General errors Debug.WriteLine($"[SYNC] Unexpected error for {mrRawPlanId}: {ex.Message}"); Debug.WriteLine($"[SYNC] Stack trace: {ex.StackTrace}"); return false; } } private bool ValidateCoveragePlan(Entities.Planning.MRRawPlan plan) { if (plan == null) return false; // Check required fields if (plan.MRRawPlanId == Guid.Empty) { Debug.WriteLine("[SYNC] Invalid MRRawPlanId"); return false; } return true; } #endregion #region Download Materials & Samplings private async Task DownloadAllMaterials() { try { var allTransactions = await _productTransRepository.GetAllAsync(); foreach (var product in allTransactions) { var endpoint = _configuration["ApiSettings:Endpoints:GetProductFiles"]; var fileUrl = $"{endpoint}{Uri.EscapeDataString(product.ProductLink)}"; var material = new ProductMaterial { ProductTransId = product.ProductTransId, ProductId = product.ProductId, ProductName = product.ProductName, ProductLink = product.ProductLink, FileUrl = fileUrl, FileExtension = Path.GetExtension(product.ProductLink) }; if (!await _fileDownloadService.IsFileDownloadedAsync(product.ProductLink)) { await DownloadMaterials(material); } } // await Application.Current.MainPage.DisplayAlert("Success", "All materials downloaded!", "OK"); } catch (Exception ex) { await Application.Current.MainPage.DisplayAlert("Error", $"Failed to download all: {ex.Message}", "OK"); } } private async Task DownloadMaterials(ProductMaterial material) { if (material == null || material.IsDownloading) return; try { material.IsDownloading = true; await _fileDownloadService.DownloadFileAsync(material.FileUrl, material.ProductLink, CancellationToken.None); material.IsDownloaded = true; material.IsDownloading = false; } catch (Exception ex) { material.IsDownloading = false; await Application.Current.MainPage.DisplayAlert("Error", $"Failed to download: {ex.Message}", "OK"); } } #endregion } }