using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using FinlyticBackend.Util; using FinlyticCore.Dtos; using FinlyticCore.Dtos.News; using FinlyticCore.Dtos.Sentiment; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; namespace FinlyticBackend.Controllers; [ApiController] [Authorize] [Route("api/v1/news")] public class NewsController : ControllerBase { private readonly WebMqttClient _mqttClient; private readonly ILogger _logger; public NewsController(WebMqttClient mqttClient, ILogger logger) { _mqttClient = mqttClient; _logger = logger; } /// /// Retrieves paginated news articles with optional filters and enriched sentiment data. /// [HttpGet] public async Task GetNews( [FromQuery] int page = 1, [FromQuery] int pageSize = 20, [FromQuery] string? symbol = null, [FromQuery] string? isin = null, [FromQuery] string? date = null, [FromQuery] string? status = null, [FromQuery] string? query = null, [FromQuery] bool? hasSentiment = null) { string? activeSymbol = !string.IsNullOrWhiteSpace(symbol) ? symbol.Trim() : isin?.Trim(); string? effectiveStatus = status; if (hasSentiment == true && string.IsNullOrEmpty(effectiveStatus)) { effectiveStatus = "Analyzed"; } try { if (_mqttClient.IsConnected) { var payload = new DailyNewsRequest( Limit: pageSize, Offset: (page - 1) * pageSize, Isin: activeSymbol, Date: DateTime.TryParse(date, out var dateTime) ? dateTime : null, Status: effectiveStatus, Query: query, HasSentiment: hasSentiment ); _logger.LogInformation("[NewsController] Fetching news. Date filter: {Date}", payload.Date?.ToString("o") ?? "None"); var articles = await _mqttClient.SendRpcRequestAsync, DailyNewsRequest>( "news_Get", payload, TimeSpan.FromSeconds(10) ); if (articles != null && articles.Count > 0) { // Parallelisierte Anreichung fehlender Sentiment-Daten if (hasSentiment == true || string.Equals(effectiveStatus, "Analyzed", StringComparison.OrdinalIgnoreCase)) { var enrichmentTasks = articles.Select(async article => { if (article.FinbertResult != null) { return article; } try { var sentimentEntry = await _mqttClient.SendRpcRequestAsync( "sentiment_GetArticle", new ArticleRequest(article.Id.ToString(), article.Id.ToString()), TimeSpan.FromSeconds(2) ); if (sentimentEntry?.FinbertResult != null) { var result = sentimentEntry.FinbertResult; return article with { Sentiment = result.Label, SentimentScore = result.CompoundScore, Confidence = result.Confidence, FinbertResult = result }; } } catch { // RPC Timeout oder nicht analysiert -> Un-angereicherten Artikel zurückgeben } return article; }); var enrichedArticles = await Task.WhenAll(enrichmentTasks); return Ok(enrichedArticles); } return Ok(articles); } } else { _logger.LogWarning("[GetNews] Cannot send MQTT RPC - MQTT client is NOT connected to broker."); } } catch (Exception ex) { _logger.LogWarning(ex, "[GetNews] MQTT RPC news_Get request failed for symbol '{Symbol}'", activeSymbol); } return Ok(new List()); } /// /// Retrieves sentiment summary for a specific ISIN. /// [HttpGet("sentiment/isin/{isin}")] public async Task GetIsinSentimentSummary(string isin) { if (string.IsNullOrWhiteSpace(isin)) return NotFound(new { message = "ISIN is required." }); var isinCode = isin.Trim().ToUpperInvariant(); try { if (_mqttClient.IsConnected) { var rpcResult = await _mqttClient.SendRpcRequestAsync( "sentiment_GetIsin", new IsinRequest(isinCode), TimeSpan.FromSeconds(5) ); if (rpcResult != null) { return Ok(rpcResult); } } } catch (Exception ex) { _logger.LogWarning(ex, "MQTT RPC sentiment_GetIsin failed for ISIN '{Isin}'", isinCode); } // File-System Fallback var candidates = new[] { Path.Combine(Directory.GetCurrentDirectory(), "data", "summaries", "isin", $"{isinCode}.json"), Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "data", "summaries", "isin", $"{isinCode}.json"), Path.Combine(Directory.GetCurrentDirectory(), "..", "data", "summaries", "isin", $"{isinCode}.json") }; foreach (var path in candidates) { if (System.IO.File.Exists(path)) { try { var json = await System.IO.File.ReadAllTextAsync(path); return Content(json, "application/json"); } catch { } } } return NotFound(new { message = $"No sentiment summary found for ISIN {isinCode}." }); } /// /// Retrieves sentiment analysis for a specific article. /// [HttpGet("sentiment/article/{articleId}")] public async Task GetArticleSentimentAnalysis(string articleId) { if (string.IsNullOrWhiteSpace(articleId)) return NotFound(new { message = "ArticleId is required." }); var targetId = articleId.Trim(); try { if (_mqttClient.IsConnected) { var rpcResult = await _mqttClient.SendRpcRequestAsync( "sentiment_GetArticle", new ArticleRequest(targetId, targetId), TimeSpan.FromSeconds(5) ); if (rpcResult != null) { return Ok(rpcResult); } } } catch (Exception ex) { _logger.LogWarning(ex, "MQTT RPC sentiment_GetArticle failed for articleId '{ArticleId}'", targetId); } // File-System Fallback var articleCandidates = new[] { Path.Combine(Directory.GetCurrentDirectory(), "data", "summaries", "articles", $"{targetId}.json"), Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "data", "summaries", "articles", $"{targetId}.json") }; foreach (var path in articleCandidates) { if (System.IO.File.Exists(path)) { try { var json = await System.IO.File.ReadAllTextAsync(path); return Content(json, "application/json"); } catch { } } } return NotFound(new { message = $"No sentiment analysis found for article {articleId}." }); } /// /// Triggers or renews sentiment analysis for a specific news article via FinlyticSentiment. /// [HttpPost("sentiment/article/{articleId}/analyze")] [HttpPost("{articleId}/reanalyze")] public async Task ReanalyzeArticleSentiment(string articleId) { if (string.IsNullOrWhiteSpace(articleId)) return BadRequest(new { message = "ArticleId ist erforderlich." }); var targetId = articleId.Trim(); try { if (_mqttClient.IsConnected) { var rpcResult = await _mqttClient.SendRpcRequestAsync( "sentiment_Analyze", new AnalyzeSentimentRequest(ArticleId: targetId, ForceReload: true), TimeSpan.FromSeconds(20) ); if (rpcResult != null) { // Fetch full article from FinlyticNews to return complete updated DTO var article = await _mqttClient.SendRpcRequestAsync( "news_GetById", new ArticleRequest(targetId, targetId), TimeSpan.FromSeconds(5) ); if (article != null && rpcResult.FinbertResult != null) { var res = rpcResult.FinbertResult; var enriched = article with { Sentiment = res.Label, SentimentScore = res.CompoundScore, Confidence = res.Confidence, FinbertResult = res, Status = "Analyzed" }; return Ok(enriched); } if (article != null) { return Ok(article); } // Fallback to synthetic DTO from entry var synthetic = new NewsArticleDto { Id = Guid.TryParse(targetId, out var g) ? g : Guid.NewGuid(), Title = rpcResult.Article?.Title ?? "Artikel", Author = rpcResult.Article?.Source ?? "FinlyticNews", Summary = rpcResult.SummarySnippet, ContentRaw = "", SourceUrl = "", ScrapedAt = DateTime.UtcNow, PublishedAt = DateTime.TryParse(rpcResult.Article?.PublishedAt, out var pDate) ? pDate : DateTime.UtcNow, Status = "Analyzed", Sentiment = rpcResult.FinbertResult?.Label ?? "NEUTRAL", SentimentScore = rpcResult.FinbertResult?.CompoundScore ?? 0.0, Confidence = rpcResult.FinbertResult?.Confidence ?? 0.0, FinbertResult = rpcResult.FinbertResult }; return Ok(synthetic); } } else { return StatusCode(503, new { message = "MQTT Broker nicht verbunden." }); } } catch (Exception ex) { _logger.LogError(ex, "Fehler beim erneuten Analysieren des Artikels {ArticleId}", targetId); return StatusCode(500, new { message = $"Analyse fehlgeschlagen: {ex.Message}" }); } return NotFound(new { message = $"Artikel {targetId} konnte nicht analysiert werden." }); } }