using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using FinlyticCore.Dtos;
using FinlyticCore.Dtos.Trading;
namespace FinlyticEngine.Services.Trading;
///
/// Coordinates the full trade proposal/trade lifecycle for FinlyticEngine: on-demand evaluation, proposal
/// acceptance/rejection, and management of the resulting active trades (fills, stop-loss updates, closes).
///
public interface ITradeLifecycleService
{
///
/// Returns trade proposals, optionally restricted to still-active, non-expired ones.
///
Task> GetProposalsAsync(bool onlyActive = true, int limit = 50, CancellationToken cancellationToken = default);
///
/// Returns the active trades owned by , optionally filtered by
/// . The filter is applied in the database, so another user's trades are never
/// materialised and a caller cannot widen the result set by omitting a parameter.
///
Task> GetActiveTradesAsync(Guid userId, ExecutionMode? mode = null, CancellationToken cancellationToken = default);
///
/// Runs the full multi-factor evaluation pipeline (technicals, sentiment, fundamentals, simulation feedback,
/// AI reasoning gate) for a single ISIN and persists a new if the opportunity
/// is approved. Unlike the old TradeProposalDto? contract, this never returns :
/// a rejection (score too low, or the AI gate declined) is reported as an
/// with Proposal == null but with the real, already-computed
/// scores and AI reasoning filled in, so a caller always learns *why*, not just *that* no proposal was made
/// (Rules.md §4). When not even a technical setup could be found for the ISIN, the score fields are 0
/// and carries a "[Regelbasiert]"-prefixed
/// explanation rather than a fabricated AI verdict.
///
/// Every call - including the early "no technical setup"/"blank ISIN" returns - now persists exactly one
/// EngineEvaluationSnapshotEntity row tagged with (and
/// when is
/// ), so the admin evaluation-history tab
/// (MqttTopics.Channels.EngineGetEvaluationHistory) can account for every asset this pipeline ever
/// looked at, not only the ones that made it all the way to scoring.
///
///
/// An approval that would otherwise create a second for an ISIN that
/// already has an active, non-expired proposal is deduplicated: no new proposal row is created and no
/// finlytic/engine/proposals/created event is re-broadcast, the persisted snapshot's
/// OutcomeReason is instead of
/// , and the returned is
/// the pre-existing proposal (never ) so a caller still learns about the open
/// opportunity. This exists because the autonomous scanner re-evaluates the same top-picks every cycle and
/// would otherwise create a near-identical proposal (and broadcast) for as long as one asset stays above
/// the approval threshold.
///
///
/// The underlying ISIN to evaluate.
/// Optional ticker hint passed through to the technical/fundamentals lookups.
///
/// When , the AI reasoning gate is consulted even if the composite score is below
/// Engine.MinCompositeScore (used by the manual "Analyze now" Web UI flow).
///
///
/// Whether this call originates from the autonomous OpportunityPollerBackgroundService scan loop
/// (, the default) or an on-demand human request
/// ().
///
///
/// The identity of the human caller when is .
/// Must be for calls - the autonomous scanner
/// never carries a user identity, and this is enforced defensively regardless of what is passed in.
///
/// Propagated to every downstream MQTT/DB call.
Task EvaluateAssetAsync(
string isin,
string? ticker = null,
bool forceAiEvaluation = false,
TriggerSource triggerSource = TriggerSource.Automatic,
Guid? triggeredByUserId = null,
CancellationToken cancellationToken = default);
///
/// Records an additional executed fill against an existing active trade and recalculates its average
/// buy-in, total quantity, fees, and dynamic take-profit levels.
///
///
/// Thrown when no trade with exists for . A trade owned
/// by a different user is reported the same way as a missing one, so ownership is never disclosed.
///
Task AddTradeFillAsync(Guid userId, Guid tradeId, decimal executedPrice, decimal quantity, decimal fee = 0m, string? note = null, CancellationToken cancellationToken = default);
///
/// Manually or algorithmically adjusts the stop-loss of an active trade owned by .
///
///
/// Thrown when no trade with exists for .
///
Task UpdateStopLossAsync(Guid userId, Guid tradeId, decimal newStopLoss, string reason, CancellationToken cancellationToken = default);
///
/// Closes an active trade owned by at the given price and computes its realized P&L.
///
///
/// Thrown when no trade with exists for .
///
Task CloseTradeAsync(Guid userId, Guid tradeId, decimal closePrice, string reason, CancellationToken cancellationToken = default);
///
/// Creates an actively tracked EngineTradeEntity owned by from an open
/// proposal. The source proposal is deliberately left active: a proposal is a system-wide opportunity that
/// several users may accept independently, each receiving their own trade. Proposals are not consumed by
/// acceptance — they disappear on their own once ExpiresAtUtc passes.
///
/// if no active, non-expired proposal with exists.
///
/// Thrown when already holds a trade created from this proposal.
///
Task CreateTradeFromProposalAsync(Guid userId, Guid proposalId, ExecutionMode mode, decimal? initialFillPrice = null, decimal? initialQuantity = null, CancellationToken cancellationToken = default);
///
/// Accepts a proposal on behalf of a single user via the engine_AcceptProposal MQTT RPC channel.
/// Thin wrapper around — see there for the ownership and
/// non-consumption semantics. Declining a proposal deliberately has no counterpart here: it has no
/// server-side effect and is handled entirely in the client.
///
///
/// The proposal does not exist, has expired, or this user already accepted it.
///
Task AcceptProposalAsync(AcceptTradeProposalRequest request, CancellationToken cancellationToken = default);
///
/// Opens an actively tracked trade owned by request.UserId with no backing proposal (manual entry,
/// e.g. from the Web UI). Unlike , the resulting
/// EngineTradeEntity.ProposalId is since there is no proposal to link to.
///
///
/// UnderlyingIsin/Symbol is blank, or EntryPrice/Quantity is not positive.
///
Task CreateManualTradeAsync(CreateManualTradeRequest request, CancellationToken cancellationToken = default);
}