using System; using System.Collections.Generic; using System.Security.Claims; using System.Threading.Tasks; using FinlyticBackend.Util; using FinlyticCore.Dtos; using FinlyticCore.Dtos.Trading; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; namespace FinlyticBackend.Controllers; [ApiController] [Authorize] [Route("api/v1/engine")] [EnableCors("AllowAll")] public class EngineController : ControllerBase { private readonly WebMqttClient _mqttClient; private readonly ILogger _logger; public EngineController(WebMqttClient mqttClient, ILogger logger) { _mqttClient = mqttClient; _logger = logger; } /// /// Resolves the authenticated caller's identity from the JWT NameIdentifier claim, parsed as a /// exactly like the global user-validation middleware in /// FinlyticBackend/Program.cs. There is no pseudo-identity fallback: every trade in FinlyticEngine /// is tenant-scoped by EngineTradeEntity.UserId, so a request whose identity cannot be established /// must be rejected with 401. /// /// /// The NameIdentifier claim (or its sub/nameid fallbacks) is missing or is not a /// parseable . /// private Guid GetUserIdFromClaims() { var claimUserId = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub") ?? User.FindFirstValue("nameid"); if (Guid.TryParse(claimUserId, out var userId)) { return userId; } _logger.LogWarning("[EngineController] Claim NameIdentifier missing or not a valid GUID for an authenticated request."); throw new UnauthorizedAccessException("The request does not carry a valid user identity claim."); } /// /// Retrieves active or all AI-validated trade proposals generated by FinlyticEngine. Proposals are /// system-wide opportunities owned by no user, so this action is not scoped to the caller's identity. /// [HttpGet("proposals")] public async Task GetProposals([FromQuery] bool onlyActive = true, [FromQuery] int limit = 50) { if (!_mqttClient.IsConnected) { return StatusCode(503, new { error = "FinlyticEngine service is currently unreachable." }); } try { var request = new GetTradeProposalsRequest(onlyActive, limit); var proposals = await _mqttClient.SendRpcRequestAsync, GetTradeProposalsRequest>( "engine_GetProposals", request, TimeSpan.FromSeconds(5)); return Ok(proposals ?? new List()); } catch (Exception ex) { _logger.LogError(ex, "[EngineController] Failed to retrieve trade proposals via MQTT RPC."); return StatusCode(500, new { error = "Internal server error fetching trade proposals." }); } } /// /// Retrieves active trades (positions) owned by the authenticated caller and being tracked or managed by /// FinlyticEngine. /// [HttpGet("trades")] public async Task GetActiveTrades([FromQuery] ExecutionMode? mode = null) { if (!_mqttClient.IsConnected) { return StatusCode(503, new { error = "FinlyticEngine service is currently unreachable." }); } try { var userId = GetUserIdFromClaims(); var request = new GetActiveTradesRequest(UserId: userId, Mode: mode); var trades = await _mqttClient.SendRpcRequestAsync, GetActiveTradesRequest>( "engine_GetTrades", request, TimeSpan.FromSeconds(5)); return Ok(trades ?? new List()); } catch (UnauthorizedAccessException) { return Unauthorized(new { error = "A valid user identity is required." }); } catch (Exception ex) { _logger.LogError(ex, "[EngineController] Failed to retrieve active trades via MQTT RPC."); return StatusCode(500, new { error = "Internal server error fetching active trades." }); } } /// /// Triggers an immediate full-pipeline evaluation (FTA + Sentiment + Fundamentals + AI Gate + Knock-Out Resolver) for an asset. /// Always returns 200 OK with a full body — including the real /// scores and AI reasoning when the evaluation did not clear the bar for a proposal (Proposal == null), /// instead of the previous anonymous placeholder object (Rules.md §3/§4). /// [HttpPost("evaluate")] public async Task EvaluateAsset([FromBody] EvaluateAssetRequest request) { if (string.IsNullOrWhiteSpace(request?.Isin)) { return BadRequest(new { error = "Mandatory ISIN parameter is missing." }); } if (!_mqttClient.IsConnected) { return StatusCode(503, new { error = "FinlyticEngine service is currently unreachable." }); } try { var userId = GetUserIdFromClaims(); // Any UserId supplied by the client in the body is discarded and replaced with the JWT identity, // exactly like every other engine request DTO with a UserId field (see EvaluateAssetRequest's doc // comment) - it is never trusted from the client. var req = request with { UserId = userId }; var evaluation = await _mqttClient.SendRpcRequestAsync( "engine_EvaluateIsin", req, TimeSpan.FromSeconds(15)); if (evaluation == null) { // Null means the RPC call itself got no response (transport failure), not a legitimate // "evaluated, no proposal" outcome - that case is now always a populated DTO. return StatusCode(502, new { error = "FinlyticEngine did not respond to the evaluation request." }); } return Ok(evaluation); } catch (UnauthorizedAccessException) { return Unauthorized(new { error = "A valid user identity is required." }); } catch (Exception ex) { _logger.LogError(ex, "[EngineController] Failed to evaluate asset {Isin} via MQTT RPC.", request.Isin); return StatusCode(500, new { error = "Internal server error evaluating asset." }); } } /// /// Adds an executed fill (partial buy or scale-in) to an existing active trade owned by the authenticated /// caller, triggering dynamic buy-in recalculation. /// [HttpPost("trades/{tradeId:guid}/fills")] public async Task AddTradeFill(Guid tradeId, [FromBody] AddTradeFillRequest request) { if (!_mqttClient.IsConnected) { return StatusCode(503, new { error = "FinlyticEngine service is currently unreachable." }); } try { var userId = GetUserIdFromClaims(); // Any UserId supplied by the client in the body is discarded and replaced with the JWT identity, // so a caller can never add a fill to another user's trade. var req = request with { UserId = userId, TradeId = tradeId }; var updatedTrade = await _mqttClient.SendRpcRequestAsync( "engine_AddFill", req, TimeSpan.FromSeconds(5)); if (updatedTrade != null) { return Ok(updatedTrade); } return NotFound(new { error = $"Trade {tradeId} not found." }); } catch (UnauthorizedAccessException) { return Unauthorized(new { error = "A valid user identity is required." }); } catch (Exception ex) { _logger.LogError(ex, "[EngineController] Failed to add fill to trade {TradeId}.", tradeId); return StatusCode(500, new { error = "Internal server error adding trade fill." }); } } /// /// Manually or algorithmically adjusts the Stop-Loss of an active trade owned by the authenticated caller. /// [HttpPut("trades/{tradeId:guid}/stoploss")] public async Task UpdateStopLoss(Guid tradeId, [FromBody] UpdateTradeStopLossRequest request) { if (!_mqttClient.IsConnected) { return StatusCode(503, new { error = "FinlyticEngine service is currently unreachable." }); } try { var userId = GetUserIdFromClaims(); // Any UserId supplied by the client in the body is discarded and replaced with the JWT identity. var req = request with { UserId = userId, TradeId = tradeId }; var updatedTrade = await _mqttClient.SendRpcRequestAsync( "engine_UpdateStopLoss", req, TimeSpan.FromSeconds(5)); if (updatedTrade != null) { return Ok(updatedTrade); } return NotFound(new { error = $"Trade {tradeId} not found." }); } catch (UnauthorizedAccessException) { return Unauthorized(new { error = "A valid user identity is required." }); } catch (Exception ex) { _logger.LogError(ex, "[EngineController] Failed to update stop loss for trade {TradeId}.", tradeId); return StatusCode(500, new { error = "Internal server error updating stop loss." }); } } /// /// Closes an active trade owned by the authenticated caller at a specific market or exit price. /// [HttpPost("trades/{tradeId:guid}/close")] public async Task CloseTrade(Guid tradeId, [FromBody] CloseEngineTradeRequest request) { if (!_mqttClient.IsConnected) { return StatusCode(503, new { error = "FinlyticEngine service is currently unreachable." }); } try { var userId = GetUserIdFromClaims(); // Any UserId supplied by the client in the body is discarded and replaced with the JWT identity. var req = request with { UserId = userId, TradeId = tradeId }; var closedTrade = await _mqttClient.SendRpcRequestAsync( "engine_CloseTrade", req, TimeSpan.FromSeconds(5)); if (closedTrade != null) { return Ok(closedTrade); } return NotFound(new { error = $"Trade {tradeId} not found." }); } catch (UnauthorizedAccessException) { return Unauthorized(new { error = "A valid user identity is required." }); } catch (Exception ex) { _logger.LogError(ex, "[EngineController] Failed to close trade {TradeId}.", tradeId); return StatusCode(500, new { error = "Internal server error closing trade." }); } } }