using System; using System.Collections.Generic; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using FinlyticCore.Dtos; using FinlyticCore.Dtos.Settings; using FinlyticCore.Models; using FinlyticCore.Services; using FinlyticCore.Util; using FinlyticTechnicalAnalysis.Services; using FinlyticTechnicalAnalysis.Util; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace FinlyticTechnicalAnalysis.Util; public class TAMqttClient : ManagedMqttClient, IHostedService { private readonly IConfiguration _configuration; private readonly IServiceScopeFactory _scopeFactory; private readonly ILogger _logger; public TAMqttClient( ILogger logger, IConfiguration configuration, IServiceScopeFactory scopeFactory) : base(logger) { _logger = logger; _configuration = configuration; _scopeFactory = scopeFactory; } /// /// Starts the MQTT client. /// public async Task StartAsync(CancellationToken cancellationToken) { var host = _configuration["MQTT:Host"] ?? _configuration["MQTT__Host"] ?? "localhost"; var portStr = _configuration["MQTT:Port"] ?? _configuration["MQTT__Port"] ?? "1883"; var clientId = _configuration["MQTT:ClientId"] ?? "finlytic_ta_" + Guid.NewGuid().ToString("N"); var config = new MqttConfiguration { Host = host, Port = int.TryParse(portStr, out var p) ? p : 1883, ClientId = clientId }; _logger.LogInformation("Starting Technical Analysis MQTT client. Host: {Host}, ClientId: {ClientId}", config.Host, config.ClientId); await ConnectAsync(config); } /// /// Stops the MQTT client. /// public async Task StopAsync(CancellationToken cancellationToken) { _logger.LogInformation("Stopping Technical Analysis MQTT client."); await DisconnectAsync(); } protected override async Task OnConnectedAsync() { _logger.LogInformation("Technical Analysis MQTT client connected. Subscribing to RPC topics..."); await SubscribeAsync("services/request/ta_GetAnalysis/#"); await SubscribeAsync("services/request/tr_GetLivePrice/#"); await SubscribeAsync("services/request/ta_settings_GetAll/#"); await SubscribeAsync("services/request/ta_settings_Update/#"); await SubscribeAsync("services/request/health_Ping/#"); await SubscribeAsync("services/config/updated/#"); FinlyticCore.Services.FinlyticLogBroadcaster.OnLogPublished = async (logDto) => { if (IsConnected && string.Equals(logDto.ServiceName, "FinlyticTechnicalAnalysis", StringComparison.OrdinalIgnoreCase)) { await PublishAsync("finlytic/logs/FinlyticTechnicalAnalysis", logDto); } }; } protected override async Task OnMessageReceivedAsync(string topic, string payload) { if (string.IsNullOrWhiteSpace(topic)) return; if (topic.StartsWith("services/config/updated", StringComparison.OrdinalIgnoreCase)) { await HandleConfigUpdatedAsync(topic, payload); return; } var segments = topic.Split('/'); if (segments.Length < 4) return; var channel = segments[2]; var correlationId = segments[^1]; switch (channel) { case "ta_GetAnalysis": await HandleGetAnalysisAsync(payload, correlationId); break; case "tr_GetLivePrice": await HandleGetLivePriceAsync(payload, correlationId); break; case "ta_settings_GetAll": await HandleSettingsGetAllAsync(correlationId); break; case "ta_settings_Update": await HandleSettingsUpdateAsync(payload, correlationId); break; case "health_Ping": await HandleHealthPingAsync(topic, segments, correlationId); break; default: _logger.LogDebug("Received unhandled RPC channel: {Channel}", channel); break; } } private async Task HandleSettingsGetAllAsync(string correlationId) { using var scope = _scopeFactory.CreateScope(); var finlyticLogger = scope.ServiceProvider.GetRequiredService>(); var settingsService = scope.ServiceProvider.GetRequiredService(); await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticTechnicalAnalysis] [Settings_GetAll] Retrieving all dynamic settings via reflection [CorrelationId: {CorrelationId}]", correlationId); try { var settings = await settingsService.GetAllRegisteredSettingsAsync(new[] { typeof(SettingKeys) }); var responseTopic = $"services/response/ta_settings_GetAll/{correlationId}"; await PublishAsync(responseTopic, settings); await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticTechnicalAnalysis] [Settings_GetAll] Published {Count} settings to '{ResponseTopic}'", settings.Count, responseTopic); } catch (Exception ex) { await finlyticLogger.LogErrorAsync(SettingKeys.MqttChannel, ex, "[FinlyticTechnicalAnalysis] [Settings_GetAll] Failed to retrieve settings."); } } private async Task HandleSettingsUpdateAsync(string payload, string correlationId) { if (string.IsNullOrWhiteSpace(payload)) return; using var scope = _scopeFactory.CreateScope(); var finlyticLogger = scope.ServiceProvider.GetRequiredService>(); var settingsService = scope.ServiceProvider.GetRequiredService(); await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticTechnicalAnalysis] [Settings_Update] Processing settings update RPC [CorrelationId: {CorrelationId}]", correlationId); try { Dictionary? updates = null; try { updates = JsonSerializer.Deserialize>(payload); } catch { var list = JsonSerializer.Deserialize>(payload); if (list != null) { updates = new Dictionary(); foreach (var item in list) updates[item.Key] = item.Value; } } if (updates != null && updates.Count > 0) { await settingsService.UpdateSettingsAsync(updates); await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticTechnicalAnalysis] [Settings_Update] Successfully updated {Count} settings in database and cache.", updates.Count); } var currentSettings = await settingsService.GetAllRegisteredSettingsAsync(new[] { typeof(SettingKeys) }); var responseTopic = $"services/response/ta_settings_Update/{correlationId}"; await PublishAsync(responseTopic, currentSettings); } catch (Exception ex) { await finlyticLogger.LogErrorAsync(SettingKeys.MqttChannel, ex, "[FinlyticTechnicalAnalysis] [Settings_Update] Failed to update settings."); } } private async Task HandleConfigUpdatedAsync(string topic, string payload) { if (!topic.EndsWith("FinlyticTechnicalAnalysis", StringComparison.OrdinalIgnoreCase)) return; try { using var doc = JsonDocument.Parse(payload); if (doc.RootElement.TryGetProperty("settings", out var settingsProp)) { var dict = JsonSerializer.Deserialize>(settingsProp.GetRawText()); if (dict != null && dict.Count > 0) { using var scope = _scopeFactory.CreateScope(); var settings = scope.ServiceProvider.GetRequiredService(); await settings.UpdateSettingsAsync(dict); } } } catch { } } private async Task HandleHealthPingAsync(string topic, string[] segments, string correlationId) { bool isForMe = segments.Length >= 5 ? segments[3].Equals("FinlyticTechnicalAnalysis", StringComparison.OrdinalIgnoreCase) : topic.Contains("FinlyticTechnicalAnalysis", StringComparison.OrdinalIgnoreCase); if (isForMe) { string respTopic = $"services/response/health_Ping/{correlationId}"; await PublishAsync(respTopic, new ServiceHealthResponse("FinlyticTechnicalAnalysis", "Online", DateTime.UtcNow, "Connected")); using var scope = _scopeFactory.CreateScope(); var finlyticLogger = scope.ServiceProvider.GetRequiredService>(); await finlyticLogger.LogInfoAsync(SettingKeys.HealthPingChannel, "[FinlyticTechnicalAnalysis] Responded to live health_Ping RPC request [CorrelationId: {CorrelationId}].", correlationId); } } private async Task HandleGetAnalysisAsync(string payload, string correlationId) { using var scope = _scopeFactory.CreateScope(); var finlyticLogger = scope.ServiceProvider.GetRequiredService>(); await finlyticLogger.LogInfoAsync(SettingKeys.TechnicalAnalysisChannel, "[FinlyticTechnicalAnalysis] Received RPC ta_GetAnalysis request. CorrelationId: {CorrelationId}", correlationId); var req = JsonSerializer.Deserialize(payload, FinlyticJsonSerializerContext.Default.IsinRequest); string responseTopic = $"services/response/ta_GetAnalysis/{correlationId}"; if (string.IsNullOrWhiteSpace(req?.Isin)) { await finlyticLogger.LogWarningAsync(SettingKeys.TechnicalAnalysisChannel, "[FinlyticTechnicalAnalysis] Request missing mandatory ISIN parameter."); await PublishAsync(responseTopic, null); return; } try { var taDbService = scope.ServiceProvider.GetRequiredService(); var analysis = await taDbService.GetAnalysisAsync(req.Isin, req.ForceRefresh, req.Ticker); await finlyticLogger.LogInfoAsync(SettingKeys.TechnicalAnalysisChannel, "[FinlyticTechnicalAnalysis] Publishing RPC response to {ResponseTopic}", responseTopic); await PublishAsync(responseTopic, analysis); } catch (Exception ex) { await finlyticLogger.LogErrorAsync(SettingKeys.TechnicalAnalysisChannel, ex, "[FinlyticTechnicalAnalysis] Failed to fetch technical analysis for ISIN {Isin}", req.Isin); try { await PublishAsync(responseTopic, null); } catch { } } } private async Task HandleGetLivePriceAsync(string payload, string correlationId) { using var scope = _scopeFactory.CreateScope(); var finlyticLogger = scope.ServiceProvider.GetRequiredService>(); await finlyticLogger.LogInfoAsync(SettingKeys.TechnicalAnalysisChannel, "[FinlyticTechnicalAnalysis] Received RPC tr_GetLivePrice request. CorrelationId: {CorrelationId}", correlationId); var req = JsonSerializer.Deserialize(payload, FinlyticJsonSerializerContext.Default.IsinRequest); string responseTopic = $"services/response/tr_GetLivePrice/{correlationId}"; if (string.IsNullOrWhiteSpace(req?.Isin)) { await finlyticLogger.LogWarningAsync(SettingKeys.TechnicalAnalysisChannel, "[FinlyticTechnicalAnalysis] tr_GetLivePrice request missing mandatory ISIN parameter."); await PublishAsync(responseTopic, null); return; } try { var taDbService = scope.ServiceProvider.GetRequiredService(); var livePrice = await taDbService.GetLivePriceAsync(req.Isin); await finlyticLogger.LogInfoAsync(SettingKeys.TechnicalAnalysisChannel, "[FinlyticTechnicalAnalysis] Publishing RPC response to {ResponseTopic} for ISIN {Isin}", responseTopic, req.Isin); await PublishAsync(responseTopic, livePrice); } catch (Exception ex) { await finlyticLogger.LogErrorAsync(SettingKeys.TechnicalAnalysisChannel, ex, "[FinlyticTechnicalAnalysis] Failed to fetch live price for ISIN {Isin}", req.Isin); try { await PublishAsync(responseTopic, null); } catch { } } } }