#nullable enable

using System.Data;
using System.Globalization;
using MMoneyCoderSharp.Data;

namespace MBN_STOCK_WEBVIEW.Core.Playout.Scenes;

public enum S5001StockTradingStatus
{
    Regular,
    Halted
}

public abstract record S5001SceneLoadRequest
{
    private protected S5001SceneLoadRequest()
    {
    }

    public abstract S5001Branch Branch { get; }
}

public sealed record S5001BalticDryIndexLoadRequest : S5001SceneLoadRequest
{
    public override S5001Branch Branch => S5001Branch.BalticDryIndex;
}

public sealed record S5001CommodityLoadRequest(S5001CommodityTarget Target) : S5001SceneLoadRequest
{
    public override S5001Branch Branch => S5001Branch.Commodity;
}

public sealed record S5001DomesticIndexLoadRequest(
    S5001DomesticIndexTarget Target,
    S5001IndexMode Mode) : S5001SceneLoadRequest
{
    public override S5001Branch Branch => S5001Branch.DomesticIndex;
}

public sealed record S5001ForeignIndexLoadRequest(S5001ForeignIndexTarget Target) : S5001SceneLoadRequest
{
    public override S5001Branch Branch => S5001Branch.ForeignIndex;
}

public sealed record S5001ExchangeRateLoadRequest(S5001ExchangeRateTarget Target) : S5001SceneLoadRequest
{
    public override S5001Branch Branch => S5001Branch.ExchangeRate;
}

public sealed record S5001DomesticIndustryLoadRequest(
    LegacyDomesticEquityMarket Market,
    string IndustryName) : S5001SceneLoadRequest
{
    public override S5001Branch Branch => S5001Branch.DomesticIndustry;
}

public sealed record S5001ForeignIndustryLoadRequest(string IndustryName) : S5001SceneLoadRequest
{
    public override S5001Branch Branch => S5001Branch.ForeignIndustry;
}

/// <summary>
/// Internal-only exact UC5 industry identity. Native selector rows carry the
/// master input name and symbol in addition to the visible Korean name.
/// </summary>
internal sealed record S5001ExactForeignIndustryLoadRequest(
    string IndustryName,
    string InputName,
    string ForeignSymbol,
    string ForeignNationCode) : S5001SceneLoadRequest
{
    public override S5001Branch Branch => S5001Branch.ForeignIndustry;
}

public sealed record S5001ForeignStockLoadRequest(string StockName) : S5001SceneLoadRequest
{
    public override S5001Branch Branch => S5001Branch.ForeignStock;
}

/// <summary>
/// Internal-only exact UC5 identity. The public request contract intentionally
/// exposes no symbol or nation fields to callers outside the native scene path.
/// </summary>
internal sealed record S5001ExactForeignStockLoadRequest(
    string StockName,
    string ForeignSymbol,
    string ForeignNationCode) : S5001SceneLoadRequest
{
    public override S5001Branch Branch => S5001Branch.ForeignStock;
}

public sealed record S5001DomesticStockLoadRequest(
    LegacyDomesticEquityMarket Market,
    S5001DomesticStockMode Mode,
    S5001NxtBadge Badge,
    S5001StockTradingStatus TradingStatus,
    string StockName) : S5001SceneLoadRequest
{
    public override S5001Branch Branch => S5001Branch.DomesticStock;
}

public sealed class S5001SceneDataLoader
{
    private readonly IDataQueryExecutor _executor;

    public S5001SceneDataLoader(IDataQueryExecutor executor)
    {
        _executor = executor ?? throw new ArgumentNullException(nameof(executor));
    }

    public async Task<S5001SceneData> LoadAsync(
        S5001SceneLoadRequest request,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(request);
        return request switch
        {
            S5001BalticDryIndexLoadRequest => await LoadBalticDryIndexAsync(cancellationToken).ConfigureAwait(false),
            S5001CommodityLoadRequest commodity => await LoadCommodityAsync(commodity, cancellationToken).ConfigureAwait(false),
            S5001DomesticIndexLoadRequest index => await LoadDomesticIndexAsync(index, cancellationToken).ConfigureAwait(false),
            S5001ForeignIndexLoadRequest index => await LoadForeignIndexAsync(index, cancellationToken).ConfigureAwait(false),
            S5001ExchangeRateLoadRequest exchange => await LoadExchangeAsync(exchange, cancellationToken).ConfigureAwait(false),
            S5001DomesticIndustryLoadRequest industry => await LoadDomesticIndustryAsync(industry, cancellationToken).ConfigureAwait(false),
            S5001ForeignIndustryLoadRequest industry => await LoadForeignIndustryAsync(industry, cancellationToken).ConfigureAwait(false),
            S5001ExactForeignIndustryLoadRequest industry => await LoadExactForeignIndustryAsync(industry, cancellationToken).ConfigureAwait(false),
            S5001ForeignStockLoadRequest stock => await LoadForeignStockAsync(stock, cancellationToken).ConfigureAwait(false),
            S5001ExactForeignStockLoadRequest stock => await LoadExactForeignStockAsync(stock, cancellationToken).ConfigureAwait(false),
            S5001DomesticStockLoadRequest stock => await LoadDomesticStockAsync(stock, cancellationToken).ConfigureAwait(false),
            _ => throw new LegacySceneDataException("Unsupported s5001 load request.")
        };
    }

    private async Task<S5001SceneData> LoadBalticDryIndexAsync(CancellationToken cancellationToken)
    {
        var row = await ExecuteSingleAsync(
            ParameterizedMarketSceneQueries.WorldFuture("MK@BDI"),
            cancellationToken).ConfigureAwait(false);
        return new S5001BalticDryIndexSceneData(
            ParameterizedSceneRowReader.String(row, "TITLE"),
            ParameterizedSceneRowReader.Direction(row, "DIRECTION"),
            ParameterizedSceneRowReader.Int64(row, "PRICE"),
            ParameterizedSceneRowReader.Int64(row, "NET_CHANGE"),
            ParameterizedSceneRowReader.Decimal(row, "RATE"));
    }

    private async Task<S5001SceneData> LoadCommodityAsync(
        S5001CommodityLoadRequest request,
        CancellationToken cancellationToken)
    {
        S5001SceneMutationBuilder.RequireDefined(request.Target, nameof(request.Target));
        var row = await ExecuteSingleAsync(
            ParameterizedMarketSceneQueries.WorldFuture(CommoditySymbol(request.Target)),
            cancellationToken).ConfigureAwait(false);
        return new S5001CommoditySceneData(
            request.Target,
            ParameterizedSceneRowReader.String(row, "TITLE"),
            ParameterizedSceneRowReader.Direction(row, "DIRECTION"),
            ParameterizedSceneRowReader.Decimal(row, "PRICE"),
            ParameterizedSceneRowReader.Decimal(row, "NET_CHANGE"),
            ParameterizedSceneRowReader.Decimal(row, "RATE"));
    }

    private async Task<S5001SceneData> LoadDomesticIndexAsync(
        S5001DomesticIndexLoadRequest request,
        CancellationToken cancellationToken)
    {
        S5001SceneMutationBuilder.RequireDefined(request.Target, nameof(request.Target));
        S5001SceneMutationBuilder.RequireDefined(request.Mode, nameof(request.Mode));
        var target = ToMarketTarget(request.Target);
        var row = await ExecuteSingleAsync(
            ParameterizedMarketSceneQueries.MarketQuote(target, request.Mode == S5001IndexMode.Expected),
            cancellationToken).ConfigureAwait(false);
        return new S5001DomesticIndexSceneData(
            request.Target,
            request.Mode,
            ParameterizedSceneRowReader.String(row, "TITLE"),
            ParameterizedSceneRowReader.Direction(row, "DIRECTION"),
            ParameterizedSceneRowReader.Decimal(row, "PRICE"),
            ParameterizedSceneRowReader.Decimal(row, "NET_CHANGE"),
            ParameterizedSceneRowReader.Decimal(row, "RATE"));
    }

    private async Task<S5001SceneData> LoadForeignIndexAsync(
        S5001ForeignIndexLoadRequest request,
        CancellationToken cancellationToken)
    {
        S5001SceneMutationBuilder.RequireDefined(request.Target, nameof(request.Target));
        var row = await ExecuteSingleAsync(
            ParameterizedMarketSceneQueries.MarketQuote(ToMarketTarget(request.Target), expected: false),
            cancellationToken).ConfigureAwait(false);
        return new S5001ForeignIndexSceneData(
            request.Target,
            ParameterizedSceneRowReader.String(row, "TITLE"),
            ParameterizedSceneRowReader.Direction(row, "DIRECTION"),
            ParameterizedSceneRowReader.Decimal(row, "PRICE"),
            ParameterizedSceneRowReader.Decimal(row, "NET_CHANGE"),
            ParameterizedSceneRowReader.Decimal(row, "RATE"));
    }

    private async Task<S5001SceneData> LoadExchangeAsync(
        S5001ExchangeRateLoadRequest request,
        CancellationToken cancellationToken)
    {
        S5001SceneMutationBuilder.RequireDefined(request.Target, nameof(request.Target));
        var row = await ExecuteSingleAsync(
            ParameterizedMarketSceneQueries.MarketQuote(ToMarketTarget(request.Target), expected: false),
            cancellationToken).ConfigureAwait(false);
        return new S5001ExchangeRateSceneData(
            request.Target,
            ParameterizedSceneRowReader.String(row, "TITLE"),
            ParameterizedSceneRowReader.Direction(row, "DIRECTION"),
            ParameterizedSceneRowReader.Decimal(row, "PRICE"),
            ParameterizedSceneRowReader.Decimal(row, "NET_CHANGE"),
            ParameterizedSceneRowReader.Decimal(row, "RATE"));
    }

    private async Task<S5001SceneData> LoadDomesticIndustryAsync(
        S5001DomesticIndustryLoadRequest request,
        CancellationToken cancellationToken)
    {
        S5001SceneMutationBuilder.RequireDomesticMarket(request.Market, allowNxt: true);
        var name = ParameterizedSceneRowReader.Selector(request.IndustryName, nameof(request.IndustryName));
        var row = await ExecuteSingleAsync(
            ParameterizedMarketSceneQueries.DomesticIndustry(request.Market, name),
            cancellationToken).ConfigureAwait(false);
        return new S5001DomesticIndustrySceneData(
            request.Market,
            ParameterizedSceneRowReader.String(row, "TITLE"),
            ParameterizedSceneRowReader.Direction(row, "DIRECTION"),
            ParameterizedSceneRowReader.Decimal(row, "PRICE"),
            ParameterizedSceneRowReader.Decimal(row, "NET_CHANGE"),
            ParameterizedSceneRowReader.Decimal(row, "RATE"));
    }

    private async Task<S5001SceneData> LoadForeignIndustryAsync(
        S5001ForeignIndustryLoadRequest request,
        CancellationToken cancellationToken)
    {
        var name = ParameterizedSceneRowReader.Selector(request.IndustryName, nameof(request.IndustryName));
        var row = await ExecuteSingleAsync(
            ParameterizedMarketSceneQueries.ForeignIndustry(name),
            cancellationToken).ConfigureAwait(false);
        return DecimalScene(row, static (title, direction, price, change, rate) =>
            new S5001ForeignIndustrySceneData(title, direction, price, change, rate));
    }

    private async Task<S5001SceneData> LoadExactForeignIndustryAsync(
        S5001ExactForeignIndustryLoadRequest request,
        CancellationToken cancellationToken)
    {
        var name = ExactForeignIndustryText(
            request.IndustryName,
            nameof(request.IndustryName));
        var inputName = ExactForeignIndustryText(
            request.InputName,
            nameof(request.InputName));
        var symbol = ParameterizedSceneRowReader.Selector(
            request.ForeignSymbol,
            nameof(request.ForeignSymbol));
        var nationCode = ParameterizedSceneRowReader.Selector(
            request.ForeignNationCode,
            nameof(request.ForeignNationCode));
        if (!LegacyNamedForeignStockRestoreService.IsCanonicalBoundSymbol(symbol) ||
            !string.Equals(nationCode, "US", StringComparison.Ordinal))
        {
            throw new LegacySceneDataException("The foreign-industry identity is invalid.");
        }

        var row = await ExecuteSingleAsync(
            ParameterizedMarketSceneQueries.ForeignIndustry(
                name,
                inputName,
                symbol,
                nationCode),
            cancellationToken).ConfigureAwait(false);
        return DecimalScene(row, static (title, direction, price, change, rate) =>
            new S5001ForeignIndustrySceneData(title, direction, price, change, rate));
    }

    private static string ExactForeignIndustryText(string? value, string name)
    {
        var text = LegacySceneBuilderGuard.Text(value, name);
        if (text.Length > 200 ||
            !string.Equals(text, text.Trim(), StringComparison.Ordinal) ||
            text.Any(character =>
                char.IsSurrogate(character) ||
                character == '\uFFFD' ||
                CharUnicodeInfo.GetUnicodeCategory(character) is
                    UnicodeCategory.Format or
                    UnicodeCategory.LineSeparator or
                    UnicodeCategory.ParagraphSeparator))
        {
            throw new LegacySceneDataException("The foreign-industry identity is invalid.");
        }

        return text;
    }

    private async Task<S5001SceneData> LoadForeignStockAsync(
        S5001ForeignStockLoadRequest request,
        CancellationToken cancellationToken)
    {
        var name = ParameterizedSceneRowReader.Selector(request.StockName, nameof(request.StockName));
        var row = await ExecuteSingleAsync(
            ParameterizedMarketSceneQueries.ForeignStock(name),
            cancellationToken).ConfigureAwait(false);
        return DecimalScene(row, static (title, direction, price, change, rate) =>
            new S5001ForeignStockSceneData(title, direction, price, change, rate));
    }

    private async Task<S5001SceneData> LoadExactForeignStockAsync(
        S5001ExactForeignStockLoadRequest request,
        CancellationToken cancellationToken)
    {
        var name = ParameterizedSceneRowReader.Selector(request.StockName, nameof(request.StockName));
        var symbol = ParameterizedSceneRowReader.Selector(
            request.ForeignSymbol,
            nameof(request.ForeignSymbol));
        var nationCode = ParameterizedSceneRowReader.Selector(
            request.ForeignNationCode,
            nameof(request.ForeignNationCode));
        if (!LegacyNamedForeignStockRestoreService.IsCanonicalBoundSymbol(symbol) ||
            nationCode.Length != 2 ||
            nationCode.Any(character => character is < 'A' or > 'Z'))
        {
            throw new LegacySceneDataException("The foreign-stock identity is invalid.");
        }

        var row = await ExecuteSingleAsync(
            ParameterizedMarketSceneQueries.ForeignStock(name, symbol, nationCode),
            cancellationToken).ConfigureAwait(false);
        return DecimalScene(row, static (title, direction, price, change, rate) =>
            new S5001ForeignStockSceneData(title, direction, price, change, rate));
    }

    private async Task<S5001SceneData> LoadDomesticStockAsync(
        S5001DomesticStockLoadRequest request,
        CancellationToken cancellationToken)
    {
        ValidateS5001StockRequest(request);
        var rawSelector = ParameterizedSceneRowReader.Selector(request.StockName, nameof(request.StockName));
        var selector = ParameterizedSceneRowReader.Selector(
            rawSelector.Replace("(NXT)", string.Empty, StringComparison.Ordinal).Trim(),
            nameof(request.StockName));
        var query = request.Mode switch
        {
            S5001DomesticStockMode.Current => ParameterizedMarketSceneQueries.CurrentStock(
                request.Market, selector, request.TradingStatus == S5001StockTradingStatus.Halted),
            S5001DomesticStockMode.ExpectedOpening => ParameterizedMarketSceneQueries.ExpectedStock(request.Market, selector),
            S5001DomesticStockMode.AfterHoursSinglePrice => ParameterizedMarketSceneQueries.AfterHoursStock(request.Market, selector),
            _ => throw new LegacySceneDataException("Unsupported s5001 stock mode.")
        };
        var row = await ExecuteSingleAsync(query, cancellationToken).ConfigureAwait(false);
        return new S5001DomesticStockSceneData(
            request.Market,
            request.Mode,
            request.Badge,
            ParameterizedSceneRowReader.String(row, "TITLE"),
            ParameterizedSceneRowReader.Direction(row, "DIRECTION"),
            ParameterizedSceneRowReader.Int64(row, "PRICE"),
            ParameterizedSceneRowReader.Int64(row, "NET_CHANGE"),
            ParameterizedSceneRowReader.Decimal(row, "RATE"));
    }

    private async Task<DataRow> ExecuteSingleAsync(
        ParameterizedSceneQuery query,
        CancellationToken cancellationToken)
    {
        var table = await _executor.ExecuteAsync(
            query.Source,
            query.TableName,
            query.Spec,
            cancellationToken).ConfigureAwait(false);
        return ParameterizedSceneRowReader.SingleQuote(table);
    }

    private static S5001SceneData DecimalScene(
        DataRow row,
        Func<string, ScenePriceDirection, decimal, decimal, decimal, S5001SceneData> factory) => factory(
        ParameterizedSceneRowReader.String(row, "TITLE"),
        ParameterizedSceneRowReader.Direction(row, "DIRECTION"),
        ParameterizedSceneRowReader.Decimal(row, "PRICE"),
        ParameterizedSceneRowReader.Decimal(row, "NET_CHANGE"),
        ParameterizedSceneRowReader.Decimal(row, "RATE"));

    private static void ValidateS5001StockRequest(S5001DomesticStockLoadRequest request)
    {
        S5001SceneMutationBuilder.RequireDomesticMarket(request.Market, allowNxt: true);
        S5001SceneMutationBuilder.RequireDefined(request.Mode, nameof(request.Mode));
        S5001SceneMutationBuilder.RequireDefined(request.Badge, nameof(request.Badge));
        S5001SceneMutationBuilder.RequireDefined(request.TradingStatus, nameof(request.TradingStatus));
        var nxt = request.Market is LegacyDomesticEquityMarket.NxtKospi or LegacyDomesticEquityMarket.NxtKosdaq;
        if (request.Mode != S5001DomesticStockMode.Current && nxt)
        {
            throw new LegacySceneDataException("Only current stock queries support NXT.");
        }

        if (request.TradingStatus == S5001StockTradingStatus.Halted &&
            (nxt || request.Mode != S5001DomesticStockMode.Current))
        {
            throw new LegacySceneDataException("The closed halted-stock query is available only for current KRX quotes.");
        }

        if (request.Mode == S5001DomesticStockMode.Current)
        {
            if (nxt == (request.Badge == S5001NxtBadge.None))
            {
                throw new LegacySceneDataException("The current stock badge does not match its market.");
            }
        }
        else if (request.Badge != S5001NxtBadge.None)
        {
            throw new LegacySceneDataException("A non-current stock query cannot carry an NXT badge.");
        }
    }

    internal static LegacyMarketQuoteTarget ToMarketTarget(S5001DomesticIndexTarget target) => target switch
    {
        S5001DomesticIndexTarget.Kospi => LegacyMarketQuoteTarget.Kospi,
        S5001DomesticIndexTarget.Kosdaq => LegacyMarketQuoteTarget.Kosdaq,
        S5001DomesticIndexTarget.Kospi200 => LegacyMarketQuoteTarget.Kospi200,
        S5001DomesticIndexTarget.Futures => LegacyMarketQuoteTarget.Futures,
        S5001DomesticIndexTarget.Krx100 => LegacyMarketQuoteTarget.Krx100,
        _ => throw new LegacySceneDataException("Unsupported domestic index target.")
    };

    internal static LegacyMarketQuoteTarget ToMarketTarget(S5001ForeignIndexTarget target) => target switch
    {
        S5001ForeignIndexTarget.Dow => LegacyMarketQuoteTarget.Dow,
        S5001ForeignIndexTarget.Nasdaq => LegacyMarketQuoteTarget.Nasdaq,
        S5001ForeignIndexTarget.Sp500 => LegacyMarketQuoteTarget.Sp500,
        S5001ForeignIndexTarget.GermanyDax => LegacyMarketQuoteTarget.GermanyDax,
        S5001ForeignIndexTarget.UnitedKingdomFtse => LegacyMarketQuoteTarget.UnitedKingdomFtse,
        S5001ForeignIndexTarget.FranceCac => LegacyMarketQuoteTarget.FranceCac,
        S5001ForeignIndexTarget.Nikkei => LegacyMarketQuoteTarget.Nikkei,
        S5001ForeignIndexTarget.ShanghaiComposite => LegacyMarketQuoteTarget.ShanghaiComposite,
        S5001ForeignIndexTarget.HangSeng => LegacyMarketQuoteTarget.HangSeng,
        S5001ForeignIndexTarget.TaiwanWeighted => LegacyMarketQuoteTarget.TaiwanWeighted,
        S5001ForeignIndexTarget.SingaporeStraitsTimes => LegacyMarketQuoteTarget.SingaporeStraitsTimes,
        S5001ForeignIndexTarget.ThailandSet => LegacyMarketQuoteTarget.ThailandSet,
        S5001ForeignIndexTarget.PhilippinesComposite => LegacyMarketQuoteTarget.PhilippinesComposite,
        S5001ForeignIndexTarget.MalaysiaKlse => LegacyMarketQuoteTarget.MalaysiaKlse,
        S5001ForeignIndexTarget.IndonesiaComposite => LegacyMarketQuoteTarget.IndonesiaComposite,
        _ => throw new LegacySceneDataException("Unsupported foreign index target.")
    };

    internal static LegacyMarketQuoteTarget ToMarketTarget(S5001ExchangeRateTarget target) => target switch
    {
        S5001ExchangeRateTarget.WonDollar => LegacyMarketQuoteTarget.WonDollar,
        S5001ExchangeRateTarget.WonYen => LegacyMarketQuoteTarget.WonYen,
        S5001ExchangeRateTarget.WonYuan => LegacyMarketQuoteTarget.WonYuan,
        S5001ExchangeRateTarget.WonEuro => LegacyMarketQuoteTarget.WonEuro,
        _ => throw new LegacySceneDataException("Unsupported exchange-rate target.")
    };

    private static string CommoditySymbol(S5001CommodityTarget target) => target switch
    {
        S5001CommodityTarget.FeedWheat => "MK@WHEAT",
        S5001CommodityTarget.Corn => "CBT$CORN",
        S5001CommodityTarget.Wheat => "CBT@WHEAT",
        S5001CommodityTarget.Soybean => "CBT@SOYBEAN",
        S5001CommodityTarget.Bean => "MK@BEAN",
        S5001CommodityTarget.BrownRice => "MK@RICE",
        S5001CommodityTarget.Coffee => "NYB@KC",
        S5001CommodityTarget.Cocoa => "MK@COCOA",
        S5001CommodityTarget.Sugar => "NYB@SB",
        S5001CommodityTarget.RawCotton => "MK@COTTON",
        S5001CommodityTarget.Cotton => "NYB@CT",
        S5001CommodityTarget.LiveCattle => "MK@LC",
        S5001CommodityTarget.FeederCattle => "MK@FC",
        S5001CommodityTarget.LeanHogs => "MK@PORK",
        S5001CommodityTarget.Copper => "LME@CDY",
        S5001CommodityTarget.IronOre => "MK@IRON",
        S5001CommodityTarget.Nickel => "LME@NDY",
        S5001CommodityTarget.NaturalGas => "NYM@NG",
        S5001CommodityTarget.Platinum => "NYM@PL",
        S5001CommodityTarget.Palladium => "NYM@PA",
        S5001CommodityTarget.Lead => "MK@PDA",
        S5001CommodityTarget.Zinc => "MK@ZDA",
        S5001CommodityTarget.Tin => "LME@SDY",
        S5001CommodityTarget.InternationalGold => "COM@GC",
        S5001CommodityTarget.InternationalSilver => "COM@SI",
        S5001CommodityTarget.DomesticGold => "MK@GC",
        S5001CommodityTarget.DomesticSilver => "MK@SI",
        _ => throw new LegacySceneDataException("Unsupported commodity target.")
    };
}

public abstract record S5032SceneLoadRequest
{
    private protected S5032SceneLoadRequest()
    {
    }

    public abstract S5032Branch Branch { get; }
}

public sealed record S5032IndexAndFuturesLoadRequest(
    LegacyMarketQuoteTarget PrimaryTarget) : S5032SceneLoadRequest
{
    public override S5032Branch Branch => S5032Branch.IndexAndFutures;
}

public sealed record S5032DomesticIndustryPairLoadRequest(
    LegacyDomesticEquityMarket Market,
    string FirstIndustry,
    string SecondIndustry) : S5032SceneLoadRequest
{
    public override S5032Branch Branch => S5032Branch.DomesticIndustryPair;
}

public sealed record S5032DomesticStockPairLoadRequest(
    LegacyDomesticEquityMarket FirstMarket,
    string FirstStock,
    LegacyDomesticEquityMarket SecondMarket,
    string SecondStock) : S5032SceneLoadRequest
{
    public override S5032Branch Branch => S5032Branch.DomesticStockPair;
}

public sealed class S5032SceneDataLoader
{
    private readonly IDataQueryExecutor _executor;

    public S5032SceneDataLoader(IDataQueryExecutor executor)
    {
        _executor = executor ?? throw new ArgumentNullException(nameof(executor));
    }

    public async Task<S5032SceneData> LoadAsync(
        S5032SceneLoadRequest request,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(request);
        return request switch
        {
            S5032IndexAndFuturesLoadRequest index => await LoadIndexAsync(index, cancellationToken).ConfigureAwait(false),
            S5032DomesticIndustryPairLoadRequest industry => await LoadIndustryAsync(industry, cancellationToken).ConfigureAwait(false),
            S5032DomesticStockPairLoadRequest stocks => await LoadStocksAsync(stocks, cancellationToken).ConfigureAwait(false),
            _ => throw new LegacySceneDataException("Unsupported s5032 load request.")
        };
    }

    private async Task<S5032SceneData> LoadIndexAsync(
        S5032IndexAndFuturesLoadRequest request,
        CancellationToken cancellationToken)
    {
        S5001SceneMutationBuilder.RequireDefined(request.PrimaryTarget, nameof(request.PrimaryTarget));
        if (request.PrimaryTarget == LegacyMarketQuoteTarget.Futures)
        {
            throw new LegacySceneDataException("s5032 normalizes futures to the companion side.");
        }

        var primary = await LoadMarketQuoteAsync(request.PrimaryTarget, cancellationToken).ConfigureAwait(false);
        var futureQuery = ParameterizedMarketSceneQueries.FuturesDetail();
        var futureTable = await ExecuteAsync(futureQuery, cancellationToken).ConfigureAwait(false);
        var future = ParameterizedSceneRowReader.SingleFuture(futureTable);
        return new S5032IndexAndFuturesSceneData(
            request.PrimaryTarget,
            LegacyMarketQuoteTarget.Futures,
            new S5032PrimaryQuote(primary.Title, primary.Price, primary.NetChange, primary.Rate, primary.Direction),
            new S5032CompanionQuote(
                ParameterizedSceneRowReader.String(future, "TITLE"),
                ParameterizedSceneRowReader.Decimal(future, "PRICE"),
                ParameterizedSceneRowReader.Decimal(future, "NET_CHANGE"),
                ToCompanionSignal(ParameterizedSceneRowReader.Direction(future, "DIRECTION")),
                ParameterizedSceneRowReader.Decimal(future, "NET_VOLUME"),
                ParameterizedSceneRowReader.Decimal(future, "NET_TURNOVER"),
                ParameterizedSceneRowReader.Decimal(future, "OUTSTANDING_VOLUME")));
    }

    private async Task<S5032SceneData> LoadIndustryAsync(
        S5032DomesticIndustryPairLoadRequest request,
        CancellationToken cancellationToken)
    {
        S5001SceneMutationBuilder.RequireDomesticMarket(request.Market, allowNxt: false);
        var firstName = ParameterizedSceneRowReader.Selector(request.FirstIndustry, nameof(request.FirstIndustry));
        var secondName = ParameterizedSceneRowReader.Selector(request.SecondIndustry, nameof(request.SecondIndustry));
        var first = await LoadQuoteAsync(
            ParameterizedMarketSceneQueries.DomesticIndustry(request.Market, firstName),
            cancellationToken).ConfigureAwait(false);
        var second = await LoadQuoteAsync(
            ParameterizedMarketSceneQueries.DomesticIndustry(request.Market, secondName),
            cancellationToken).ConfigureAwait(false);
        return new S5032DomesticIndustryPairSceneData(
            request.Market,
            new S5032PrimaryQuote(first.Title, first.Price, first.NetChange, first.Rate, first.Direction),
            new S5032CompanionQuote(second.Title, second.Price, second.NetChange,
                ToCompanionSignal(second.Direction), 0m, 0m, 0m));
    }

    private async Task<S5032SceneData> LoadStocksAsync(
        S5032DomesticStockPairLoadRequest request,
        CancellationToken cancellationToken)
    {
        S5001SceneMutationBuilder.RequireDomesticMarket(request.FirstMarket, allowNxt: false);
        S5001SceneMutationBuilder.RequireDomesticMarket(request.SecondMarket, allowNxt: false);
        var firstName = ParameterizedSceneRowReader.Selector(request.FirstStock, nameof(request.FirstStock));
        var secondName = ParameterizedSceneRowReader.Selector(request.SecondStock, nameof(request.SecondStock));
        var first = await LoadQuoteAsync(
            ParameterizedMarketSceneQueries.CurrentStock(request.FirstMarket, firstName, halted: false),
            cancellationToken).ConfigureAwait(false);
        var second = await LoadQuoteAsync(
            ParameterizedMarketSceneQueries.CurrentStock(request.SecondMarket, secondName, halted: false),
            cancellationToken).ConfigureAwait(false);
        return new S5032DomesticStockPairSceneData(
            request.FirstMarket,
            request.SecondMarket,
            ToStockQuote(first),
            ToStockQuote(second));
    }

    private async Task<ParameterizedQuoteRow> LoadMarketQuoteAsync(
        LegacyMarketQuoteTarget target,
        CancellationToken cancellationToken) => await LoadQuoteAsync(
        ParameterizedMarketSceneQueries.MarketQuote(target, expected: false),
        cancellationToken).ConfigureAwait(false);

    private async Task<ParameterizedQuoteRow> LoadQuoteAsync(
        ParameterizedSceneQuery query,
        CancellationToken cancellationToken)
    {
        var table = await ExecuteAsync(query, cancellationToken).ConfigureAwait(false);
        return ParameterizedSceneRowReader.Quote(ParameterizedSceneRowReader.SingleQuote(table));
    }

    private Task<DataTable> ExecuteAsync(
        ParameterizedSceneQuery query,
        CancellationToken cancellationToken) => _executor.ExecuteAsync(
        query.Source, query.TableName, query.Spec, cancellationToken);

    private static S5032StockQuote ToStockQuote(ParameterizedQuoteRow value) => new(
        value.Title,
        ParameterizedSceneRowReader.CheckedInt64(value.Price),
        ParameterizedSceneRowReader.CheckedInt64(value.NetChange),
        value.Rate,
        value.Direction);

    private static S5032CompanionSignal ToCompanionSignal(ScenePriceDirection direction) => direction switch
    {
        ScenePriceDirection.LimitUp or ScenePriceDirection.Up => S5032CompanionSignal.Positive,
        ScenePriceDirection.Flat => S5032CompanionSignal.Flat,
        ScenePriceDirection.Down or ScenePriceDirection.LimitDown => S5032CompanionSignal.Negative,
        _ => throw new LegacySceneDataException("The companion quote returned an unknown direction.")
    };
}

public enum S8018MarketDataMode
{
    Current,
    Expected
}

public abstract record S8018SceneLoadRequest
{
    private protected S8018SceneLoadRequest()
    {
    }

    public abstract S8018Branch Branch { get; }
}

public sealed record S8018MarketPairLoadRequest(
    S8018MarketDataMode Mode,
    LegacyMarketQuoteTarget FirstTarget,
    LegacyMarketQuoteTarget SecondTarget) : S8018SceneLoadRequest
{
    public override S8018Branch Branch => S8018Branch.MarketPair;
}

public sealed record S8018MixedMarketAndStockLoadRequest(
    S8018PairSide StockSide,
    LegacyMarketQuoteTarget MarketTarget,
    LegacyDomesticEquityMarket StockMarket,
    string StockName,
    S8018MarketDataMode Mode = S8018MarketDataMode.Current) : S8018SceneLoadRequest
{
    public override S8018Branch Branch => S8018Branch.MixedMarketAndStock;
}

public sealed record S8018ExpectedStockPairLoadRequest(
    LegacyDomesticEquityMarket FirstMarket,
    string FirstStock,
    LegacyDomesticEquityMarket SecondMarket,
    string SecondStock) : S8018SceneLoadRequest
{
    public override S8018Branch Branch => S8018Branch.ExpectedStockPair;
}

public sealed record S8018CurrentStockSelection(S8018StockMarket Market, string StockName);

public sealed record S8018CurrentStockPairLoadRequest(
    S8018SessionPhase Session,
    S8018CurrentStockSelection First,
    S8018CurrentStockSelection Second) : S8018SceneLoadRequest
{
    public override S8018Branch Branch => S8018Branch.CurrentStockPair;
}

public sealed record S8018DomesticIndustryPairLoadRequest(
    LegacyDomesticEquityMarket Market,
    string FirstIndustry,
    string SecondIndustry) : S8018SceneLoadRequest
{
    public override S8018Branch Branch => S8018Branch.DomesticIndustryPair;
}

public sealed class S8018SceneDataLoader
{
    private readonly IDataQueryExecutor _executor;

    public S8018SceneDataLoader(IDataQueryExecutor executor)
    {
        _executor = executor ?? throw new ArgumentNullException(nameof(executor));
    }

    public async Task<S8018SceneData> LoadAsync(
        S8018SceneLoadRequest request,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(request);
        return request switch
        {
            S8018MarketPairLoadRequest pair => await LoadMarketPairAsync(pair, cancellationToken).ConfigureAwait(false),
            S8018MixedMarketAndStockLoadRequest mixed => await LoadMixedAsync(mixed, cancellationToken).ConfigureAwait(false),
            S8018ExpectedStockPairLoadRequest expected => await LoadExpectedAsync(expected, cancellationToken).ConfigureAwait(false),
            S8018CurrentStockPairLoadRequest current => await LoadCurrentAsync(current, cancellationToken).ConfigureAwait(false),
            S8018DomesticIndustryPairLoadRequest industry => await LoadIndustryAsync(industry, cancellationToken).ConfigureAwait(false),
            _ => throw new LegacySceneDataException("Unsupported s8018 load request.")
        };
    }

    private async Task<S8018SceneData> LoadMarketPairAsync(
        S8018MarketPairLoadRequest request,
        CancellationToken cancellationToken)
    {
        S5001SceneMutationBuilder.RequireDefined(request.Mode, nameof(request.Mode));
        ValidateMarketTarget(request.FirstTarget, request.Mode);
        ValidateMarketTarget(request.SecondTarget, request.Mode);
        var expected = request.Mode == S8018MarketDataMode.Expected;
        var first = await LoadQuoteAsync(
            ParameterizedMarketSceneQueries.MarketQuote(request.FirstTarget, expected),
            cancellationToken).ConfigureAwait(false);
        var second = await LoadQuoteAsync(
            ParameterizedMarketSceneQueries.MarketQuote(request.SecondTarget, expected),
            cancellationToken).ConfigureAwait(false);
        return new S8018MarketPairSceneData(
            request.FirstTarget,
            request.SecondTarget,
            ToMarketQuote(first),
            ToMarketQuote(second));
    }

    private async Task<S8018SceneData> LoadMixedAsync(
        S8018MixedMarketAndStockLoadRequest request,
        CancellationToken cancellationToken)
    {
        S5001SceneMutationBuilder.RequireDefined(request.StockSide, nameof(request.StockSide));
        S5001SceneMutationBuilder.RequireDefined(request.Mode, nameof(request.Mode));
        ValidateMarketTarget(request.MarketTarget, request.Mode);
        S5001SceneMutationBuilder.RequireDomesticMarket(
            request.StockMarket,
            allowNxt: request.Mode == S8018MarketDataMode.Current);
        var stockName = ParameterizedSceneRowReader.Selector(request.StockName, nameof(request.StockName));
        var expected = request.Mode == S8018MarketDataMode.Expected;
        var stockQuery = expected
            ? ParameterizedMarketSceneQueries.ExpectedStock(request.StockMarket, stockName)
            : ParameterizedMarketSceneQueries.CurrentStock(request.StockMarket, stockName, halted: false);
        var marketQuery = ParameterizedMarketSceneQueries.MarketQuote(request.MarketTarget, expected);

        ParameterizedQuoteRow market;
        ParameterizedQuoteRow stock;
        if (request.StockSide == S8018PairSide.First)
        {
            stock = await LoadQuoteAsync(stockQuery, cancellationToken).ConfigureAwait(false);
            market = await LoadQuoteAsync(marketQuery, cancellationToken).ConfigureAwait(false);
        }
        else
        {
            market = await LoadQuoteAsync(marketQuery, cancellationToken).ConfigureAwait(false);
            stock = await LoadQuoteAsync(stockQuery, cancellationToken).ConfigureAwait(false);
        }

        return new S8018MixedMarketAndStockSceneData(
            request.StockSide,
            request.MarketTarget,
            ToMarketQuote(market),
            new S8018MixedStockQuote(
                request.StockMarket,
                stock.Title,
                ParameterizedSceneRowReader.CheckedInt64(stock.Price),
                ParameterizedSceneRowReader.CheckedInt64(stock.NetChange),
                stock.Rate,
                stock.Direction));
    }

    private async Task<S8018SceneData> LoadExpectedAsync(
        S8018ExpectedStockPairLoadRequest request,
        CancellationToken cancellationToken)
    {
        S5001SceneMutationBuilder.RequireDomesticMarket(request.FirstMarket, allowNxt: false);
        S5001SceneMutationBuilder.RequireDomesticMarket(request.SecondMarket, allowNxt: false);
        var firstName = ParameterizedSceneRowReader.Selector(request.FirstStock, nameof(request.FirstStock));
        var secondName = ParameterizedSceneRowReader.Selector(request.SecondStock, nameof(request.SecondStock));
        var first = await LoadQuoteAsync(
            ParameterizedMarketSceneQueries.ExpectedStock(request.FirstMarket, firstName),
            cancellationToken).ConfigureAwait(false);
        var second = await LoadQuoteAsync(
            ParameterizedMarketSceneQueries.ExpectedStock(request.SecondMarket, secondName),
            cancellationToken).ConfigureAwait(false);
        return new S8018ExpectedStockPairSceneData(
            ToExpectedStock(request.FirstMarket, first),
            ToExpectedStock(request.SecondMarket, second));
    }

    private async Task<S8018SceneData> LoadCurrentAsync(
        S8018CurrentStockPairLoadRequest request,
        CancellationToken cancellationToken)
    {
        S5001SceneMutationBuilder.RequireDefined(request.Session, nameof(request.Session));
        var firstSelection = ValidateSelection(request.First, nameof(request.First));
        var secondSelection = ValidateSelection(request.Second, nameof(request.Second));
        var first = await LoadCurrentSelectionAsync(firstSelection, cancellationToken).ConfigureAwait(false);
        var second = await LoadCurrentSelectionAsync(secondSelection, cancellationToken).ConfigureAwait(false);
        return new S8018CurrentStockPairSceneData(
            request.Session,
            ToCurrentStock(firstSelection.Market, first),
            ToCurrentStock(secondSelection.Market, second));
    }

    private async Task<S8018SceneData> LoadIndustryAsync(
        S8018DomesticIndustryPairLoadRequest request,
        CancellationToken cancellationToken)
    {
        S5001SceneMutationBuilder.RequireDomesticMarket(request.Market, allowNxt: false);
        var firstName = ParameterizedSceneRowReader.Selector(request.FirstIndustry, nameof(request.FirstIndustry));
        var secondName = ParameterizedSceneRowReader.Selector(request.SecondIndustry, nameof(request.SecondIndustry));
        var first = await LoadQuoteAsync(
            ParameterizedMarketSceneQueries.DomesticIndustry(request.Market, firstName),
            cancellationToken).ConfigureAwait(false);
        var second = await LoadQuoteAsync(
            ParameterizedMarketSceneQueries.DomesticIndustry(request.Market, secondName),
            cancellationToken).ConfigureAwait(false);
        return new S8018DomesticIndustryPairSceneData(
            request.Market,
            ToMarketQuote(first),
            ToMarketQuote(second));
    }

    private async Task<ParameterizedQuoteRow> LoadCurrentSelectionAsync(
        S8018CurrentStockSelection selection,
        CancellationToken cancellationToken)
    {
        var rawName = ParameterizedSceneRowReader.Selector(
            selection.StockName,
            nameof(selection.StockName));
        var name = ParameterizedSceneRowReader.Selector(
            rawName.Replace("(NXT)", string.Empty, StringComparison.Ordinal).Trim(),
            nameof(selection.StockName));
        var query = selection.Market switch
        {
            S8018StockMarket.Kospi => ParameterizedMarketSceneQueries.CurrentStock(
                LegacyDomesticEquityMarket.Kospi, name, halted: false),
            S8018StockMarket.Kosdaq => ParameterizedMarketSceneQueries.CurrentStock(
                LegacyDomesticEquityMarket.Kosdaq, name, halted: false),
            S8018StockMarket.NxtKospi => ParameterizedMarketSceneQueries.CurrentStock(
                LegacyDomesticEquityMarket.NxtKospi, name, halted: false),
            S8018StockMarket.NxtKosdaq => ParameterizedMarketSceneQueries.CurrentStock(
                LegacyDomesticEquityMarket.NxtKosdaq, name, halted: false),
            S8018StockMarket.World => ParameterizedMarketSceneQueries.ForeignStock(name),
            _ => throw new LegacySceneDataException("Unsupported s8018 stock market.")
        };
        return await LoadQuoteAsync(query, cancellationToken).ConfigureAwait(false);
    }

    private async Task<ParameterizedQuoteRow> LoadQuoteAsync(
        ParameterizedSceneQuery query,
        CancellationToken cancellationToken)
    {
        var table = await _executor.ExecuteAsync(
            query.Source, query.TableName, query.Spec, cancellationToken).ConfigureAwait(false);
        return ParameterizedSceneRowReader.Quote(ParameterizedSceneRowReader.SingleQuote(table));
    }

    private static S8018CurrentStockSelection ValidateSelection(
        S8018CurrentStockSelection? selection,
        string name)
    {
        var value = LegacySceneBuilderGuard.NotNull(selection, name);
        S5001SceneMutationBuilder.RequireDefined(value.Market, name + ".Market");
        var stockName = ParameterizedSceneRowReader.Selector(value.StockName, name + ".StockName");
        return value with { StockName = stockName };
    }

    private static void ValidateMarketTarget(
        LegacyMarketQuoteTarget target,
        S8018MarketDataMode mode)
    {
        S5001SceneMutationBuilder.RequireDefined(target, nameof(target));
        if (mode == S8018MarketDataMode.Expected && target is not (
            LegacyMarketQuoteTarget.Kospi or LegacyMarketQuoteTarget.Kosdaq or
            LegacyMarketQuoteTarget.Kospi200 or LegacyMarketQuoteTarget.Futures or
            LegacyMarketQuoteTarget.Krx100))
        {
            throw new LegacySceneDataException("Expected market data is supported only for domestic index targets.");
        }
    }

    private static S8018MarketQuote ToMarketQuote(ParameterizedQuoteRow value) => new(
        value.Title, value.Price, value.NetChange, value.Rate, value.Direction);

    private static S8018ExpectedStockQuote ToExpectedStock(
        LegacyDomesticEquityMarket market,
        ParameterizedQuoteRow value) => new(
        market,
        value.Title,
        ParameterizedSceneRowReader.CheckedInt64(value.Price),
        ParameterizedSceneRowReader.CheckedInt64(value.NetChange),
        value.Rate,
        value.Direction);

    private static S8018CurrentStockQuote ToCurrentStock(
        S8018StockMarket market,
        ParameterizedQuoteRow value) => new(
        market, value.Title, value.Price, value.NetChange, value.Rate, value.Direction);
}

public sealed record S8001SceneLoadRequest(S8001Market Market);

public sealed class S8001SceneDataLoader
{
    private readonly IDataQueryExecutor _executor;

    public S8001SceneDataLoader(IDataQueryExecutor executor)
    {
        _executor = executor ?? throw new ArgumentNullException(nameof(executor));
    }

    public async Task<S8001SceneData> LoadAsync(
        S8001SceneLoadRequest request,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(request);
        S5001SceneMutationBuilder.RequireDefined(request.Market, nameof(request.Market));
        var query = ParameterizedMarketSceneQueries.Sectors(request.Market);
        var table = await _executor.ExecuteAsync(
            query.Source, query.TableName, query.Spec, cancellationToken).ConfigureAwait(false);
        var expectedCount = request.Market == S8001Market.Kospi ? 15 : 6;
        var rows = ParameterizedSceneRowReader.ExactRows(
            table,
            expectedCount,
            expectedCount,
            "SECTOR_CODE", "SECTOR_NAME", "DIRECTION", "MAGNITUDE");
        var quotes = new List<S8001SectorQuote>(expectedCount);
        var seen = new HashSet<S8001Sector>();
        foreach (var row in rows)
        {
            var sector = request.Market == S8001Market.Kospi
                ? KospiSector(ParameterizedSceneRowReader.String(row, "SECTOR_CODE"))
                : KosdaqSector(ParameterizedSceneRowReader.String(row, "SECTOR_NAME"));
            if (!seen.Add(sector))
            {
                throw new LegacySceneDataException("The s8001 query returned a duplicate sector.");
            }

            quotes.Add(new S8001SectorQuote(
                sector,
                ParameterizedSceneRowReader.SectorSignal(row, "DIRECTION"),
                ParameterizedSceneRowReader.NonNegativeDecimal(row, "MAGNITUDE")));
        }

        return new S8001SceneData(request.Market, quotes);
    }

    private static S8001Sector KospiSector(string value) => value switch
    {
        "005" => S8001Sector.FoodAndBeverage,
        "006" => S8001Sector.TextilesAndApparel,
        "007" => S8001Sector.PaperAndWood,
        "008" => S8001Sector.Chemicals,
        // The live taxonomy now names code 009 "제약" while the authoritative
        // 8001 cut keeps the legacy on-air object name "의약품".
        "009" => S8001Sector.Pharmaceuticals,
        "011" => S8001Sector.SteelAndMetal,
        "012" => S8001Sector.Machinery,
        "013" => S8001Sector.ElectricalAndElectronics,
        "014" => S8001Sector.MedicalPrecision,
        "015" => S8001Sector.TransportationEquipment,
        "016" => S8001Sector.Distribution,
        "018" => S8001Sector.Construction,
        "019" => S8001Sector.TransportationWarehouse,
        "020" => S8001Sector.Communications,
        "021" => S8001Sector.Finance,
        _ => throw new LegacySceneDataException("The s8001 query returned an unknown closed sector code.")
    };

    private static S8001Sector KosdaqSector(string value) => value switch
    {
        "기계장비" => S8001Sector.MachineryAndEquipment,
        "IT부품" => S8001Sector.ItComponents,
        "운송" => S8001Sector.Transportation,
        "종이목재" => S8001Sector.PaperAndWood,
        "반도체" => S8001Sector.Semiconductor,
        "인터넷" => S8001Sector.Internet,
        _ => throw new LegacySceneDataException("The s8001 query returned an unknown closed sector.")
    };
}

internal sealed record ParameterizedQuoteRow(
    string Title,
    decimal Price,
    decimal NetChange,
    decimal Rate,
    ScenePriceDirection Direction);

internal static class ParameterizedSceneRowReader
{
    private static readonly string[] QuoteColumns = ["TITLE", "PRICE", "DIRECTION", "NET_CHANGE", "RATE"];
    private static readonly string[] FutureColumns =
    [
        "TITLE", "PRICE", "DIRECTION", "NET_CHANGE", "RATE",
        "NET_VOLUME", "OUTSTANDING_VOLUME", "NET_TURNOVER"
    ];

    public static DataRow SingleQuote(DataTable? table) =>
        ExactRows(table, 1, 1, QuoteColumns)[0];

    public static DataRow SingleFuture(DataTable? table) =>
        ExactRows(table, 1, 1, FutureColumns)[0];

    public static IReadOnlyList<DataRow> ExactRows(
        DataTable? table,
        int minimum,
        int maximum,
        params string[] columns)
    {
        if (table is null || table.Rows.Count < minimum || table.Rows.Count > maximum)
        {
            throw new LegacySceneDataException("The parameterized scene query returned an invalid row count.");
        }

        if (table.Columns.Count != columns.Length)
        {
            throw new LegacySceneDataException("The parameterized scene query returned an unexpected schema.");
        }

        var available = table.Columns.Cast<DataColumn>()
            .Select(column => column.ColumnName)
            .ToHashSet(StringComparer.OrdinalIgnoreCase);
        if (columns.Any(column => !available.Contains(column)))
        {
            throw new LegacySceneDataException("The parameterized scene query returned an unexpected schema.");
        }

        return table.Rows.Cast<DataRow>().ToArray();
    }

    public static ParameterizedQuoteRow Quote(DataRow row) => new(
        String(row, "TITLE"),
        Decimal(row, "PRICE"),
        Decimal(row, "NET_CHANGE"),
        Decimal(row, "RATE"),
        Direction(row, "DIRECTION"));

    public static string String(DataRow row, string column, bool allowEmpty = false)
    {
        var value = Required(row, column);
        var text = Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty;
        return LegacySceneBuilderGuard.Text(text, column, allowEmpty);
    }

    public static decimal Decimal(DataRow row, string column)
    {
        try
        {
            return Convert.ToDecimal(Required(row, column), CultureInfo.InvariantCulture);
        }
        catch (Exception exception) when (exception is FormatException or InvalidCastException or OverflowException)
        {
            throw new LegacySceneDataException("The parameterized scene query returned an invalid decimal.");
        }
    }

    public static decimal NonNegativeDecimal(DataRow row, string column)
    {
        var value = Decimal(row, column);
        return value >= 0
            ? value
            : throw new LegacySceneDataException("The parameterized scene query returned a negative magnitude.");
    }

    public static long Int64(DataRow row, string column) => CheckedInt64(Decimal(row, column));

    public static long CheckedInt64(decimal value)
    {
        if (decimal.Truncate(value) != value || value is < long.MinValue or > long.MaxValue)
        {
            throw new LegacySceneDataException("The parameterized scene query returned an invalid integer.");
        }

        return decimal.ToInt64(value);
    }

    public static ScenePriceDirection Direction(DataRow row, string column)
    {
        var value = String(row, column, allowEmpty: true).Trim();
        return value switch
        {
            "1" or "상한" => ScenePriceDirection.LimitUp,
            "2" or "상승" or "+" => ScenePriceDirection.Up,
            "" or "3" or "보합" => ScenePriceDirection.Flat,
            "5" or "하락" or "-" => ScenePriceDirection.Down,
            "4" or "하한" => ScenePriceDirection.LimitDown,
            _ => throw new LegacySceneDataException("The parameterized scene query returned an unknown direction.")
        };
    }

    public static S8001SectorSignal SectorSignal(DataRow row, string column) => Direction(row, column) switch
    {
        ScenePriceDirection.LimitUp or ScenePriceDirection.Up => S8001SectorSignal.Positive,
        ScenePriceDirection.Flat => S8001SectorSignal.Flat,
        ScenePriceDirection.Down or ScenePriceDirection.LimitDown => S8001SectorSignal.Negative,
        _ => throw new LegacySceneDataException("The parameterized scene query returned an unknown sector signal.")
    };

    public static string Selector(string? value, string name)
    {
        var text = LegacySceneBuilderGuard.Text(value, name);
        if (text.Length > 128)
        {
            throw new LegacySceneDataException("A parameterized scene selector is too long.");
        }

        return text;
    }

    private static object Required(DataRow row, string column) => row[column] is { } value && value is not DBNull
        ? value
        : throw new LegacySceneDataException("The parameterized scene query returned a required null value.");
}
