using FinlyticAssets.Models; using FinlyticAssets.Models.DataToObject.TradeRepublic; using FinlyticCore.Models.Assets; namespace FinlyticAssets.Services; /// /// A background service that runs a continuous asset synchronization loop, /// scanning Trade Republic to retrieve, update, and index all supported asset types. /// public class AssetsFullScanService : BackgroundService { private readonly IServiceScopeFactory _serviceScopeFactory; private readonly ILogger _logger; private AssetsCount? _assetsCount; private AssetsCount? _currAssetsCount; /// /// Initializes a new instance of the class. /// /// Factory used to create service scopes for database and API requests. /// Logger for service lifecycle and scanning progress messages. public AssetsFullScanService(IServiceScopeFactory serviceScopeFactory, ILogger logger) { _serviceScopeFactory = serviceScopeFactory; _logger = logger; } /// /// Executes the background scanning task, handling initial startup, recovery, and periodic full-scan cycles. /// /// Triggered when the host is shutting down. /// A task that represents the background operation. protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("AssetsFullScanService has started."); try { using (var scope = _serviceScopeFactory.CreateScope()) { var indexService = scope.ServiceProvider.GetRequiredService(); _logger.LogInformation("Building initial asset index on service startup..."); await indexService.ReCreateIndexFileAsync(stoppingToken); } } catch (Exception ex) { _logger.LogError(ex, "Failed to build initial asset index on startup. Continuing service execution."); } do { try { using (var scope = _serviceScopeFactory.CreateScope()) { var tradeRepublicService = scope.ServiceProvider.GetRequiredService(); var settingsService = scope.ServiceProvider.GetRequiredService(); var assetsDbService = scope.ServiceProvider.GetRequiredService(); var indexService = scope.ServiceProvider.GetRequiredService(); _logger.LogInformation("Requesting total asset counts from Trade Republic..."); _assetsCount = await tradeRepublicService.GetAssetsCount(stoppingToken); _currAssetsCount = new AssetsCount(); var initSettings = await settingsService.GetSettings(); var isRecoveryMode = initSettings.CurrentScanningPage > 0; foreach (var type in Enum.GetValues()) { if (stoppingToken.IsCancellationRequested) break; if (isRecoveryMode) { if (type != initSettings.CurrentScanningType) { _logger.LogInformation("Recovery: {AssetType} was already processed. Skipping.", type); continue; } isRecoveryMode = false; } else { var settings = await settingsService.GetSettings(); settings.CurrentScanningType = type; settings.CurrentScanningPage = 0; await settingsService.SaveSettings(settings); } _logger.LogInformation("Processing asset type: {AssetType}...", type); await HandleAssetType(type, tradeRepublicService, settingsService, assetsDbService, indexService, stoppingToken); var currentSettings = await settingsService.GetSettings(); var delaySeconds = currentSettings.FinishedInitialScan ? currentSettings.AssetUpdateTypeDelay : currentSettings.InitAssetUpdateTypeDelay; var jitter = Random.Shared.Next(0, 480); _logger.LogDebug("Waiting {Delay} seconds before the next asset type.", delaySeconds + jitter); await Task.Delay(TimeSpan.FromSeconds(delaySeconds + jitter), stoppingToken); } var finalSettings = await settingsService.GetSettings(); finalSettings.CurrentScanningPage = 0; if (!finalSettings.FinishedInitialScan && !stoppingToken.IsCancellationRequested) { _logger.LogInformation("Initial scan successfully completed. Switching FinishedInitialScan to true."); finalSettings.FinishedInitialScan = true; } await settingsService.SaveSettings(finalSettings); } _logger.LogInformation("Full scan cycle completed. Waiting 1 minute before starting the next cycle."); await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken); } catch (Exception e) { _logger.LogError(e, "An unhandled exception occurred in AssetsFullScanService. Retrying in 10 seconds."); try { await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); } catch { /* Ignore */ } } } while (!stoppingToken.IsCancellationRequested); _logger.LogInformation("AssetsFullScanService is stopping."); } /// /// Handles the scanning process for a specific asset type, iterating through its paginated results. /// /// The type of assets to process (e.g., Stock, Crypto). /// Service to fetch asset data from Trade Republic. /// Service to read and write application state/settings. /// Service to insert or update assets in the database. /// Service to recreate the local index file if needed. /// Cancellation token monitored for cancellation requests. /// A task representing the asynchronous operation. private async Task HandleAssetType(AssetType type, ITradeRepublicService tradeRepublicService, ISettingsDbService settingsDbService, IAssetsDbService assetsDbService, IAssetsIndexService indexService, CancellationToken stoppingToken) { var totalCount = _assetsCount?.GetCountFromType(type) ?? 0; if (totalCount == 0) { _logger.LogWarning("No assets found for type {AssetType}.", type); return; } _currAssetsCount ??= new AssetsCount(); var currentItemOffset = 0; var settings = await settingsDbService.GetSettings(); if (settings.CurrentScanningType == type && settings.CurrentScanningPage > 0) { var pageSize = settings.TradeRepublicMaxRequestPageSize <= 0 ? 50 : settings.TradeRepublicMaxRequestPageSize; if (pageSize > 100) pageSize = 100; currentItemOffset = (settings.CurrentScanningPage - 1) * pageSize; _logger.LogInformation("Resuming full scan for {AssetType} from Page {Page} (Offset: {Offset}).", type, settings.CurrentScanningPage, currentItemOffset); } while (currentItemOffset < totalCount && !stoppingToken.IsCancellationRequested) { var currentSettings = await settingsDbService.GetSettings(); var pageSize = currentSettings.TradeRepublicMaxRequestPageSize; if (pageSize <= 0 || pageSize > 100) pageSize = 100; var currentPage = (currentItemOffset / pageSize) + 1; currentSettings.CurrentScanningType = type; currentSettings.CurrentScanningPage = currentPage; await settingsDbService.SaveSettings(currentSettings); _logger.LogDebug("Fetching {AssetType} - Page {Page} (Size: {PageSize}). Offset: {Offset}/{Total}", type, currentPage, pageSize, currentItemOffset, totalCount); var assets = await tradeRepublicService.GetAssets(type, currentPage, pageSize, stoppingToken); if (assets?.Results == null || assets.Results.Count == 0) { _logger.LogWarning("Fetch for {AssetType} (Page {Page}) returned no results. Retrying in 5 seconds...", type, currentPage); await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken); continue; } await ProcessAssets(assets.Results, assetsDbService, indexService,stoppingToken); _currAssetsCount.SetCountOfType(type, _currAssetsCount.GetCountFromType(type) + assets.Results.Count); currentItemOffset = currentPage * pageSize; if (assets.Results.Count < pageSize) { _logger.LogInformation("Reached the last page for {AssetType}.", type); break; } var delaySeconds = currentSettings.FinishedInitialScan ? currentSettings.BatchAssetUpdateDelay : currentSettings.InitBatchAssetUpdateDelay; var jitter = Random.Shared.Next(0, 360); _logger.LogDebug("Waiting {Delay} seconds before the next batch.", delaySeconds + jitter); await Task.Delay(TimeSpan.FromSeconds(delaySeconds + jitter), stoppingToken); } } /// /// Processes a list of fetched Trade Republic assets, updates them in the database, /// and triggers an index recreation if changes were detected. /// /// The list of Trade Republic assets to process. /// Service to insert or update assets in the database. /// Service to recreate the local index file. /// Cancellation token monitored for cancellation requests. /// A task representing the asynchronous operation. private async Task ProcessAssets(IList assets, IAssetsDbService assetsDbService, IAssetsIndexService indexService, CancellationToken stoppingToken) { if (assets == null || assets.Count == 0) return; var changedRows = await assetsDbService.AddOrUpdateAssetsAsync(assets); _logger.LogInformation("[Scan] {Count} assets passed to the DB service. {Changed} modifications/inserts executed.", assets.Count, changedRows); if (changedRows > 0) { _logger.LogInformation("Database modifications detected. Recreating the asset index file..."); await indexService.ReCreateIndexFileAsync(stoppingToken); } } }