using KArtSell.BuildingBlocks.Time;
using Microsoft.Extensions.Logging;

namespace KArtSell.Modules.ModelOperations.ShadowRun;

/// <summary>
/// Replays model over historical data window to generate signals, orders, and fills.
/// Implements idempotent replay: same input = same output (deterministic price/fills).
/// </summary>
public sealed class ReplayEngine(
    ILogger<ReplayEngine> logger)
{
    public record Signal(
        Guid SignalId,
        DateOnly Date,
        string Ticker,
        SignalAction Action,
        decimal Confidence,
        string Rationale);

    public record Order(
        Guid OrderId,
        DateOnly PlacedDate,
        DateOnly? FilledDate,
        string Ticker,
        SignalAction Action,
        long Quantity,
        decimal InitialPrice,
        decimal? FilledPrice);

    public record Portfolio(
        DateOnly AsOfDate,
        Dictionary<string, long> Positions,  // ticker -> shares
        decimal CashBalance,
        decimal TotalValue);

    public enum SignalAction
    {
        Buy = 0,
        Sell = 1,
        Hold = 2,
        Exit = 3
    }

    /// <summary>
    /// Replay model across historical window.
    /// Returns daily portfolio snapshots and order fills.
    /// </summary>
    public async Task<ReplayResult> ReplayAsync(
        Guid modelId,
        IReadOnlyList<DataBackfiller.OhlcvBar> ohlcvBars,
        IReadOnlyList<DataBackfiller.FeeScheduleEntry> feeSchedule,
        decimal initialCashBalance,
        IReadOnlyList<DateOnly> tradingSessions,
        CancellationToken cancellationToken)
    {
        logger.LogInformation(
            "Replaying model {ModelId} across {TradingDays} sessions, initial cash: {CashBalance:C}",
            modelId, tradingSessions.Count, initialCashBalance);

        var portfolioHistory = new List<Portfolio>();
        var signals = new List<Signal>();
        var orders = new List<Order>();
        var dailyReturns = new List<(DateOnly Date, decimal Return)>();

        var currentPortfolio = new Portfolio(
            tradingSessions[0],
            new Dictionary<string, long>(),
            initialCashBalance,
            initialCashBalance);

        decimal previousPortfolioValue = initialCashBalance;

        foreach (var session in tradingSessions)
        {
            cancellationToken.ThrowIfCancellationRequested();

            // Simulate signals at market open (simplified: use model.predict logic)
            var daySignals = await GenerateSignalsAsync(modelId, session, ohlcvBars, cancellationToken);
            signals.AddRange(daySignals);

            // Convert signals to orders with dynamic position sizing
            var dayOrders = daySignals
                .Select(s =>
                {
                    var closePrice = GetClosePrice(session, s.Ticker, ohlcvBars);
                    if (closePrice <= 0) return null;

                    // Position size: 2% of portfolio per signal (Kelly Criterion simplified)
                    // Higher confidence → larger position (0.5x to 1.5x multiplier)
                    var riskPercentage = 0.02m * s.Confidence * 2m;  // Ranges 0.01-0.03
                    var targetCash = currentPortfolio.TotalValue * riskPercentage;
                    var quantity = Math.Max(1L, (long)(targetCash / closePrice));

                    return new Order(
                        OrderId: Guid.NewGuid(),
                        PlacedDate: session,
                        FilledDate: session,
                        Ticker: s.Ticker,
                        Action: s.Action,
                        Quantity: quantity,
                        InitialPrice: closePrice,
                        FilledPrice: closePrice);
                })
                .Where(o => o != null)
                .Cast<Order>()
                .ToList();

            orders.AddRange(dayOrders);

            // Get fee schedule for this date
            var todayFee = feeSchedule.FirstOrDefault(f => f.EffectiveDate <= session);
            var feePercent = todayFee?.TransactionFeePercent ?? 0.001m;  // 0.1% default

            // Update portfolio
            foreach (var order in dayOrders)
            {
                if (order.FilledPrice.HasValue)
                {
                    var cost = order.Quantity * order.FilledPrice.Value;
                    var fees = cost * feePercent;
                    var totalCost = cost + fees;

                    switch (order.Action)
                    {
                        case SignalAction.Buy:
                            currentPortfolio.Positions.TryGetValue(order.Ticker, out var existing);
                            currentPortfolio.Positions[order.Ticker] = existing + order.Quantity;
                            currentPortfolio = currentPortfolio with
                            {
                                CashBalance = currentPortfolio.CashBalance - totalCost
                            };
                            break;
                        case SignalAction.Sell:
                        case SignalAction.Exit:
                            currentPortfolio.Positions.TryGetValue(order.Ticker, out var current);
                            currentPortfolio.Positions[order.Ticker] = Math.Max(0, current - order.Quantity);
                            currentPortfolio = currentPortfolio with
                            {
                                CashBalance = currentPortfolio.CashBalance + cost - fees  // Sell proceeds minus fees
                            };
                            break;
                    }
                }
            }

            // Calculate portfolio value
            var holdingValue = currentPortfolio.Positions
                .Sum(pos => pos.Value * GetClosePrice(session, pos.Key, ohlcvBars));
            var totalValue = currentPortfolio.CashBalance + holdingValue;

            currentPortfolio = currentPortfolio with
            {
                AsOfDate = session,
                TotalValue = totalValue
            };

            portfolioHistory.Add(currentPortfolio);

            // Daily return
            var dailyReturn = (totalValue - previousPortfolioValue) / previousPortfolioValue;
            dailyReturns.Add((session, dailyReturn));
            previousPortfolioValue = totalValue;
        }

        logger.LogInformation(
            "Replay complete: {PortfolioDays} snapshots, {SignalCount} signals, {OrderCount} orders",
            portfolioHistory.Count, signals.Count, orders.Count);

        return new ReplayResult(
            ModelId: modelId,
            PortfolioHistory: portfolioHistory.AsReadOnly(),
            Signals: signals.AsReadOnly(),
            Orders: orders.AsReadOnly(),
            DailyReturns: dailyReturns.AsReadOnly());
    }

    private async Task<List<Signal>> GenerateSignalsAsync(
        Guid modelId,
        DateOnly date,
        IReadOnlyList<DataBackfiller.OhlcvBar> bars,
        CancellationToken cancellationToken)
    {
        await Task.Delay(5, cancellationToken);

        var signals = new List<Signal>();

        // EMA12/EMA26 Crossover Strategy
        var barsByDate = bars.OrderBy(b => b.Date).ToList();
        var currentIdx = barsByDate.FindIndex(b => b.Date == date);
        if (currentIdx < 26)
            return signals; // Not enough data

        var prices = barsByDate.Take(currentIdx + 1).GroupBy(b => b.Ticker)
            .ToDictionary(g => g.Key, g => g.Select(b => b.Close).ToList());

        foreach (var (ticker, closes) in prices)
        {
            var ema12 = CalculateEMA(closes, 12);
            var ema26 = CalculateEMA(closes, 26);

            if (ema12 > ema26 * 1.001m) // 0.1% threshold to avoid noise
                signals.Add(new Signal(Guid.NewGuid(), date, ticker, SignalAction.Buy, 0.75m, "EMA12 > EMA26"));
            else if (ema12 < ema26 * 0.999m)
                signals.Add(new Signal(Guid.NewGuid(), date, ticker, SignalAction.Sell, 0.75m, "EMA12 < EMA26"));
        }

        return signals;
    }

    private static decimal CalculateEMA(List<decimal> prices, int period)
    {
        if (prices.Count < period) return prices.Last();

        var multiplier = 2m / (period + 1);
        var ema = prices.Take(period).Average();

        foreach (var price in prices.Skip(period))
            ema = (price * multiplier) + (ema * (1 - multiplier));

        return ema;
    }

    private static decimal GetClosePrice(
        DateOnly date,
        string ticker,
        IReadOnlyList<DataBackfiller.OhlcvBar> bars)
    {
        var bar = bars.FirstOrDefault(b => b.Date == date && b.Ticker == ticker);
        return bar?.Close ?? 0m;
    }
}

public sealed record ReplayResult(
    Guid ModelId,
    IReadOnlyList<ReplayEngine.Portfolio> PortfolioHistory,
    IReadOnlyList<ReplayEngine.Signal> Signals,
    IReadOnlyList<ReplayEngine.Order> Orders,
    IReadOnlyList<(DateOnly Date, decimal Return)> DailyReturns);
