using System;
using PolyTrader.Core.Persistence;
using PolyTrader.Core.Trading;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using PolyTraderSharp.Models;
using PolyTrader.Modules.CopyTrading.Logic;
using System.Collections.Concurrent;
using System.Linq;

namespace PolyTraderSharp.Services
{
    public class CopyTradingEngine : BackgroundService
    {
        private readonly TradingState _state;
        private readonly CopyTradingState _copyState;
        private readonly ChannelReader<CopySignal> _signalReader;
        private readonly ChannelWriter<ClosedTrade> _closedTradeWriter;
        private readonly TerminalLogger _logger;
        private readonly IClobClient _clob;
        private readonly PolymarketApiService _api;
        private readonly IPositionRepository _positionRepo;
        private readonly IMarketRepository _marketRepo;
        private readonly IAccountRepository _accountRepo;
        private readonly SellLadderService _sellLadder;
        private readonly IDecisionJournal _journal;
        private readonly IOrderEventLog _orderEvents;
        private readonly ConcurrentDictionary<int, SemaphoreSlim> _accountSemaphores = new();
        private readonly ConcurrentDictionary<int, DateTime> _lastInactiveLogPerTrader = new();

        public CopyTradingEngine(
            TradingState state,
            CopyTradingState copyState,
            ChannelReader<CopySignal> signalReader,
            ChannelWriter<ClosedTrade> closedTradeWriter,
            TerminalLogger logger,
            IClobClient clob,
            PolymarketApiService api,
            IPositionRepository positionRepo,
            IMarketRepository marketRepo,
            IAccountRepository accountRepo,
            SellLadderService sellLadder,
            IDecisionJournal journal,
            IOrderEventLog orderEvents)
        {
            _state = state;
            _copyState = copyState;
            _signalReader = signalReader;
            _closedTradeWriter = closedTradeWriter;
            _logger = logger;
            _clob = clob;
            _api = api;
            _positionRepo = positionRepo;
            _marketRepo = marketRepo;
            _accountRepo = accountRepo;
            _sellLadder = sellLadder;
            _journal = journal;
            _orderEvents = orderEvents;
        }

        /// <summary>
        /// S-0 (Supervisor): schreibt eine Entscheidung strukturiert ins Journal (core_decision_journal).
        /// Ergänzt die bestehenden Freitext-Logs, ersetzt sie nicht. Darf den Trading-Pfad nie brechen
        /// (Robustheit in der Journal-Implementierung).
        /// </summary>
        /// <summary>Kompakter Kontext als JSON (kulturinvariant – kein deutsches Dezimalkomma).</summary>
        private static string Ctx(object o) => System.Text.Json.JsonSerializer.Serialize(o);

        private void Journal(CopySignal signal, AccountState? account, TradeDecision decision,
            DecisionReason reason, string message, string contextJson = "")
        {
            _journal.Write(new DecisionRecord
            {
                SignalId = signal.SignalId,
                ModuleName = "CopyTrading",
                AccountId = account?.AccountId ?? 0,
                IsDemo = account?.IsDemo ?? false,
                SourceTraderId = signal.TraderId,
                TokenId = signal.TokenId,
                MarketSlug = signal.MarketSlug,
                MarketQuestion = signal.MarketQuestion,
                Side = signal.Side,
                SignalPrice = signal.Price,
                MarketEndDate = signal.EndDate,
                Decision = decision,
                Reason = reason,
                Message = message,
                ContextJson = contextJson
            });
        }

        public override async Task StartAsync(CancellationToken cancellationToken)
        {
            _logger.Info("Starte Preload des MarketCache aus der Datenbank um Flaschenhälse zu vermeiden...");
            try
            {
                // Initialize cache for EVERYTHING in DB that is not closed!
                var activeMarkets = _marketRepo.GetActive();
                int loaded = 0;

                foreach (var md in activeMarkets)
                {
                    if (!string.IsNullOrEmpty(md.ClobTokenIds))
                    {
                        try
                        {
                            var tokenIds = System.Text.Json.JsonSerializer.Deserialize<List<string>>(md.ClobTokenIds);
                            if (tokenIds != null)
                            {
                                foreach (var token in tokenIds)
                                {
                                    _state.MarketCache[token] = md;
                                    loaded++;
                                }
                            }
                        }
                        catch { } // Ignore malformed JSON cleanly
                    }
                }
                _logger.Info($"MarketCache Preload abgeschlossen: {loaded} Token herangeführt.");
            }
            catch (Exception ex)
            {
                // DB nicht erreichbar o.ä.: Start NICHT abbrechen (sonst erscheint keine UI).
                // Der Cache füllt sich zur Laufzeit über den MarketSync/Signale nach.
                _logger.Error($"MarketCache Preload übersprungen (DB nicht erreichbar?): {ex.Message}");
            }
            await base.StartAsync(cancellationToken);
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            _logger.Info("CopyTradingEngine Channel Listener started (Concurrent).");
            var semaphore = new SemaphoreSlim(15, 15);

            await foreach (var signal in _signalReader.ReadAllAsync(stoppingToken))
            {
                await semaphore.WaitAsync(stoppingToken);
                
                _ = Task.Run(async () =>
                {
                    try
                    {
                        await ProcessSignalAsync(signal);
                    }
                    catch (Exception ex)
                    {
                        _logger.Error($"Absturz im SignalProcessor: {ex.Message}");
                    }
                    finally
                    {
                        semaphore.Release();
                    }
                }, stoppingToken);
            }
        }

        private async Task ProcessSignalAsync(CopySignal signal)
        {
            if (_state.GlobalTradingPaused)
            {
                _logger.TradeReasoning($"⏸️ Trade {signal.Side} [{signal.MarketQuestion}] ignoriert: GlobalTradingPaused ist aktiv.");
                return;
            }

            // ==========================================
            // GLOBAL EXPENSIVE DB/API MARKET CACHE HYDRATION
            // Runs once per signal, before any account locks!
            // ==========================================
            bool isNegRisk = false;
            if (!string.IsNullOrEmpty(signal.TokenId))
            {
                if (_state.MarketCache.TryGetValue(signal.TokenId, out var cachedData))
                {
                    if (!string.IsNullOrEmpty(cachedData.Slug)) signal.MarketSlug = cachedData.Slug;
                    if (!string.IsNullOrEmpty(cachedData.Question)) signal.MarketQuestion = cachedData.Question;
                    if (cachedData.EndDate.HasValue) signal.EndDate = cachedData.EndDate;
                    isNegRisk = cachedData.NegRisk;
                }
                else
                {
                    try
                    {
                        var marketData = _marketRepo.FindByTokenId(signal.TokenId);

                        if (marketData == null)
                        {
                            var fetchedMarket = await _api.GetMarketByTokenIdAsync(signal.TokenId);
                            if (fetchedMarket != null) { _marketRepo.Upsert(fetchedMarket); marketData = fetchedMarket; }
                        }

                        if (marketData == null && !string.IsNullOrEmpty(signal.MarketSlug) && !signal.MarketSlug.StartsWith("0x"))
                        {
                            var fetchedMarkets = await _api.GetMarketsByEventSlugAsync(signal.MarketSlug);
                            foreach (var fetched in fetchedMarkets) {
                                _marketRepo.Upsert(fetched);
                                if (fetched.ClobTokenIds != null && fetched.ClobTokenIds.Contains(signal.TokenId)) marketData = fetched;
                            }
                        }

                        if (marketData != null)
                        {
                            if (!string.IsNullOrEmpty(marketData.Slug)) signal.MarketSlug = marketData.Slug;
                            if (!string.IsNullOrEmpty(marketData.Question)) signal.MarketQuestion = marketData.Question;
                            if (marketData.EndDate.HasValue) signal.EndDate = marketData.EndDate;
                            isNegRisk = marketData.NegRisk;
                            
                            // Add to Cache for fast lookup
                            _state.MarketCache[signal.TokenId] = marketData;
                        }
                    }
                    catch (Exception ex)
                    {
                        _logger.Warning($"Fehler beim Abrufen von MarketData f\u00fcr Token {signal.TokenId}: {ex.Message}");
                    }
                }
            }
            // ==========================================

            // Internal System Signal (e.g. Demo Auto-Close)
            if (signal.TraderId == 0)
            {
                var sysaccountTasks = new List<Task>();
                foreach (var account in _state.Accounts.Values.Where(a => a.IsDemo && a.IsActive))
                {
                    if (account.OpenPositions.ContainsKey(signal.TokenId))
                    {
                        sysaccountTasks.Add(ProcessAccountOrderAsync(account, null, signal, isNegRisk));
                    }
                }
                await Task.WhenAll(sysaccountTasks);
                return;
            }

            if (!_copyState.Traders.TryGetValue(signal.TraderId, out var trader) || !trader.IsActive)
            {
                _logger.TradeReasoning($"\u23f8\ufe0f Trade {signal.Side} [{signal.MarketQuestion}] ignoriert: Trader (ID={signal.TraderId}) nicht gefunden oder inaktiv.");
                return;
            }

            var accountTasks = new List<Task>();

            foreach (var accountId in trader.AssignedAccountIds)
            {
                if (!_state.Accounts.TryGetValue(accountId, out var account) || !account.IsActive)
                {
                    _logger.TradeReasoning($"\u23f8\ufe0f Trade {signal.Side} [{signal.MarketQuestion}] ignoriert: Account (ID={accountId}) nicht gefunden oder inaktiv.");
                    continue;
                }

                accountTasks.Add(ProcessAccountOrderAsync(account, trader, signal, isNegRisk));
            }

            await Task.WhenAll(accountTasks);
        }

        internal async Task ProcessAccountOrderAsync(AccountState account, TrackedTrader? trader, CopySignal signal, bool isNegRisk)
        {
            // Copytrading-Detail-Einstellungen (Limits) dieses Accounts.
            var settings = _copyState.GetAccountSettings(account.AccountId);

            var mode = account.IsDemo ? _state.DemoTradingMode : _state.LiveTradingMode;
            if (mode == TradingMode.Inactive)
            {
                // Rate-limited log: max 1 per trader per 60s to prevent HF spam
                var traderId = signal.TraderId;
                var now = DateTime.UtcNow;
                if (!_lastInactiveLogPerTrader.TryGetValue(traderId, out var lastLog) || (now - lastLog).TotalSeconds >= 60)
                {
                    _lastInactiveLogPerTrader[traderId] = now;
                    string modeLabel = account.IsDemo ? "Demo" : "Live";
                    _logger.TradeReasoning($"⏸️ Trade {signal.Side} [{signal.MarketQuestion}] [{(string.IsNullOrEmpty(signal.Outcome) ? signal.Side : signal.Outcome)}] ignoriert:\n" +
                                           $"   Konto:      {account.Name}\n" +
                                           $"   Begründung: {modeLabel}-Trading Modus ist 'Inactive'. Weitere Trades dieses Traders werden für 60s nicht geloggt.");
                    Journal(signal, account, TradeDecision.Skipped, DecisionReason.ModeInactive,
                        $"{modeLabel}-Trading 'Inactive' (Journal ratenbegrenzt wie das Log: 1/60s je Trader)");
                }
                return;
            }

            // Restrict BUY operations if mode is SellOnly
            if (mode == TradingMode.SellOnly && signal.Side == "BUY")
            {
                _logger.TradeReasoning($"⏸️ Trade BUY [{signal.MarketQuestion}] [{(string.IsNullOrEmpty(signal.Outcome) ? signal.Side : signal.Outcome)}] ignoriert:\n" +
                                       $"   Konto:      {account.Name}\n" +
                                       $"   Begründung: Trading Modus ist 'SellOnly' — BUY-Trades werden nicht kopiert.");
                Journal(signal, account, TradeDecision.Skipped, DecisionReason.SellOnlyModeBuyBlocked, "SellOnly-Modus blockiert BUY");
                return;
            }

            string shareType = string.IsNullOrEmpty(signal.Outcome) ? signal.Side : signal.Outcome;

            var accountSemaphore = _accountSemaphores.GetOrAdd(account.AccountId, _ => new SemaphoreSlim(1, 1));
            await accountSemaphore.WaitAsync();

            try
            {
                // ==========================================
                // OPEN ORDER CLEANUP (LIVE ACCOUNTS ONLY)
                // ==========================================
                // Wenn ein neues Signal für diesen Markt reinkommt, prüfen wir auf veraltete offene Orders.
                // Identische Preise bleiben bestehen. Abweichende verhindern ungültiges Blockieren von Funds.
                if (!account.IsDemo && !string.IsNullOrEmpty(signal.TokenId))
                {
                    // H2: Läuft für diesen Markt eine SELL-Eskalationsleiter, gehört die ruhende Order der
                    // Leiter. Der Pre-Signal-Cleanup würde sie sonst bedingungslos canceln – danach liegt bis
                    // zur nächsten Leiter-Stufe (20/120s) keine Order im Markt und die Leiter merkt nichts.
                    string ladderKey = $"{account.AccountId}_{signal.TokenId}";
                    if (_copyState.ExitLadders.ContainsKey(ladderKey))
                    {
                        _logger.TradeReasoning($"⏭️ [Order-Cleanup übersprungen] {account.Name} | {signal.MarketQuestion}: aktive SELL-Leiter besitzt die Order.");
                    }
                    else if (account.HasOpenLimitOrders)
                    {
                        await _clob.CancelConflictingOrdersAsync(account, signal.TokenId, signal.Price, signal.Side);
                    }
                }

                if (_state.MarketCache.TryGetValue(signal.TokenId, out var fastCachedData))
                {
                    if (fastCachedData.EndDate.HasValue) signal.EndDate = fastCachedData.EndDate;
                }

                // ==========================================
                // PRE-FLIGHT RISK CHECKS (Before DB/API!)
                // ==========================================
                decimal exactShares = 0m;
                decimal exactUsdc = 0m;
                decimal orderPrice = signal.Price;
                
                if (signal.Side == "BUY")
                {
                    // H3 (Entscheidung Richard): Läuft für diese Position bereits eine SELL-Eskalationsleiter
                    // (ExitPending), NICHT zukaufen. Sonst stockt der BUY die Position auf, die die Leiter
                    // danach inkl. der neuen Shares zum alten Floor verkauft (ökonomisch widersprüchlich).
                    // Wir überspringen den BUY und lassen die Leiter den Exit sauber zu Ende führen.
                    if (account.OpenPositions.TryGetValue(signal.TokenId, out var exitingPos) && exitingPos.ExitPending)
                    {
                        _logger.TradeReasoning($"⏸️ Trade BUY [{signal.MarketQuestion}] [{shareType}] übersprungen:\n" +
                                               $"   Konto:      {account.Name}\n" +
                                               $"   Begründung: SELL-Eskalationsleiter läuft (Exit aktiv) – kein Zukauf während des Ausstiegs.");
                        Journal(signal, account, TradeDecision.Skipped, DecisionReason.ExitPendingBuySkip, "SELL-Leiter aktiv – kein Zukauf (H3)");
                        return;
                    }

                    if (signal.Price > settings.MaxBuyPrice)
                    {
                        _logger.TradeReasoning($"❌ Trade BUY [{signal.MarketQuestion}] [{shareType}] verworfen (Risk Limit):\n" +
                                               $"   Konto:      {account.Name}\n" +
                                               $"   Begründung: Preis (${signal.Price:F3}) übersteigt das MaxBuy Limit (${settings.MaxBuyPrice:F3})");
                        Journal(signal, account, TradeDecision.Rejected, DecisionReason.MaxBuyPriceExceeded,
                            $"Preis {signal.Price:F3} > MaxBuyPrice {settings.MaxBuyPrice:F3}",
                            Ctx(new { price = signal.Price, maxBuyPrice = settings.MaxBuyPrice }));
                        return;
                    }

                    var activePositions = account.OpenPositions.Values.Where(p => IsPositionMarketActive(p)).ToList();
                    decimal investedInMarket = activePositions.FirstOrDefault(p => p.TokenId == signal.TokenId)?.AmountUsd ?? 0m;
                    
                    decimal minTrade = 1.0m;
                        // Markt-Budget inkl. Low-Balance-Bypass (CopyTradingRisk, unit-getestet)
                        decimal maxAllowed = CopyTradingRisk.MaxPerMarket(account.TotalBalance, account.AvailableBalance, settings.PerMarketLimit);

                        if (_copyState.SixSharesMinimum && account.TotalBalance < 500m)
                        {
                            // Adjust maxAllowed to cover at least 6 shares * order limit price.
                            decimal orderPriceForSix = CopyTradingRisk.CalculateBuyOrderPrice(
                                signal.Price, trader != null && trader.Category == "HF", settings.MaxPriceDifference, settings.MaxBuyPrice);
                            decimal costSix = 6m * orderPriceForSix;
                            
                            if (costSix > maxAllowed)
                            {
                                maxAllowed = Math.Min(costSix, Math.Max(account.AvailableBalance, 0m));
                            }
                        }

                    decimal maxAmountToBuy = maxAllowed - investedInMarket;
                    
                    decimal investedInMaster = trader != null ? activePositions.Where(p => p.SourceTraderId == trader.Id).Sum(p => (decimal)p.AmountUsd) : 0m;
                        
                        decimal maxAllowedPerMaster = account.TotalBalance * (settings.PerMasterLimit / 100.0m);

                        if (trader != null && (investedInMaster + maxAmountToBuy) > maxAllowedPerMaster)
                        {
                            decimal pctInvested = account.TotalBalance > 0 ? (investedInMaster / account.TotalBalance) * 100m : 0m;
                            _logger.TradeReasoning($"❌ Trade BUY [{signal.MarketQuestion}] [{shareType}] verworfen:\n" +
                                                   $"   Konto:      {account.Name}\n" +
                                                   $"   Begründung: PerMasterLimit ({settings.PerMasterLimit:F1}%) erreicht. Bisher investiert in '{trader.DisplayName}': ${investedInMaster:F2} ({pctInvested:F1}%).");
                            Journal(signal, account, TradeDecision.Rejected, DecisionReason.PerMasterLimitReached,
                                $"PerMasterLimit {settings.PerMasterLimit:F1}% erreicht",
                                Ctx(new { investedInMaster, maxAllowedPerMaster, perMasterLimitPct = settings.PerMasterLimit }));
                            return;
                        }

                        // Time Limit Restriktion (Zeitfenster-Logik: CopyTradingRisk, unit-getestet)
                        var nowUtc = DateTime.UtcNow;
                        double hoursLeft = signal.EndDate.HasValue ? (signal.EndDate.Value - nowUtc).TotalHours : 999999;
                        var timeBucket = CopyTradingRisk.ResolveTimeBucket(hoursLeft);
                        decimal applicableTimeLimitPct = CopyTradingRisk.TimeLimitPct(timeBucket, settings);
                        string timeframeLabel = CopyTradingRisk.TimeBucketLabel(timeBucket);
                        decimal investedInTimeframe = activePositions
                            .Where(p => CopyTradingRisk.IsPositionInBucket(p.ExpiryDate, timeBucket, nowUtc))
                            .Sum(p => (decimal)p.AmountUsd);

                        decimal maxAllowedTimeframe = account.TotalBalance * (applicableTimeLimitPct / 100.0m);

                        if ((investedInTimeframe + maxAmountToBuy) > maxAllowedTimeframe)
                        {
                            decimal remainingForTimeframe = maxAllowedTimeframe - investedInTimeframe;
                            if (remainingForTimeframe < minTrade)
                            {
                                _logger.TradeReasoning($"❌ Trade BUY [{signal.MarketQuestion}] [{shareType}] verworfen:\n" +
                                                       $"   Konto:      {account.Name}\n" +
                                                       $"   Begründung: TimeLimit '{timeframeLabel}' ({applicableTimeLimitPct:F1}%) erreicht. Bisher investiert: ${investedInTimeframe:F2} / max. ${maxAllowedTimeframe:F2}");
                                Journal(signal, account, TradeDecision.Rejected, DecisionReason.TimeWindowLimitReached,
                                    $"TimeLimit '{timeframeLabel}' ({applicableTimeLimitPct:F1}%) erreicht",
                                    Ctx(new { bucket = timeframeLabel, investedInTimeframe, maxAllowedTimeframe, hoursLeft }));
                                return;
                            }
                            else
                            {
                                maxAmountToBuy = remainingForTimeframe;
                            }
                        }

                        if (maxAmountToBuy < minTrade)
                        {
                            _logger.TradeReasoning($"❌ Trade BUY [{signal.MarketQuestion}] [{shareType}] verworfen:\n" +
                                                   $"   Konto:      {account.Name}\n" +
                                                   $"   Begründung: Kauflimit (${maxAllowed:F2}) bereits in Markt investiert (${investedInMarket:F2}). Rest: ${maxAmountToBuy:F2} < MinTrade (${minTrade:F2})");
                            Journal(signal, account, TradeDecision.Rejected, DecisionReason.MarketBudgetExhausted,
                                $"Markt-Budget erschöpft (Rest {maxAmountToBuy:F2} < MinTrade)",
                                Ctx(new { maxAllowed, investedInMarket, remaining = maxAmountToBuy, minTrade }));
                            return;
                        }

                        if (maxAmountToBuy > account.AvailableBalance)
                        {
                            _logger.TradeReasoning($"❌ Trade BUY [{signal.MarketQuestion}] [{shareType}] verworfen:\n" +
                                                   $"   Konto:      {account.Name}\n" +
                                                   $"   Begründung: Kontostand (${account.AvailableBalance:F2}) nicht ausreichend für errechnetes Size (${maxAmountToBuy:F2})");
                            Journal(signal, account, TradeDecision.Rejected, DecisionReason.InsufficientBalance,
                                "Verfügbares Guthaben reicht nicht",
                                Ctx(new { availableBalance = account.AvailableBalance, requested = maxAmountToBuy }));
                            return;
                        }

                        // Limit-Preis (HF-fest / prozentual, gedeckelt) – CopyTradingRisk, unit-getestet.
                        // Phase 4.1: Maker-Einstieg platziert OHNE Aufschlag (ruhendes GTC-Limit am
                        // Master-Preis statt teurem Taker-Fill über dem Ask) – spart Fees/Spread bei
                        // langsamen Mastern. (Ohne Orderbuch ist der Master-Preis die Näherung für den Bid.)
                        bool makerEntry = trader != null && trader.MakerEntry;
                        orderPrice = makerEntry
                            ? CopyTradingRisk.CalculateBuyOrderPrice(signal.Price, false, 0m, settings.MaxBuyPrice)
                            : CopyTradingRisk.CalculateBuyOrderPrice(signal.Price, trader != null && trader.Category == "HF", settings.MaxPriceDifference, settings.MaxBuyPrice);

                        var exact = PolymarketClobClient.CalculateExactOrderAmounts(maxAmountToBuy, orderPrice, orderPrice, "BUY");
                        if (exact.shares <= 0 || exact.usdc > account.AvailableBalance)
                        {
                            _logger.TradeReasoning($"❌ Trade BUY [{signal.MarketQuestion}] [{shareType}] gestoppt:\n" +
                                                   $"   Begründung: Mathematisch unmöglicher Trade ({exact.shares} Shares für ${exact.usdc:F2}). Kontostand (${account.AvailableBalance:F2}) reicht für Minimum nicht aus.");
                            Journal(signal, account, TradeDecision.Rejected, DecisionReason.BelowPolymarketMinimum,
                                "Mathematisch unmöglicher Trade (Balance reicht für Minimum nicht)",
                                Ctx(new { shares = exact.shares, usdc = exact.usdc, availableBalance = account.AvailableBalance }));
                            return;
                        }

                        // ===== MICRO-ORDER FILTER: Polymarket Minimum Size Enforcement =====
                        // Polymarket lehnt Orders mit < 5 Shares ab ("Size lower than the minimum: 5").
                        // Statt die API zu belasten und Fehler-Logs zu erzeugen, filtern wir hier sofort.
                        if (CopyTradingRisk.IsBelowPolymarketMinimum(exact.shares, exact.usdc))
                        {
                            _logger.TradeReasoning($"❌ Trade BUY [{signal.MarketQuestion}] [{shareType}] gestoppt:\n" +
                                                   $"   Konto:      {account.Name}\n" +
                                                   $"   Begründung: Unter Polymarket Minimum ({exact.shares:F1} Shares / ${exact.usdc:F2} USDC). Min: 5.5 Shares / $0.10.");
                            Journal(signal, account, TradeDecision.Rejected, DecisionReason.BelowPolymarketMinimum,
                                $"Unter Polymarket-Minimum ({exact.shares:F1} Shares / {exact.usdc:F2} USDC)",
                                Ctx(new { shares = exact.shares, usdc = exact.usdc }));
                            return;
                        }

                    exactShares = exact.shares;
                    exactUsdc = exact.usdc;

                        // Phase 0.2: erwartete Taker-Fee transparent loggen (API-Satz bevorzugt,
                        // sonst Kategorie-Fallback). Wird im Zielland gegen echte API-Fees verifiziert.
                        int feeBps = _state.MarketCache.TryGetValue(signal.TokenId, out var feeMd)
                            ? (feeMd.TakerFeeBps > 0 ? feeMd.TakerFeeBps : FeeModel.FallbackBps(feeMd.Category))
                            : FeeModel.FallbackBps(null);
                        decimal expectedFee = FeeModel.FeeUsd(exactUsdc, feeBps);
                        _logger.TradeReasoning($"💸 [FEE] {account.Name} | {signal.MarketQuestion}: erwartete Taker-Fee ~${expectedFee:F4} ({feeBps} bps auf ${exactUsdc:F2}).");
                }
            else if (signal.Side == "SELL")
            {
                // PRE-FLIGHT SELL Check: Exists in portfolio AND opened by the SAME master trader?
                // CRITICAL: We must NOT sell a position opened by Trader A based on a SELL signal from Trader B.
                // K3: System-Signale (TraderId == 0, Demo-Auto-Close bei Marktauflösung) sind vom
                // Ownership-Check ausgenommen (CopyTradingRisk.IsAuthorizedSell) – sonst wird die Position
                // hier als "gehört anderem Trader" abgewiesen und schließt bei Resolution nie.
                var inPortfolio = account.OpenPositions.Values.FirstOrDefault(p =>
                    (p.TokenId == signal.TokenId || (p.MarketSlug == signal.MarketSlug && p.Outcome == signal.Outcome))
                    && CopyTradingRisk.IsAuthorizedSell(signal.TraderId, p.SourceTraderId));
                if (inPortfolio == null)
                {
                    // Check if position exists but belongs to a different trader (for clearer logging)
                    var wrongTraderPos = account.OpenPositions.Values.FirstOrDefault(p => 
                        p.TokenId == signal.TokenId || (p.MarketSlug == signal.MarketSlug && p.Outcome == signal.Outcome));
                    if (wrongTraderPos != null)
                    {
                        _logger.Info($"❌ Trade SELL [{signal.MarketQuestion}] [{shareType}] ignoriert:\n" +
                                     $"   Konto:      {account.Name}\n" +
                                     $"   Begründung: Position gehört Trader '{wrongTraderPos.SourceTraderName}' (ID {wrongTraderPos.SourceTraderId}), SELL kam aber von Trader ID {signal.TraderId}.");
                        Journal(signal, account, TradeDecision.Rejected, DecisionReason.OwnershipMismatch,
                            $"Position gehört Trader {wrongTraderPos.SourceTraderId}, SELL kam von {signal.TraderId}",
                            Ctx(new { positionOwner = wrongTraderPos.SourceTraderId, signalTrader = signal.TraderId }));
                    }
                    else
                    {
                        _logger.Info($"❌ Trade SELL [{signal.MarketQuestion}] [{shareType}] ignoriert:\n" +
                                     $"   Konto:      {account.Name}\n" +
                                     $"   Begründung: Position nicht im Portfolio gefunden (möglicherweise zuvor gefiltert).");
                        Journal(signal, account, TradeDecision.Rejected, DecisionReason.PositionNotFound,
                            "Keine Position im Portfolio (möglicherweise zuvor gefiltert)");
                    }
                    return;
                }

                // PROPORTIONALITY CHECK: Only copy SELL if the master trader is exiting a significant portion (≥30%) of their position.
                // Active day-traders like SwissTony buy 500 shares then sell 2 (0.4%) — we should NOT copy that.
                // But if they sell 200 of 500 (40%), that's a real exit signal we must copy.
                string masterPosKey = $"{signal.TraderId}_{inPortfolio.TokenId}";
                if (_copyState.MasterTraderPositions.TryGetValue(masterPosKey, out var masterPos))
                {
                    decimal masterShares = masterPos.Shares;
                    if (masterShares > 0 && signal.Size > 0)
                    {
                        // Calculate what percentage of the master's known position this SELL represents
                        // (Proportionalität: SellLogic, unit-getestet)
                        decimal sellRatio = SellLogic.SellProportion(masterShares, signal.Size);
                        if (sellRatio < SellLogic.MinSignificantSellRatio)
                        {
                            _logger.TradeReasoning($"📊 Trade SELL [{signal.MarketQuestion}] [{shareType}] ignoriert:\n" +
                                                   $"   Konto:      {account.Name}\n" +
                                                   $"   Begründung: Teilverkauf ({sellRatio:P0} des Bestands). Master hält noch {masterShares:F1} Shares. Signal nur {signal.Size:F1} Shares. Schwelle: 30%.");
                            Journal(signal, account, TradeDecision.Rejected, DecisionReason.PartialSellBelowThreshold,
                                $"Teilverkauf {sellRatio:P0} unter Schwelle",
                                Ctx(new { sellRatio, masterShares, signalSize = signal.Size }));
                            return;
                        }
                        _logger.TradeReasoning($"📊 Trade SELL [{signal.MarketQuestion}] [{shareType}] FREIGEGEBEN:\n" +
                                               $"   Konto:      {account.Name}\n" +
                                               $"   Begründung: Signifikanter Verkauf ({sellRatio:P0} des Bestands). Master hatte {masterShares + signal.Size:F1} Shares, verkauft {signal.Size:F1}.");
                    }
                    else if (masterShares <= 0)
                    {
                        // Master has 0 shares according to our tracking, but a SELL signal came in.
                        // This is an inconsistency — either our tracking is stale, or it's a phantom signal.
                        // Do NOT sell blindly. Wait for the next background sync to update the real position.
                        _logger.TradeReasoning($"📊 Trade SELL [{signal.MarketQuestion}] [{shareType}] ignoriert:\n" +
                                               $"   Konto:      {account.Name}\n" +
                                               $"   Begründung: Master hält laut Tracking 0 Shares, aber SELL Signal mit {signal.Size:F1} Shares erhalten. Inkonsistenz — ignoriert.");
                        Journal(signal, account, TradeDecision.Rejected, DecisionReason.MasterPositionInconsistent,
                            "Master hält laut Tracking 0 Shares (Inkonsistenz)",
                            Ctx(new { signalSize = signal.Size }));
                        return;
                    }
                }
                else
                {
                    // No tracking data yet — apply soft grace period (2 min) as fallback until first sync completes
                    double holdingMinutes = (DateTime.UtcNow - inPortfolio.OpenedAt).TotalMinutes;
                    if (holdingMinutes < 2.0)
                    {
                        _logger.TradeReasoning($"⏳ Trade SELL [{signal.MarketQuestion}] [{shareType}] ignoriert:\n" +
                                               $"   Konto:      {account.Name}\n" +
                                               $"   Begründung: Kein Master-Position-Tracking verfügbar und Haltezeit erst {holdingMinutes:F1} Min. Warte auf ersten Sync.");
                        Journal(signal, account, TradeDecision.Skipped, DecisionReason.SyncGracePeriod,
                            $"Kein Tracking, Haltezeit {holdingMinutes:F1} Min < Schonfrist",
                            Ctx(new { holdingMinutes }));
                        return;
                    }
                }
            }

            // Market Metadata loaded globally. Ready for execution.
            // If BUY -> Invest
            if (signal.Side == "BUY")
            {
                    if (account.IsDemo)
                    {
                        var pos = new Position
                        {
                            TokenId = signal.TokenId,
                            MarketSlug = signal.MarketSlug,
                            SourceTraderId = trader?.Id ?? 0,
                            SourceTraderName = trader?.DisplayName ?? "System",
                            SourceTraderAddress = trader?.WalletAddress ?? "",
                            MarketQuestion = signal.MarketQuestion,
                            Outcome = signal.Outcome,
                            Side = "BUY",
                            EntryPrice = orderPrice,
                            Size = exactShares,
                            AmountUsd = exactUsdc,
                            ExpiryDate = signal.EndDate ?? DateTime.UtcNow.AddDays(14)
                        };

                        _copyState.GetNextTradeId();
                        
                        var finalPos = account.OpenPositions.AddOrUpdate(signal.TokenId, pos, (k, old) =>
                        {
                            old.Size += pos.Size;
                            old.AmountUsd += pos.AmountUsd;
                            old.EntryPrice = TradeMath.WeightedAverageEntryPrice(old.AmountUsd, old.Size); // weighted average
                            return old;
                        });

                        _positionRepo.UpsertDemo(account.AccountId, finalPos);

                        // Entry-Fee auch im Demo abziehen, damit Balance und PnL konsistent bleiben (siehe Close).
                        int demoBuyFeeBps = _state.MarketCache.TryGetValue(signal.TokenId, out var demoBuyMd)
                            ? FeeModel.ResolveBps(demoBuyMd.TakerFeeBps, demoBuyMd.Category)
                            : FeeModel.FallbackBps(null);
                        decimal demoEntryFee = FeeModel.FeeUsd(exactUsdc, demoBuyFeeBps);
                        account.UpdateBalance(account.AvailableBalance - exactUsdc - demoEntryFee);
                        _accountRepo.Upsert(account);
                        _logger.Trade($"✅ [DEMO AUSGEFÜHRT]\n" +
                                      $"   Konto: {account.Name}\n" +
                                      $"   Markt: {signal.MarketQuestion}\n" +
                                      $"   BUY:   {exactShares:F4} Shares [{shareType}] @ ${orderPrice:F3} (Gesamt: ${exactUsdc:F2})");
                        Journal(signal, account, TradeDecision.Executed, DecisionReason.DemoFilled,
                            $"Demo-BUY {exactShares:F2} Shares @ {orderPrice:F3}",
                            Ctx(new { shares = exactShares, usdc = exactUsdc, orderPrice }));
                    }
                    else
                    {
                        _logger.Info($"🌐 [LIVE-EXECUTION] Sende MARKET BUY an Polymarket CTF-Router...\n" +
                                     $"   Account: {account.Name}\n" +
                                     $"   Limit:   ${orderPrice:F3} (Target: {signal.Price:F3})");
                        
                        // Maker-Einstieg (Phase 4.1) ruht als GTC-Limit; sonst GTD (Taker-nah).
                        string buyOrderType = (trader != null && trader.MakerEntry) ? "GTC" : "GTD";
                        // M6: realen Taker-Satz (bzw. Kategorie-Fallback) in die signierte Order schreiben.
                        int buyFeeBps = _state.MarketCache.TryGetValue(signal.TokenId, out var buyMd)
                            ? FeeModel.ResolveBps(buyMd.TakerFeeBps, buyMd.Category)
                            : FeeModel.FallbackBps(null);
                        var result = await _clob.PlaceOrderAsync(account, signal.TokenId, signal.Side, exactUsdc, orderPrice, buyOrderType, _state.DebugOrderPayloadLog, isNegRisk, buyFeeBps);

                        // S-0: Order-Lifecycle als Daten (core_order_events).
                        _orderEvents.Write(new OrderEventRecord
                        {
                            SignalId = signal.SignalId,
                            ModuleName = "CopyTrading",
                            AccountId = account.AccountId,
                            TokenId = signal.TokenId,
                            EventType = result == "OK" ? OrderEventType.Placed : OrderEventType.PlaceFailed,
                            Side = "BUY",
                            Price = orderPrice,
                            AmountUsd = exactUsdc,
                            OrderType = buyOrderType,
                            Response = result,
                            DetailsJson = Ctx(new { feeBps = buyFeeBps, signalPrice = signal.Price })
                        });

                        if (result == "OK")
                        {
                            var pos = new Position
                            {
                                TokenId = signal.TokenId,
                                MarketSlug = signal.MarketSlug,
                                SourceTraderId = trader?.Id ?? 0,
                                SourceTraderName = trader?.DisplayName ?? "System",
                                SourceTraderAddress = trader?.WalletAddress ?? "",
                                MarketQuestion = signal.MarketQuestion,
                                Outcome = signal.Outcome,
                                Side = "BUY",
                                EntryPrice = orderPrice, // Real execution price will update on next SyncOpenPositions poll
                                Size = exactShares,
                                AmountUsd = exactUsdc,
                                ExpiryDate = signal.EndDate ?? DateTime.UtcNow.AddDays(14)
                            };

                            _copyState.GetNextTradeId();
                            account.OpenPositions.AddOrUpdate(signal.TokenId, pos, (k, old) =>
                            {
                                old.Size += pos.Size;
                                old.AmountUsd += pos.AmountUsd;
                                old.EntryPrice = TradeMath.WeightedAverageEntryPrice(old.AmountUsd, old.Size);
                                return old;
                            });

                            account.UpdateBalance(account.AvailableBalance - exactUsdc);
                            _accountRepo.Upsert(account);

                            if (account.OpenPositions.TryGetValue(signal.TokenId, out var savedPos))
                            {
                                _positionRepo.UpsertLive(account.AccountId, savedPos);
                            }

                            // Track order placement time for stale order cleanup
                            string orderKey = $"{account.AccountId}_{signal.TokenId}";
                            _copyState.PendingOrderTimestamps[orderKey] = (DateTime.UtcNow, signal.TraderId, "BUY");

                            // Initialize master position tracking with signal size if not yet tracked
                            // The background sync will update with the real value within 30 seconds
                            string masterKey = $"{signal.TraderId}_{signal.TokenId}";
                            _copyState.MasterTraderPositions.TryAdd(masterKey, (signal.Size, DateTime.UtcNow));

                            Journal(signal, account, TradeDecision.Executed, DecisionReason.OrderPlaced,
                                $"Live-BUY {buyOrderType} {exactShares:F2} Shares @ {orderPrice:F3}",
                                Ctx(new { shares = exactShares, usdc = exactUsdc, orderPrice, orderType = buyOrderType }));
                        }
                        else
                        {
                            Journal(signal, account, TradeDecision.Failed, DecisionReason.OrderFailed,
                                $"Live-BUY fehlgeschlagen: {result}",
                                Ctx(new { usdc = exactUsdc, orderPrice, orderType = buyOrderType }));
                        }
                    }
                }
                // If SELL -> Divest
                else if (signal.Side == "SELL")
                {
                    string orderKey = $"{account.AccountId}_{signal.TokenId}";
                    if (_copyState.PendingOrderTimestamps.TryGetValue(orderKey, out var pendingInfo))
                    {
                        // Nur ein KÜRZLICHER SELL blockiert (Doppel-SELL/Retry-Spam); ein vorheriger
                        // BUY blockiert einen legitimen schnellen SELL des Masters NICHT mehr.
                        if (pendingInfo.Side == "SELL" && (DateTime.UtcNow - pendingInfo.PlacedAt).TotalSeconds < 20)
                        {
                            Journal(signal, account, TradeDecision.Skipped, DecisionReason.SellSpamBlock,
                                "SELL <20s nach letztem SELL (Spam-Blockade)");
                            return; // Spam-Blockade: SELL wurde in den letzten 20 Sekunden bereits versendet
                        }
                    }

                    // Läuft für diese Position bereits eine SELL-Eskalationsleiter (Phase 0.1)?
                    // Dann NICHT erneut verkaufen – der SellLadderService preist selbst nach.
                    if (account.OpenPositions.TryGetValue(signal.TokenId, out var laddering) && laddering.ExitPending)
                    {
                        _logger.Info($"SELL [{signal.MarketQuestion}] ignoriert: Eskalationsleiter läuft bereits (Konto {account.Name}).");
                        Journal(signal, account, TradeDecision.Skipped, DecisionReason.LadderAlreadyActive, "Eskalationsleiter läuft bereits");
                        return;
                    }

                    bool removed = account.OpenPositions.TryRemove(signal.TokenId, out var openPos);

                    // Defense-in-depth: Verify the removed position actually belongs to this trader.
                    // K3: System-Signale (TraderId == 0, z.B. Demo-Auto-Close bei Marktauflösung) sind vom
                    // Ownership-Check ausgenommen (CopyTradingRisk.IsAuthorizedSell) – sie gehören keinem
                    // Master und würden sonst IMMER abgelehnt (Copy-Positionen haben SourceTraderId > 0),
                    // sodass Demo-Positionen bei Resolution nie schließen und der 30s-Loop endlos feuert.
                    if (removed && openPos != null && !CopyTradingRisk.IsAuthorizedSell(signal.TraderId, openPos.SourceTraderId))
                    {
                        // Wrong trader! Put the position back and treat as not found.
                        account.OpenPositions.TryAdd(signal.TokenId, openPos);
                        removed = false;
                        openPos = null;
                        _logger.Info($"❌ Trade SELL [{signal.MarketQuestion}] [{shareType}] ignoriert:\n" +
                                     $"   Konto:      {account.Name}\n" +
                                     $"   Begründung: Position gehört einem anderen Trader (Safety Check).");
                        Journal(signal, account, TradeDecision.Rejected, DecisionReason.OwnershipMismatch,
                            "Position gehört einem anderen Trader (Defense-in-depth)");
                    }
                    
                    if (!removed && !string.IsNullOrEmpty(signal.MarketSlug))
                    {
                        // Fallback matching must ALSO respect SourceTraderId!
                        var altPos = account.OpenPositions.Values.FirstOrDefault(p => 
                            p.MarketSlug == signal.MarketSlug && p.Outcome == signal.Outcome && p.SourceTraderId == signal.TraderId);
                        if (altPos != null)
                        {
                            removed = account.OpenPositions.TryRemove(altPos.TokenId, out openPos);
                            if (removed)
                            {
                                _logger.Info($"Fallback: Position für SELL über Slug+Outcome gefunden ({altPos.TokenId}) statt TokenId ({signal.TokenId})");
                                signal.TokenId = altPos.TokenId; // Fix for further processing
                            }
                        }
                    }

                    if (removed && openPos != null)
                    {
                        if (account.IsDemo)
                        {
                            _positionRepo.DeleteDemo(account.AccountId, signal.TokenId);

                            // Phase 4.2: realistischer Demo-Close (Exit-Slippage + Fees), statt zum
                            // Signalpreis ohne Kosten – sonst ist Demo als Master-Validierung wertlos.
                            int demoFeeBps = _state.MarketCache.TryGetValue(signal.TokenId, out var demoMd)
                                ? (demoMd.TakerFeeBps > 0 ? demoMd.TakerFeeBps : FeeModel.FallbackBps(demoMd.Category))
                                : FeeModel.FallbackBps(null);
                            var (exitUsd, realizedPnl, totalFees) = DemoModel.CloseWithFees(
                                openPos.Size, signal.Price, openPos.AmountUsd, demoFeeBps, DemoModel.FallbackHalfSpread);
                            decimal demoExitPrice = DemoModel.ExitFillPrice(signal.Price, DemoModel.FallbackHalfSpread);

                            _state.GlobalPnl += realizedPnl;
                            // Balance netto gutschreiben (exitUsd − Exit-Fee), damit Σ(Balance-Änderungen) = Σ(PnL)
                            // statt um die Fees zu driften (Entry-Fee wurde beim BUY abgezogen).
                            decimal demoExitFee = FeeModel.FeeUsd(exitUsd, demoFeeBps);
                            account.UpdateBalance(account.AvailableBalance + exitUsd - demoExitFee);
                            _accountRepo.Upsert(account);

                            var ct = new ClosedTrade
                            {
                                TradeId = _copyState.GetNextTradeId(),
                                AccountId = account.AccountId,
                                SourceTraderId = signal.TraderId,
                                IsDemo = account.IsDemo,
                                TokenId = signal.TokenId,
                                MarketSlug = signal.MarketSlug,
                                MarketQuestion = signal.MarketQuestion,
                                Outcome = signal.Outcome,
                                Side = signal.Side,
                                EntryPrice = openPos.EntryPrice,
                                ExitPrice = demoExitPrice,
                                Size = openPos.Size,
                                RealizedPnl = realizedPnl,
                                PnlPercent = TradeMath.PnlPercent(realizedPnl, openPos.AmountUsd),
                                TotalFees = totalFees,
                                OpenedAt = openPos.OpenedAt,
                                ClosedAt = DateTime.UtcNow,
                                ExitReason = signal.Reason,
                                SignalId = signal.SignalId
                            };
                            
                            _closedTradeWriter.TryWrite(ct);
                            _logger.Trade($"✅ [DEMO GESCHLOSSEN]\n" +
                                          $"   Konto: {account.Name}\n" +
                                          $"   Markt: {signal.MarketQuestion}\n" +
                                          $"   SELL:  {openPos.Size:F2} Shares [{shareType}] @ ${signal.Price:F3} (Gewinn: ${realizedPnl:F2})");
                            Journal(signal, account, TradeDecision.Executed,
                                signal.TraderId == 0 ? DecisionReason.SystemResolutionClose : DecisionReason.DemoClosed,
                                $"Demo-Close {openPos.Size:F2} Shares, PnL {realizedPnl:F2}",
                                Ctx(new { size = openPos.Size, exitPrice = demoExitPrice, realizedPnl, totalFees }));
                        }
                        else
                        {
                            // Phase 0.1: SELL-Eskalationsleiter statt Market-Dump (Logik zentral in
                            // SellLadderService – gleiche Quelle wie der Profit-Target-Exit im Sync).
                            // openPos wurde oben entfernt; StartLadderAsync stellt es als ExitPending zurück.
                            bool isHf = trader != null && trader.Category == "HF";
                            bool ladderOk = await _sellLadder.StartLadderAsync(
                                account, openPos, signal.Price, signal.TraderId, isHf,
                                settings.MaxPriceDifference, settings.SellFloorPct, isNegRisk, "Master SELL", signal.SignalId);
                            Journal(signal, account,
                                ladderOk ? TradeDecision.Executed : TradeDecision.Failed,
                                ladderOk ? DecisionReason.LadderStarted : DecisionReason.LadderStartFailed,
                                ladderOk ? $"SELL-Leiter gestartet (Referenz {signal.Price:F3})" : "SELL-Leiter-Start fehlgeschlagen/abgelehnt",
                                Ctx(new { referencePrice = signal.Price, size = openPos.Size, isHf }));
                        }
                    }
                    else
                    {
                        _logger.Info($"❌ Trade SELL [{signal.MarketQuestion}] [{shareType}] ignoriert:\n" +
                                     $"   Konto:      {account.Name}\n" +
                                     $"   Begründung: Position nicht im Portfolio gefunden (möglicherweise zuvor gefiltert).");
                        Journal(signal, account, TradeDecision.Rejected, DecisionReason.PositionNotFound,
                            "Position nicht im Portfolio gefunden");
                    }
                }
            }
            finally
            {
                accountSemaphore.Release();
            }
        }

        private bool IsPositionMarketActive(Position pos)
        {
            // O(1) RAM Lookup. Eliminated LiteDB queries for ultra-low latency.
            if (_state.MarketCache.TryGetValue(pos.TokenId, out var md))
            {
                return !md.Closed;
            }

            // Defaults to active until cache hydrates. 
            // Better to assume active and restrict budget than auto-open budget on unknown markets.
            return true;
        }
    }
}
