#nullable enable

using System.Globalization;

namespace MBN_STOCK_WEBVIEW.Core.Playout.Scenes;

public enum SectorIndexMarket
{
    Dow,
    Nasdaq,
    StandardAndPoor,
    Kospi,
    Kosdaq
}

public enum SectorBarVariant
{
    Primary,
    Secondary
}

public sealed record SectorIndexBarData(
    string Category,
    double Value,
    SectorBarVariant Variant);

public sealed record S5078SceneData(
    SectorIndexMarket Market,
    IReadOnlyList<SectorIndexBarData> Rows) : ILegacySceneData;

public sealed class S5078SceneMutationBuilder : ILegacySceneMutationBuilder<S5078SceneData>
{
    public string BuilderKey => "s5078";

    public IReadOnlyList<PlayoutMutation> Build(S5078SceneData data)
    {
        ArgumentNullException.ThrowIfNull(data);
        var rows = LegacySceneBuilderGuard.Count(data.Rows, 10, nameof(data.Rows));
        var (title, primaryScaleFactor) = data.Market switch
        {
            SectorIndexMarket.Dow => ("다우 섹터지수", -1d),
            SectorIndexMarket.Nasdaq => ("나스닥 섹터지수", -1d),
            SectorIndexMarket.StandardAndPoor => ("S&P 섹터지수", -1d),
            SectorIndexMarket.Kospi => ("코스피 섹터지수", 1d),
            SectorIndexMarket.Kosdaq => ("코스닥 섹터지수", 1d),
            _ => throw new LegacySceneDataException("Unknown sector index market.")
        };

        var validatedRows = new SectorIndexBarData[rows.Count];
        var maximumMagnitude = 0d;
        for (var index = 0; index < rows.Count; index++)
        {
            var row = LegacySceneBuilderGuard.NotNull(rows[index], $"Rows[{index}]");
            LegacySceneBuilderGuard.Text(row.Category, $"Rows[{index}].Category");
            if (!double.IsFinite(row.Value))
            {
                throw new LegacySceneDataException("Sector index values must be finite.");
            }

            _ = VariantSuffix(row.Variant);
            validatedRows[index] = row;
            maximumMagnitude = Math.Max(maximumMagnitude, Math.Abs(row.Value));
        }

        var mutations = new List<PlayoutMutation>(91)
        {
            new PlayoutSetValue("title", title)
        };

        for (var index = 1; index <= 10; index++)
        {
            mutations.Add(new PlayoutSetVisible($"bar{index}_1", false));
            mutations.Add(new PlayoutSetVisible($"value{index}_1", false));
            mutations.Add(new PlayoutSetVisible($"bar{index}_2", false));
            mutations.Add(new PlayoutSetVisible($"value{index}_2", false));
        }

        for (var index = 1; index <= validatedRows.Length; index++)
        {
            var row = validatedRows[index - 1];
            var suffix = VariantSuffix(row.Variant);
            var scaleFactor = row.Variant == SectorBarVariant.Primary
                ? primaryScaleFactor
                : 1d;
            var scale = maximumMagnitude == 0d
                ? 0f
                : LegacyChartSafety.FiniteFloat(
                    row.Value / maximumMagnitude * scaleFactor,
                    "Sector index scale");

            mutations.Add(new PlayoutSetValue(
                "category" + index,
                row.Category));
            mutations.Add(new PlayoutSetVisible($"bar{index}_{suffix}", true));
            mutations.Add(new PlayoutSetVisible($"value{index}_{suffix}", true));
            mutations.Add(new PlayoutSetScale(
                $"bar{index}_{suffix}",
                scale,
                0,
                0,
                PlayoutVectorComponents.X));
            mutations.Add(new PlayoutSetValue(
                $"value{index}_{suffix}",
                row.Value.ToString("##0.00", CultureInfo.InvariantCulture)));
        }

        return mutations;
    }

    private static int VariantSuffix(SectorBarVariant variant) => variant switch
    {
        SectorBarVariant.Primary => 1,
        SectorBarVariant.Secondary => 2,
        _ => throw new LegacySceneDataException("Unknown sector bar variant.")
    };
}

public enum GrowthMetricPath
{
    First,
    Second,
    Third,
    Fourth
}

/// <summary>
/// Null values represent the legacy empty token and are normalized to zero. The first
/// value controls visibility exactly as the original rs[0] empty check did.
/// </summary>
public sealed record GrowthMetricSeriesData(
    GrowthMetricPath Path,
    IReadOnlyList<double?> Values);

public sealed record S5079SceneData(
    string StockName,
    IReadOnlyList<GrowthMetricSeriesData> Series,
    IReadOnlyList<string> Dates) : ILegacySceneData;

public sealed class S5079SceneMutationBuilder : ILegacySceneMutationBuilder<S5079SceneData>
{
    private static readonly float[] XPositions = [0, 368, 740, 1110];

    public string BuilderKey => "s5079";

    public IReadOnlyList<PlayoutMutation> Build(S5079SceneData data)
    {
        ArgumentNullException.ThrowIfNull(data);
        var stockName = LegacySceneBuilderGuard.Text(data.StockName, nameof(data.StockName));
        var sourceSeries = LegacySceneBuilderGuard.Count(data.Series, 4, nameof(data.Series));
        var dates = LegacySceneBuilderGuard.Count(data.Dates, 4, nameof(data.Dates));
        var normalized = new Dictionary<GrowthMetricPath, NormalizedGrowthSeries>();

        for (var seriesIndex = 0; seriesIndex < sourceSeries.Count; seriesIndex++)
        {
            var series = LegacySceneBuilderGuard.NotNull(
                sourceSeries[seriesIndex],
                $"Series[{seriesIndex}]");
            _ = PathNumber(series.Path);
            var values = LegacySceneBuilderGuard.Count(
                series.Values,
                4,
                $"Series[{seriesIndex}].Values");
            var normalizedValues = new double[4];
            for (var valueIndex = 0; valueIndex < values.Count; valueIndex++)
            {
                var value = values[valueIndex] ?? 0d;
                if (!double.IsFinite(value))
                {
                    throw new LegacySceneDataException("Growth metric values must be finite.");
                }

                normalizedValues[valueIndex] = value;
            }

            if (!normalized.TryAdd(
                    series.Path,
                    new NormalizedGrowthSeries(values[0].HasValue, normalizedValues)))
            {
                throw new LegacySceneDataException("Growth metric paths must be unique.");
            }
        }

        foreach (var path in Enum.GetValues<GrowthMetricPath>())
        {
            if (!normalized.ContainsKey(path))
            {
                throw new LegacySceneDataException("All four growth metric paths are required.");
            }
        }

        var validatedDates = new string[dates.Count];
        for (var index = 0; index < dates.Count; index++)
        {
            validatedDates[index] = LegacySceneBuilderGuard.Text(
                dates[index],
                $"Dates[{index}]",
                allowEmpty: true);
        }

        var allValues = normalized.Values.SelectMany(item => item.Values).ToArray();
        var maximum = allValues.Max();
        var minimum = allValues.Min();
        var range = maximum - minimum;
        if (!double.IsFinite(range))
        {
            throw new LegacySceneDataException("Growth metric range must be finite.");
        }

        var gap = range / 5d;
        var mutations = new List<PlayoutMutation>(36)
        {
            new PlayoutSetValue("title", stockName + " 성장성 지표")
        };
        for (var index = 1; index <= 5; index++)
        {
            mutations.Add(new PlayoutSetValue(
                "value" + index,
                Math.Ceiling(minimum + (gap * index))
                    .ToString(CultureInfo.InvariantCulture)));
        }

        foreach (var path in Enum.GetValues<GrowthMetricPath>())
        {
            var pathNumber = PathNumber(path);
            var series = normalized[path];
            var points = new PlayoutPoint[4];
            for (var index = 0; index < points.Length; index++)
            {
                var y = range == 0d
                    ? 0f
                    : LegacyChartSafety.FiniteFloat(
                        (((360d * (series.Values[index] - minimum) / range) - 180d) * 0.8d),
                        "Growth metric Y position");
                points[index] = new PlayoutPoint(XPositions[index], y, 0);
            }

            AppendLegacyGrowthPath(
                mutations,
                pathNumber,
                series.IsVisible,
                points);
        }

        for (var index = 1; index <= validatedDates.Length; index++)
        {
            mutations.Add(new PlayoutSetValue("date" + index, validatedDates[index - 1]));
        }

        return mutations;
    }

    private static void AppendLegacyGrowthPath(
        ICollection<PlayoutMutation> mutations,
        int pathNumber,
        bool isVisible,
        IReadOnlyList<PlayoutPoint> points)
    {
        var objectName = "path" + pathNumber;
        if (pathNumber != 3)
        {
            mutations.Add(new PlayoutSetVisible(objectName, false));
            if (isVisible)
            {
                // The original sets visibility once per point inside its AddPathPoint loop.
                for (var index = 0; index < points.Count; index++)
                {
                    mutations.Add(new PlayoutSetVisible(objectName, true));
                }
            }
        }

        // PlayoutSetPathPoints expands to Begin/Clear/Add*/End in the adapter.
        mutations.Add(new PlayoutSetPathPoints(objectName, points));
        if (pathNumber == 3)
        {
            mutations.Add(new PlayoutSetVisible(objectName, isVisible));
        }
    }

    private static int PathNumber(GrowthMetricPath path) => path switch
    {
        GrowthMetricPath.First => 1,
        GrowthMetricPath.Second => 2,
        GrowthMetricPath.Third => 3,
        GrowthMetricPath.Fourth => 4,
        _ => throw new LegacySceneDataException("Unknown growth metric path.")
    };

    private sealed record NormalizedGrowthSeries(bool IsVisible, double[] Values);
}

public sealed record QuarterlySalesData(string Quarter, int Value);

public sealed record S5080SceneData(
    string StockName,
    IReadOnlyList<QuarterlySalesData> Quarters) : ILegacySceneData;

public sealed class S5080SceneMutationBuilder : ILegacySceneMutationBuilder<S5080SceneData>
{
    public string BuilderKey => "s5080";

    public IReadOnlyList<PlayoutMutation> Build(S5080SceneData data)
    {
        ArgumentNullException.ThrowIfNull(data);
        var stockName = LegacySceneBuilderGuard.Text(data.StockName, nameof(data.StockName));
        var quarters = LegacySceneBuilderGuard.Count(data.Quarters, 6, nameof(data.Quarters));
        var validated = new QuarterlySalesData[quarters.Count];
        var mutations = new List<PlayoutMutation>(38)
        {
            new PlayoutSetValue("title", stockName + " 매출액")
        };

        for (var index = 0; index < quarters.Count; index++)
        {
            var quarter = LegacySceneBuilderGuard.NotNull(
                quarters[index],
                $"Quarters[{index}]");
            validated[index] = quarter;
            mutations.Add(new PlayoutSetValue(
                "quarter" + (index + 1),
                LegacySceneBuilderGuard.Text(
                    quarter.Quarter,
                    $"Quarters[{index}].Quarter",
                    allowEmpty: true)));
        }

        for (var index = 1; index <= validated.Length; index++)
        {
            var value = validated[index - 1].Value;
            var variant = value < 0 ? 2 : 1;
            mutations.Add(new PlayoutSetVisible($"barG{index}_1", false));
            mutations.Add(new PlayoutSetVisible($"barG{index}_2", false));
            mutations.Add(new PlayoutSetVisible($"barG{index}_{variant}", true));
            mutations.Add(new PlayoutSetValue(
                $"value{index}_{variant}",
                value.ToString("#,##0", CultureInfo.InvariantCulture)));
        }

        var maximum = validated.Max(item => item.Value);
        var minimum = validated.Min(item => item.Value);
        var allPositive = maximum > 0 && minimum > 0;
        var allNegative = maximum < 0 && minimum < 0;
        var mixedDenominator = (double)maximum + Math.Abs((double)minimum);
        var center = allPositive
            ? -240f
            : allNegative
                ? 115f
                : mixedDenominator == 0d
                    ? -250f
                    : LegacyChartSafety.FiniteFloat(
                        (355d * Math.Abs((double)minimum) / mixedDenominator) - 250d,
                        "Quarterly sales center");
        mutations.Add(new PlayoutSetPositionKey(
            "centerbar",
            0,
            0,
            center,
            0,
            PlayoutVectorComponents.Y));

        var denominator = allPositive
            ? (double)maximum
            : allNegative
                ? (double)minimum
                : (double)maximum - minimum;
        for (var index = 1; index <= validated.Length; index++)
        {
            var value = validated[index - 1].Value;
            var scale = denominator == 0d
                ? 0f
                : LegacyChartSafety.FiniteFloat(
                    1.8d * Math.Abs(value / denominator),
                    "Quarterly sales scale");
            var variant = value < 0 ? 2 : 1;
            mutations.Add(new PlayoutSetScale(
                $"bar{index}_{variant}",
                0,
                scale,
                0,
                PlayoutVectorComponents.Y));
        }

        return mutations;
    }
}

public enum InvestorFlowParticipant
{
    Individual,
    Institution,
    Foreign
}

public enum InvestorFlowMarket
{
    Kospi,
    Kosdaq,
    Kospi200
}

public sealed record InvestorFlowSummaryData(
    InvestorFlowMarket Market,
    int Value);

public sealed record InvestorFlowTrendPoint(
    InvestorFlowMarket Market,
    int Value,
    TimeOnly DataTime);

public sealed record S5083SceneData(
    InvestorFlowParticipant Participant,
    IReadOnlyList<InvestorFlowSummaryData> Summary,
    IReadOnlyList<InvestorFlowTrendPoint> TrendPoints) : ILegacySceneData;

public sealed class S5083SceneMutationBuilder : ILegacySceneMutationBuilder<S5083SceneData>
{
    public string BuilderKey => "s5083";

    public IReadOnlyList<PlayoutMutation> Build(S5083SceneData data)
    {
        ArgumentNullException.ThrowIfNull(data);
        var title = data.Participant switch
        {
            InvestorFlowParticipant.Individual => "개인 매매동향",
            InvestorFlowParticipant.Institution => "기관 매매동향",
            InvestorFlowParticipant.Foreign => "외국인 매매동향",
            _ => throw new LegacySceneDataException("Unknown investor flow participant.")
        };
        var summary = LegacySceneBuilderGuard.Count(data.Summary, 3, nameof(data.Summary));
        var trendPoints = LegacySceneBuilderGuard.Range(
            data.TrendPoints,
            3,
            10_000,
            nameof(data.TrendPoints));

        var seenSummaryMarkets = new HashSet<InvestorFlowMarket>();
        foreach (var row in summary)
        {
            var validated = LegacySceneBuilderGuard.NotNull(row, nameof(data.Summary));
            _ = MarketSlot(validated.Market);
            if (!seenSummaryMarkets.Add(validated.Market))
            {
                throw new LegacySceneDataException("Investor flow summary markets must be unique.");
            }
        }

        var grouped = new Dictionary<InvestorFlowMarket, List<int>>
        {
            [InvestorFlowMarket.Kospi] = [],
            [InvestorFlowMarket.Kosdaq] = [],
            [InvestorFlowMarket.Kospi200] = []
        };
        var maximum = 0d;
        var minimum = 0d;
        foreach (var point in trendPoints)
        {
            var validated = LegacySceneBuilderGuard.NotNull(point, nameof(data.TrendPoints));
            _ = MarketSlot(validated.Market);
            grouped[validated.Market].Add(validated.Value);
            maximum = Math.Max(maximum, validated.Value);
            minimum = Math.Min(minimum, validated.Value);
        }

        if (grouped.Values.Any(values => values.Count == 0))
        {
            throw new LegacySceneDataException("Every investor flow market requires trend data.");
        }

        var scale = LegacyIntradayChartScale.Create(maximum, minimum);
        var lineWidth = LegacyIntradayChartScale.LineWidth(trendPoints[^1].DataTime);
        var mutations = new List<PlayoutMutation>(8)
        {
            new PlayoutSetValue("title", title)
        };
        foreach (var row in summary)
        {
            mutations.Add(new PlayoutSetValue(
                "price" + MarketSlot(row.Market),
                row.Value.ToString("#,##0", CultureInfo.InvariantCulture)));
        }

        mutations.Add(new PlayoutSetPosition(
            "baseline",
            0,
            scale.BaselinePosition,
            0,
            PlayoutVectorComponents.Y));

        var kospi = grouped[InvestorFlowMarket.Kospi];
        mutations.Add(new PlayoutSetPathPoints(
            "path1",
            BuildS5083Path(kospi, lineWidth, scale, S5083XMode.FirstPath, kospi.Count)));
        mutations.Add(new PlayoutSetPathPoints(
            "path2",
            BuildS5083Path(
                grouped[InvestorFlowMarket.Kosdaq],
                lineWidth,
                scale,
                S5083XMode.FollowingPath,
                kospi.Count)));
        mutations.Add(new PlayoutSetPathPoints(
            "path3",
            BuildS5083Path(
                grouped[InvestorFlowMarket.Kospi200],
                lineWidth,
                scale,
                S5083XMode.FollowingPath,
                grouped[InvestorFlowMarket.Kospi200].Count)));
        return mutations;
    }

    private static IReadOnlyList<PlayoutPoint> BuildS5083Path(
        IReadOnlyList<int> values,
        double lineWidth,
        LegacyIntradayChartScale scale,
        S5083XMode xMode,
        int denominatorCount)
    {
        var points = new List<PlayoutPoint>(values.Count + 1)
        {
            new(0, scale.Baseline, 0)
        };
        for (var index = 0; index < values.Count; index++)
        {
            double x;
            if (xMode == S5083XMode.FirstPath)
            {
                x = values.Count == 1
                    ? 0d
                    : lineWidth / (values.Count - 1d) * index;
            }
            else
            {
                x = lineWidth / denominatorCount * (index + 1d);
            }

            points.Add(new PlayoutPoint(
                LegacyChartSafety.FiniteFloat(x, "Investor flow X position"),
                scale.Y(values[index]),
                0));
        }

        return points;
    }

    private static int MarketSlot(InvestorFlowMarket market) => market switch
    {
        InvestorFlowMarket.Kospi => 1,
        InvestorFlowMarket.Kosdaq => 2,
        InvestorFlowMarket.Kospi200 => 3,
        _ => throw new LegacySceneDataException("Unknown investor flow market.")
    };

    private enum S5083XMode
    {
        FirstPath,
        FollowingPath
    }
}

public enum InvestorFlowIndex
{
    Kospi,
    Kosdaq
}

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

public sealed record InvestorFlowIndexTrendPoint(
    int Individual,
    int Foreign,
    int Institution,
    TimeOnly DataTime);

public sealed record S5084SceneData(
    InvestorFlowIndex Index,
    InvestorFlowTotalsData Totals,
    IReadOnlyList<InvestorFlowIndexTrendPoint> TrendPoints) : ILegacySceneData;

public sealed class S5084SceneMutationBuilder : ILegacySceneMutationBuilder<S5084SceneData>
{
    public string BuilderKey => "s5084";

    public IReadOnlyList<PlayoutMutation> Build(S5084SceneData data)
    {
        ArgumentNullException.ThrowIfNull(data);
        var title = data.Index switch
        {
            InvestorFlowIndex.Kospi => "코스피 매매동향",
            InvestorFlowIndex.Kosdaq => "코스닥 매매동향",
            _ => throw new LegacySceneDataException("Unknown investor flow index.")
        };
        var totals = LegacySceneBuilderGuard.NotNull(data.Totals, nameof(data.Totals));
        var trendPoints = LegacySceneBuilderGuard.Range(
            data.TrendPoints,
            1,
            10_000,
            nameof(data.TrendPoints));

        var maximum = 0d;
        var minimum = 0d;
        foreach (var point in trendPoints)
        {
            var validated = LegacySceneBuilderGuard.NotNull(point, nameof(data.TrendPoints));
            maximum = Math.Max(
                maximum,
                Math.Max(validated.Individual, Math.Max(validated.Foreign, validated.Institution)));
            minimum = Math.Min(
                minimum,
                Math.Min(validated.Individual, Math.Min(validated.Foreign, validated.Institution)));
        }

        var scale = LegacyIntradayChartScale.Create(maximum, minimum);
        var lineWidth = LegacyIntradayChartScale.LineWidth(trendPoints[^1].DataTime);
        var mutations = new List<PlayoutMutation>(8)
        {
            new PlayoutSetValue("title", title),
            new PlayoutSetValue(
                "price1",
                totals.Individual.ToString("#,##0", CultureInfo.InvariantCulture)),
            new PlayoutSetValue(
                "price2",
                totals.Foreign.ToString("#,##0", CultureInfo.InvariantCulture)),
            new PlayoutSetValue(
                "price3",
                totals.Institution.ToString("#,##0", CultureInfo.InvariantCulture)),
            new PlayoutSetPosition(
                "baseline",
                0,
                scale.BaselinePosition,
                0,
                PlayoutVectorComponents.Y),
            new PlayoutSetPathPoints(
                "path1",
                BuildS5084Path(trendPoints.Select(item => item.Individual).ToArray(), lineWidth, scale)),
            new PlayoutSetPathPoints(
                "path2",
                BuildS5084Path(trendPoints.Select(item => item.Foreign).ToArray(), lineWidth, scale)),
            new PlayoutSetPathPoints(
                "path3",
                BuildS5084Path(trendPoints.Select(item => item.Institution).ToArray(), lineWidth, scale))
        };
        return mutations;
    }

    private static IReadOnlyList<PlayoutPoint> BuildS5084Path(
        IReadOnlyList<int> values,
        double lineWidth,
        LegacyIntradayChartScale scale)
    {
        var points = new List<PlayoutPoint>(values.Count + 1)
        {
            new(0, scale.Baseline, 0)
        };
        for (var index = 0; index < values.Count; index++)
        {
            points.Add(new PlayoutPoint(
                LegacyChartSafety.FiniteFloat(
                    lineWidth / values.Count * (index + 1d),
                    "Index flow X position"),
                scale.Y(values[index]),
                0));
        }

        return points;
    }
}

internal sealed record LegacyIntradayChartScale(
    double Minimum,
    double Range,
    float Baseline,
    float BaselinePosition)
{
    private const double GraphMinimum = -138d;
    private const double GraphHeight = 266d;
    private const double GraphOffset = 48d;
    private const double DegenerateMidpoint = -5d;
    private const double FullLineWidth = 1212.26d;

    public static LegacyIntradayChartScale Create(double maximum, double minimum)
    {
        var range = maximum - minimum;
        if (!double.IsFinite(maximum) || !double.IsFinite(minimum) || !double.IsFinite(range))
        {
            throw new LegacySceneDataException("Intraday chart range must be finite.");
        }

        var baseline = range == 0d
            ? DegenerateMidpoint
            : GraphMinimum + ((0d - minimum) / range * GraphHeight);
        var baselinePosition = Convert.ToInt32(GraphOffset + baseline);
        return new LegacyIntradayChartScale(
            minimum,
            range,
            LegacyChartSafety.FiniteFloat(baseline, "Intraday baseline"),
            baselinePosition);
    }

    public static double LineWidth(TimeOnly time)
    {
        var minutes = (time.Hour * 60) + time.Minute;
        if (minutes >= 930)
        {
            return FullLineWidth;
        }

        // The legacy formula becomes negative before 09:00. Clamp to the physical graph.
        var progress = Math.Clamp((minutes - 540d) / 390d, 0d, 1d);
        return FullLineWidth * progress;
    }

    public float Y(double value)
    {
        var y = Range == 0d
            ? DegenerateMidpoint
            : GraphMinimum + ((value - Minimum) / Range * GraphHeight);
        return LegacyChartSafety.FiniteFloat(y, "Intraday Y position");
    }
}

internal static class LegacyChartSafety
{
    public static float FiniteFloat(double value, string name)
    {
        if (!double.IsFinite(value) || value < -float.MaxValue || value > float.MaxValue)
        {
            throw new LegacySceneDataException(name + " must be a finite float.");
        }

        return (float)value;
    }
}
