using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using FinlyticAssets.Models; using FinlyticCore.Models.Assets; using FinlyticCore.Models.TradeRepublic; using FinlyticCore.Services.TradeRepublic; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; 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 AssetScannerBackgroundService : BackgroundService { private readonly IServiceScopeFactory _serviceScopeFactory; private readonly ILogger _logger; private AssetsCount? _assetsCount; private AssetsCount? _currAssetsCount; public AssetScannerBackgroundService(IServiceScopeFactory serviceScopeFactory, ILogger logger) { _serviceScopeFactory = serviceScopeFactory; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("[{Channel}] AssetScannerBackgroundService has started.", "AssetsChannel"); try { using var scope = _serviceScopeFactory.CreateScope(); var indexService = scope.ServiceProvider.GetRequiredService(); _logger.LogInformation("[{Channel}] Building initial asset index on service startup...", "AssetsChannel"); await indexService.ReCreateIndexFileAsync(stoppingToken); } catch (Exception ex) { _logger.LogError(ex, "[{Channel}] Failed to build initial asset index on startup. Continuing service execution.", "AssetsChannel"); } 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("[{Channel}] Requesting total asset counts from Trade Republic...", "AssetsChannel"); _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("[{Channel}] Recovery: {AssetType} was already processed. Skipping.", "AssetsChannel", type); continue; } isRecoveryMode = false; } else { var settings = await settingsService.GetSettings(); settings.CurrentScanningType = type; settings.CurrentScanningPage = 0; await settingsService.SaveSettings(settings); } _logger.LogInformation("[{Channel}] Processing asset type: {AssetType}...", "AssetsChannel", type); await HandleAssetType(type, tradeRepublicService, settingsService, assetsDbService, indexService, stoppingToken); var currentSettings = await settingsService.GetSettings(); var delaySeconds = currentSettings.FinishedInitialScan ? currentSettings.AssetUpdateTypeDelay : currentSettings.InitAssetUpdateTypeDelay; if (delaySeconds > 0) { var jitter = Random.Shared.Next(0, Math.Min(15, delaySeconds)); _logger.LogInformation("[{Channel}] Waiting {Delay}s before next asset type ({Type}).", "AssetsChannel", delaySeconds + jitter, type); await Task.Delay(TimeSpan.FromSeconds(delaySeconds + jitter), stoppingToken); } } var finalSettings = await settingsService.GetSettings(); finalSettings.CurrentScanningPage = 0; if (!finalSettings.FinishedInitialScan && !stoppingToken.IsCancellationRequested) { _logger.LogInformation("[{Channel}] Initial scan successfully completed. Switching FinishedInitialScan to true.", "AssetsChannel"); finalSettings.FinishedInitialScan = true; } await settingsService.SaveSettings(finalSettings); _logger.LogInformation("[{Channel}] Full scan cycle completed. Waiting 1 minute before starting the next cycle.", "AssetsChannel"); await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken); } catch (Exception e) when (!stoppingToken.IsCancellationRequested) { _logger.LogError(e, "[{Channel}] An unhandled exception occurred in AssetScannerBackgroundService. Retrying in 10 seconds.", "AssetsChannel"); try { await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); } catch { /* Ignore */ } } } while (!stoppingToken.IsCancellationRequested); _logger.LogInformation("[{Channel}] AssetScannerBackgroundService is stopping.", "AssetsChannel"); } 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("[{Channel}] No assets found for type {AssetType}.", "AssetsChannel", type); return; } _currAssetsCount ??= new AssetsCount(); var currentItemOffset = 0; var settings = await settingsDbService.GetSettings(); var pageSize = Math.Clamp(settings.TradeRepublicMaxRequestPageSize <= 0 ? 50 : settings.TradeRepublicMaxRequestPageSize, 1, 100); if (settings.CurrentScanningType == type && settings.CurrentScanningPage > 0) { currentItemOffset = (settings.CurrentScanningPage - 1) * pageSize; _logger.LogInformation("[{Channel}] Resuming full scan for {AssetType} from Page {Page} (Calculated Offset: {Offset}).", "AssetsChannel", type, settings.CurrentScanningPage, currentItemOffset); } while (currentItemOffset < totalCount && !stoppingToken.IsCancellationRequested) { var currentSettings = await settingsDbService.GetSettings(); pageSize = Math.Clamp(currentSettings.TradeRepublicMaxRequestPageSize <= 0 ? 50 : currentSettings.TradeRepublicMaxRequestPageSize, 1, 100); var currentPage = (currentItemOffset / pageSize) + 1; currentSettings.CurrentScanningType = type; currentSettings.CurrentScanningPage = currentPage; await settingsDbService.SaveSettings(currentSettings); _logger.LogDebug("Fetching {AssetType} - Page {Page}. Numerical Offset: {Offset}/{Total}", type, currentPage, currentItemOffset, totalCount); var assets = await tradeRepublicService.GetAssets(type, currentPage, pageSize, stoppingToken); // Keine Ergebnisse geliefert -> Katalogende erreicht if (assets?.Results == null || assets.Results.Count == 0) { _logger.LogInformation("[{Channel}] Fetch for {AssetType} (Page {Page}) returned no results. Reached end of available assets.", "AssetsChannel", type, currentPage); currentSettings.CurrentScanningPage = 0; await settingsDbService.SaveSettings(currentSettings); break; } await ProcessAssets(assets.Results, assetsDbService, indexService, stoppingToken); _currAssetsCount.SetCountOfType(type, _currAssetsCount.GetCountFromType(type) + assets.Results.Count); currentItemOffset += assets.Results.Count; // Unvollständige Seite -> Letzte Seite abgearbeitet if (assets.Results.Count < pageSize) { _logger.LogInformation("[{Channel}] Reached the last page for {AssetType}.", "AssetsChannel", type); currentSettings.CurrentScanningPage = 0; await settingsDbService.SaveSettings(currentSettings); break; } var delaySeconds = currentSettings.FinishedInitialScan ? currentSettings.BatchAssetUpdateDelay : currentSettings.InitBatchAssetUpdateDelay; if (delaySeconds > 0) { // Angemessener Jitter (0 bis max. 5 Sek. bzw. kleiner als delaySeconds) var maxJitter = Math.Min(5, delaySeconds); var jitter = Random.Shared.Next(0, maxJitter + 1); _logger.LogDebug("Waiting {Delay} seconds before the next batch.", delaySeconds + jitter); await Task.Delay(TimeSpan.FromSeconds(delaySeconds + jitter), stoppingToken); } } _logger.LogInformation("[{Channel}] Finished scanning {AssetType}. Total scanned in this cycle: {Count}/{Total}", "AssetsChannel", type, _currAssetsCount.GetCountFromType(type), totalCount); } 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("[{Channel}] [Scan] {Count} assets passed to the DB service. {Changed} modifications/inserts executed.", "AssetsChannel", assets.Count, changedRows); if (changedRows > 0) { _logger.LogInformation("[{Channel}] Database modifications detected. Recreating the asset index file...", "AssetsChannel"); await indexService.ReCreateIndexFileAsync(stoppingToken); } } }