#nullable enable

using System.Globalization;

namespace MBN_STOCK_WEBVIEW.Core.Playout.Scenes;

public enum LegacyDomesticMarket
{
    Kospi,
    Kosdaq
}

public sealed record InvestorFlowAmounts(
    int Individual,
    int Foreign,
    int Institution);

public sealed record DailyInvestorFlowData(
    DateOnly TradingDate,
    InvestorFlowAmounts Amounts);

public sealed record S5023SceneData(
    LegacyDomesticMarket Market,
    IReadOnlyList<DailyInvestorFlowData?> DailyRows,
    InvestorFlowAmounts MonthlyTotal) : ILegacySceneData;

/// <summary>
/// Legacy index-investor table. Five nullable rows are required so empty objects are
/// deterministically cleared instead of retaining data from an earlier cue.
/// </summary>
public sealed class S5023SceneMutationBuilder : ILegacySceneMutationBuilder<S5023SceneData>
{
    private const int RowCount = 5;

    public string BuilderKey => "s5023";

    public IReadOnlyList<PlayoutMutation> Build(S5023SceneData data)
    {
        ArgumentNullException.ThrowIfNull(data);
        var rows = LegacySceneBuilderGuard.Count(
            data.DailyRows,
            RowCount,
            nameof(data.DailyRows));
        var title = MarketTitle(data.Market) + " \uB9E4\uB9E4\uB3D9\uD5A5";
        var monthlyTotal = LegacySceneBuilderGuard.NotNull(
            data.MonthlyTotal,
            nameof(data.MonthlyTotal));
        var normalizedRows = rows.Select((row, index) => row is null
            ? null
            : new DailyInvestorFlowData(
                row.TradingDate,
                LegacySceneBuilderGuard.NotNull(
                    row.Amounts,
                    $"DailyRows[{index}].Amounts")))
            .ToArray();
        var mutations = new List<PlayoutMutation>(43)
        {
            new PlayoutSetValue("title", title)
        };

        for (var rowIndex = 0; rowIndex < RowCount; rowIndex++)
        {
            var rowNumber = rowIndex + 1;
            var row = normalizedRows[rowIndex];
            mutations.Add(new PlayoutSetValue(
                $"row{rowNumber}_value1",
                row?.TradingDate.ToString("MM/dd", CultureInfo.InvariantCulture) ?? string.Empty));

            AddDailyAmount(mutations, rowNumber, 2, row?.Amounts.Individual);
            AddDailyAmount(mutations, rowNumber, 3, row?.Amounts.Foreign);
            AddDailyAmount(mutations, rowNumber, 4, row?.Amounts.Institution);
        }

        AddMonthlyTotal(mutations, 1, monthlyTotal.Individual);
        AddMonthlyTotal(mutations, 2, monthlyTotal.Foreign);
        AddMonthlyTotal(mutations, 3, monthlyTotal.Institution);
        return mutations;
    }

    private static void AddDailyAmount(
        ICollection<PlayoutMutation> mutations,
        int row,
        int column,
        int? value)
    {
        var objectName = $"row{row}_value{column}";
        var color = value switch
        {
            > 0 => (R: 171, G: 0, B: 0),
            < 0 => (R: 15, G: 99, B: 188),
            _ => (R: 47, G: 47, B: 47)
        };
        mutations.Add(new PlayoutSetFaceColor(objectName, color.R, color.G, color.B));
        mutations.Add(new PlayoutSetValue(
            objectName,
            value?.ToString("#,##0", CultureInfo.InvariantCulture) ?? string.Empty));
    }

    private static void AddMonthlyTotal(
        ICollection<PlayoutMutation> mutations,
        int column,
        int value)
    {
        mutations.Add(new PlayoutSetValue(
            "row6_value" + column,
            value.ToString("#,##0", CultureInfo.InvariantCulture)));
        var color = value switch
        {
            > 0 => (R: 171, G: 0, B: 0),
            < 0 => (R: 15, G: 99, B: 189),
            _ => (R: 95, G: 95, B: 95)
        };
        mutations.Add(new PlayoutSetFaceColor(
            "row6_bg" + column,
            color.R,
            color.G,
            color.B));
    }

    internal static string MarketTitle(LegacyDomesticMarket market) => market switch
    {
        LegacyDomesticMarket.Kospi => "\uCF54\uC2A4\uD53C",
        LegacyDomesticMarket.Kosdaq => "\uCF54\uC2A4\uB2E5",
        _ => throw new LegacySceneDataException("Unknown domestic market.")
    };
}

public enum InvestorParticipant
{
    Individual,
    Foreign,
    Institution
}

public sealed record InvestorTradingAmountData(
    InvestorParticipant Participant,
    double Amount);

public sealed record S5024SceneData(
    LegacyDomesticMarket Market,
    IReadOnlyList<InvestorTradingAmountData> Amounts) : ILegacySceneData;

public sealed class S5024SceneMutationBuilder : ILegacySceneMutationBuilder<S5024SceneData>
{
    private const double CenterMaximumY = 245d;
    private const double CenterMinimumY = -150d;
    private const double TextHeight = 50d;
    private const double Height = CenterMaximumY - CenterMinimumY - (TextHeight * 2d);

    public string BuilderKey => "s5024";

    public IReadOnlyList<PlayoutMutation> Build(S5024SceneData data)
    {
        ArgumentNullException.ThrowIfNull(data);
        var amounts = LegacySceneBuilderGuard.Range(
            data.Amounts,
            0,
            3,
            nameof(data.Amounts));
        if (amounts.Any(item => item is null || !double.IsFinite(item.Amount)))
        {
            throw new LegacySceneDataException("Investor trading amounts must be finite.");
        }

        if (amounts.Select(item => item.Participant).Distinct().Count() != amounts.Count)
        {
            throw new LegacySceneDataException("Investor participants must be unique.");
        }

        // Validate every enum before emitting any mutation.
        foreach (var item in amounts)
        {
            _ = ParticipantIndex(item.Participant);
        }

        var maximum = amounts.Count == 0 ? 0d : Math.Max(0d, amounts.Max(item => item.Amount));
        var minimum = amounts.Count == 0 ? 0d : Math.Min(0d, amounts.Min(item => item.Amount));
        var mutations = new List<PlayoutMutation>(17)
        {
            new PlayoutSetValue(
                "title",
                S5023SceneMutationBuilder.MarketTitle(data.Market) + " \uB9E4\uB9E4\uB3D9\uD5A5")
        };

        for (var participant = 1; participant <= 3; participant++)
        {
            for (var signVariant = 1; signVariant <= 2; signVariant++)
            {
                mutations.Add(new PlayoutSetVisible(
                    $"barG{participant}_{signVariant}",
                    false));
            }
        }

        var centerY = minimum >= 0d
            ? CenterMinimumY
            : maximum <= 0d
                ? CenterMaximumY
                : CenterMinimumY + TextHeight +
                  (Height * Math.Abs(minimum) / (maximum + Math.Abs(minimum)));
        mutations.Add(new PlayoutSetPosition(
            "centerbar",
            0,
            Convert.ToInt32(centerY),
            0,
            PlayoutVectorComponents.Y));

        foreach (var item in amounts)
        {
            var participant = ParticipantIndex(item.Participant);
            var signVariant = item.Amount < 0 ? 2 : 1;
            var ratio = ScaleRatio(item.Amount, maximum, minimum);
            mutations.Add(new PlayoutSetVisible(
                $"barG{participant}_{signVariant}",
                true));
            mutations.Add(new PlayoutSetScale(
                $"bar{participant}_{signVariant}",
                0,
                ratio,
                0,
                PlayoutVectorComponents.Y));
            mutations.Add(new PlayoutSetValue(
                $"value{participant}_{signVariant}",
                item.Amount.ToString("#,##0", CultureInfo.InvariantCulture)));
        }

        return mutations;
    }

    private static int ParticipantIndex(InvestorParticipant participant) => participant switch
    {
        InvestorParticipant.Individual => 1,
        InvestorParticipant.Foreign => 2,
        InvestorParticipant.Institution => 3,
        _ => throw new LegacySceneDataException("Unknown investor participant.")
    };

    private static float ScaleRatio(double value, double maximum, double minimum)
    {
        if (value == 0d)
        {
            // The legacy expression produces NaN for an all-zero input. Clearing the bar is
            // the approved deterministic correction and prevents an invalid SDK call.
            return 0f;
        }

        if (value > 0d)
        {
            var ratio = value / maximum;
            if (minimum < 0d)
            {
                ratio *= maximum / (maximum + Math.Abs(minimum));
            }

            return (float)ratio;
        }

        var negativeRatio = Math.Abs(value) / Math.Abs(minimum);
        if (maximum > 0d)
        {
            negativeRatio *= Math.Abs(minimum) / (maximum + Math.Abs(minimum));
        }

        return (float)negativeRatio;
    }
}

public sealed record ManualNetSellPairData(
    string LeftName,
    string LeftAmount,
    string RightName,
    string RightAmount);

public sealed record S5025SceneData(
    string Title,
    IReadOnlyList<ManualNetSellPairData?> Rows) : ILegacySceneData;

public sealed class S5025SceneMutationBuilder : ILegacySceneMutationBuilder<S5025SceneData>
{
    private const int CompatibilityPairRowCount = 4;
    private const int LegacyPairRowCount = 5;

    public string BuilderKey => "s5025";

    public IReadOnlyList<PlayoutMutation> Build(S5025SceneData data)
    {
        ArgumentNullException.ThrowIfNull(data);
        var title = LegacySceneBuilderGuard.Text(data.Title, nameof(data.Title));
        // The original .dat contract contains five pair rows plus its trailing blank
        // line and therefore writes slots 1..10. Four rows remain accepted only for
        // DTO compatibility with cues created before that source contract was traced.
        var rows = LegacySceneBuilderGuard.Range(
            data.Rows,
            CompatibilityPairRowCount,
            LegacyPairRowCount,
            nameof(data.Rows));
        var normalized = rows.Select((row, index) => Normalize(row, index)).ToArray();
        var mutations = new List<PlayoutMutation>(1 + (normalized.Length * 4))
        {
            new PlayoutSetValue("title", title + " \uC21C\uB9E4\uB3C4 \uC0C1\uC704")
        };

        for (var rowIndex = 0; rowIndex < normalized.Length; rowIndex++)
        {
            var firstSlot = (rowIndex * 2) + 1;
            var row = normalized[rowIndex];
            mutations.Add(new PlayoutSetValue("\uC885\uBAA9" + firstSlot, row.LeftName));
            mutations.Add(new PlayoutSetValue("\uAE08\uC561" + firstSlot, row.LeftAmount));
            mutations.Add(new PlayoutSetValue("\uC885\uBAA9" + (firstSlot + 1), row.RightName));
            mutations.Add(new PlayoutSetValue("\uAE08\uC561" + (firstSlot + 1), row.RightAmount));
        }

        return mutations;
    }

    private static ManualNetSellPairData Normalize(ManualNetSellPairData? row, int index)
    {
        if (row is null)
        {
            return new ManualNetSellPairData(string.Empty, string.Empty, string.Empty, string.Empty);
        }

        return new ManualNetSellPairData(
            LegacySceneBuilderGuard.Text(row.LeftName, $"Rows[{index}].LeftName", true),
            LegacySceneBuilderGuard.Text(row.LeftAmount, $"Rows[{index}].LeftAmount", true),
            LegacySceneBuilderGuard.Text(row.RightName, $"Rows[{index}].RightName", true),
            LegacySceneBuilderGuard.Text(row.RightAmount, $"Rows[{index}].RightAmount", true));
    }
}

public sealed record DealerVolumeData(string DealerName, long Volume);

public sealed record S5037SceneData(
    string StockName,
    long CurrentPrice,
    long ChangePrice,
    double Rate,
    ScenePriceDirection Direction,
    IReadOnlyList<DealerVolumeData> BuyDealers,
    IReadOnlyList<DealerVolumeData> SellDealers) : ILegacySceneData;

public sealed class S5037SceneMutationBuilder : ILegacySceneMutationBuilder<S5037SceneData>
{
    public string BuilderKey => "s5037";

    public IReadOnlyList<PlayoutMutation> Build(S5037SceneData data)
    {
        ArgumentNullException.ThrowIfNull(data);
        var stockName = LegacySceneBuilderGuard.Text(data.StockName, nameof(data.StockName));
        if (!double.IsFinite(data.Rate))
        {
            throw new LegacySceneDataException("Dealer quote rate must be finite.");
        }

        var buys = NormalizeDealers(data.BuyDealers, nameof(data.BuyDealers));
        var sells = NormalizeDealers(data.SellDealers, nameof(data.SellDealers));
        // The authoritative 5037.t2s bakes the unit label into the artwork and
        // does not expose the legacy code's "unit" object.
        var mutations = new List<PlayoutMutation>(44)
        {
            new PlayoutSetValue("title", stockName),
            new PlayoutSetValue("price", FormatInteger(data.CurrentPrice)),
            new PlayoutSetValue("changePrice", FormatInteger(data.ChangePrice)),
            new PlayoutSetValue("rate", data.Rate.ToString("##0.00", CultureInfo.InvariantCulture))
        };
        LegacySimpleQuoteMutations.AddDirection(mutations, data.Direction);

        for (var slot = 1; slot <= 5; slot++)
        {
            mutations.Add(new PlayoutSetValue("buy_title_" + slot, string.Empty));
            mutations.Add(new PlayoutSetValue("buy_vol_" + slot, string.Empty));
            mutations.Add(new PlayoutSetValue("sell_title_" + slot, string.Empty));
            mutations.Add(new PlayoutSetValue("sell_vol_" + slot, string.Empty));
        }

        for (var index = 0; index < buys.Count; index++)
        {
            var slot = index + 1;
            mutations.Add(new PlayoutSetValue("buy_title_" + slot, buys[index].DealerName));
            mutations.Add(new PlayoutSetValue("buy_vol_" + slot, FormatInteger(buys[index].Volume)));
        }

        for (var index = 0; index < sells.Count; index++)
        {
            var slot = index + 1;
            mutations.Add(new PlayoutSetValue("sell_title_" + slot, sells[index].DealerName));
            mutations.Add(new PlayoutSetValue("sell_vol_" + slot, FormatInteger(sells[index].Volume)));
        }

        return mutations;
    }

    private static IReadOnlyList<DealerVolumeData> NormalizeDealers(
        IReadOnlyList<DealerVolumeData>? dealers,
        string name)
    {
        var values = LegacySceneBuilderGuard.Range(dealers, 0, 5, name);
        return values.Select((dealer, index) =>
        {
            var value = LegacySceneBuilderGuard.NotNull(dealer, $"{name}[{index}]");
            return value with
            {
                DealerName = LegacySceneBuilderGuard.Text(
                    value.DealerName,
                    $"{name}[{index}].DealerName")
            };
        }).ToArray();
    }

    private static string FormatInteger(long value) =>
        value.ToString("#,##0", CultureInfo.InvariantCulture);
}

public sealed record OrderBookLevelData(long Price, long Quantity);

public sealed record S8003SceneData(
    string StockName,
    long CurrentPrice,
    long ChangePrice,
    double Rate,
    ScenePriceDirection Direction,
    IReadOnlyList<OrderBookLevelData?> BuyLevels,
    IReadOnlyList<OrderBookLevelData?> SellLevels,
    long TotalBuyQuantity,
    long TotalSellQuantity) : ILegacySceneData;

public sealed class S8003SceneMutationBuilder : ILegacySceneMutationBuilder<S8003SceneData>
{
    private const int LevelCount = 3;
    private static readonly string[] BuyPriceObjects =
        ["price_buy_1", "price_buy_2", "price_buy_3"];
    private static readonly string[] BuyQuantityObjects =
        ["buy_jan_1", "buy_jan_2", "buy_jan_3"];
    private static readonly string[] BuyBarObjects =
        ["buy_bar_1", "buy_bar_2", "buy_bar_3"];
    private static readonly string[] SellPriceObjects =
        ["price_sell_1", "price_sell_2", "price_sell_3"];
    private static readonly string[] SellQuantityObjects =
        ["sell_jan_1", "sell_jan_2", "sell_jan_3"];
    private static readonly string[] SellBarObjects =
        ["sell_bar_1", "sell_bar_2", "sell_bar_3"];

    public string BuilderKey => "s8003";

    public IReadOnlyList<PlayoutMutation> Build(S8003SceneData data)
    {
        ArgumentNullException.ThrowIfNull(data);
        var stockName = LegacySceneBuilderGuard.Text(data.StockName, nameof(data.StockName));
        if (!double.IsFinite(data.Rate))
        {
            throw new LegacySceneDataException("Order-book quote rate must be finite.");
        }

        if (data.TotalBuyQuantity < 0 || data.TotalSellQuantity < 0)
        {
            throw new LegacySceneDataException("Order-book total quantities cannot be negative.");
        }

        var buys = NormalizeLevels(data.BuyLevels, nameof(data.BuyLevels));
        var sells = NormalizeLevels(data.SellLevels, nameof(data.SellLevels));
        // The authoritative 8003.t2s bakes the unit label into the artwork and
        // does not expose the legacy code's "unit" object.
        var mutations = new List<PlayoutMutation>(40)
        {
            new PlayoutSetValue("title", stockName),
            new PlayoutSetValue("price", FormatInteger(data.CurrentPrice)),
            new PlayoutSetValue("changePrice", FormatInteger(data.ChangePrice)),
            new PlayoutSetValue("rate", data.Rate.ToString("##0.00", CultureInfo.InvariantCulture))
        };
        LegacySimpleQuoteMutations.AddDirection(mutations, data.Direction);

        AddPrices(mutations, BuyPriceObjects, buys, data.CurrentPrice);
        AddQuantities(mutations, BuyQuantityObjects, buys);
        AddPrices(mutations, SellPriceObjects, sells, data.CurrentPrice);
        AddQuantities(mutations, SellQuantityObjects, sells);
        mutations.Add(new PlayoutSetValue("buy_jan_total", FormatInteger(data.TotalBuyQuantity)));
        mutations.Add(new PlayoutSetValue("sell_jan_total", FormatInteger(data.TotalSellQuantity)));

        var maximumQuantity = buys.Concat(sells)
            .Where(level => level is not null)
            .Select(level => level!.Quantity)
            .DefaultIfEmpty(0)
            .Max();
        AddBarScales(mutations, BuyBarObjects, buys, maximumQuantity);
        AddBarScales(mutations, SellBarObjects, sells, maximumQuantity);
        return mutations;
    }

    private static IReadOnlyList<OrderBookLevelData?> NormalizeLevels(
        IReadOnlyList<OrderBookLevelData?>? levels,
        string name)
    {
        var values = LegacySceneBuilderGuard.Count(levels, LevelCount, name);
        if (values.Any(level => level?.Quantity < 0))
        {
            throw new LegacySceneDataException("Order-book quantities cannot be negative.");
        }

        return values;
    }

    private static void AddPrices(
        ICollection<PlayoutMutation> mutations,
        IReadOnlyList<string> objects,
        IReadOnlyList<OrderBookLevelData?> levels,
        long currentPrice)
    {
        for (var index = 0; index < LevelCount; index++)
        {
            var level = levels[index];
            mutations.Add(new PlayoutSetValue(
                objects[index],
                level is null ? string.Empty : FormatInteger(level.Price)));
            var color = level is null || level.Price == currentPrice
                ? (R: 50, G: 51, B: 50)
                : currentPrice < level.Price
                    ? (R: 172, G: 0, B: 0)
                    : (R: 19, G: 73, B: 170);
            mutations.Add(new PlayoutSetFaceColor(
                objects[index],
                color.R,
                color.G,
                color.B));
        }
    }

    private static void AddQuantities(
        ICollection<PlayoutMutation> mutations,
        IReadOnlyList<string> objects,
        IReadOnlyList<OrderBookLevelData?> levels)
    {
        for (var index = 0; index < LevelCount; index++)
        {
            mutations.Add(new PlayoutSetValue(
                objects[index],
                levels[index] is null ? string.Empty : FormatInteger(levels[index]!.Quantity)));
        }
    }

    private static void AddBarScales(
        ICollection<PlayoutMutation> mutations,
        IReadOnlyList<string> objects,
        IReadOnlyList<OrderBookLevelData?> levels,
        long maximumQuantity)
    {
        for (var index = 0; index < LevelCount; index++)
        {
            var scale = maximumQuantity == 0 || levels[index] is null
                ? 0f
                : (float)(levels[index]!.Quantity / (double)maximumQuantity);
            mutations.Add(new PlayoutSetScale(
                objects[index],
                scale,
                0,
                0,
                PlayoutVectorComponents.X));
        }
    }

    private static string FormatInteger(long value) =>
        value.ToString("#,##0", CultureInfo.InvariantCulture);
}

internal static class LegacySimpleQuoteMutations
{
    private static readonly string[] DirectionObjects =
        ["upup", "up", "flat", "down", "downdown"];

    public static void AddDirection(
        ICollection<PlayoutMutation> mutations,
        ScenePriceDirection direction)
    {
        foreach (var objectName in DirectionObjects)
        {
            mutations.Add(new PlayoutSetVisible(objectName, false));
        }

        string? active = direction switch
        {
            ScenePriceDirection.LimitUp => "upup",
            ScenePriceDirection.Up => "up",
            ScenePriceDirection.Down => "down",
            ScenePriceDirection.LimitDown => "downdown",
            ScenePriceDirection.Flat or ScenePriceDirection.Unknown => null,
            _ => throw new LegacySceneDataException("Unknown price direction.")
        };
        if (active is not null)
        {
            mutations.Add(new PlayoutSetVisible(active, true));
        }

        var variant = direction switch
        {
            ScenePriceDirection.LimitUp or ScenePriceDirection.Up => 1,
            ScenePriceDirection.Down or ScenePriceDirection.LimitDown => 3,
            ScenePriceDirection.Flat or ScenePriceDirection.Unknown => 2,
            _ => throw new LegacySceneDataException("Unknown price direction.")
        };
        for (var index = 1; index <= 3; index++)
        {
            mutations.Add(new PlayoutSetVisible("bg" + index, index == variant));
        }
    }
}
