#nullable enable

using System.Data;
using System.IO;
using MMoneyCoderSharp.Data;

namespace MBN_STOCK_WEBVIEW.Core.Playout.Scenes;

/// <summary>
/// Closed runtime plans for the legacy grid, trader, quote and trusted-manual cuts.
/// They contain typed loader requests only; Web input can never supply SQL, mutations,
/// COM operations or a manual-data path.
/// </summary>
public abstract record LegacyGridMarketSceneLoadRequest(string BuilderKey);

public sealed record LegacyS5023SceneLoadRequest(S5023SceneLoadRequest Request)
    : LegacyGridMarketSceneLoadRequest("s5023");

public sealed record LegacyS5024SceneLoadRequest(S5024SceneLoadRequest Request)
    : LegacyGridMarketSceneLoadRequest("s5024");

public sealed record LegacyS5025SceneLoadRequest(S5025ManualSceneLoadRequest Request)
    : LegacyGridMarketSceneLoadRequest("s5025");

public sealed record LegacyS5037SceneLoadRequest(S5037SceneLoadRequest Request)
    : LegacyGridMarketSceneLoadRequest("s5037");

public sealed record LegacyS5085SceneLoadRequest()
    : LegacyGridMarketSceneLoadRequest("s5085");

public sealed record LegacyS6067SceneLoadRequest()
    : LegacyGridMarketSceneLoadRequest("s6067");

public sealed record LegacyS8003SceneLoadRequest(S8003SceneLoadRequest Request)
    : LegacyGridMarketSceneLoadRequest("s8003");

/// <summary>
/// Mirrors the original MainForm mapping of playlist code/jongmok/forCutInfo/sub to
/// <see cref="LegacySceneSelection.GroupCode"/>, <see cref="LegacySceneSelection.Subject"/>,
/// <see cref="LegacySceneSelection.GraphicType"/> and
/// <see cref="LegacySceneSelection.Subtype"/>. Stock-master resolution remains read-only
/// and parameterized. The optional hidden <see cref="LegacySceneSelection.DataCode"/> is
/// only an integrity check and is never used as SQL or as a path.
/// </summary>
public sealed class LegacyGridMarketSceneRequestResolver
{
    private static readonly string[] StockCodeColumns = ["STOCK_CODE"];

    private readonly IDataQueryExecutor _executor;
    private readonly IS5025TrustedManualDataSource? _trustedManualSource;

    public LegacyGridMarketSceneRequestResolver(
        IDataQueryExecutor executor,
        IS5025TrustedManualDataSource? trustedManualSource = null)
    {
        _executor = executor ?? throw new ArgumentNullException(nameof(executor));
        _trustedManualSource = trustedManualSource;
    }

    public async Task<LegacyGridMarketSceneLoadRequest> ResolveAsync(
        LegacyPlaylistEntry entry,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(entry);
        return entry.CutCode switch
        {
            "5023" => new LegacyS5023SceneLoadRequest(CreateS5023(entry)),
            "5024" => new LegacyS5024SceneLoadRequest(CreateS5024(entry)),
            "5025" => new LegacyS5025SceneLoadRequest(CreateS5025(entry)),
            "5037" => new LegacyS5037SceneLoadRequest(CreateS5037(entry)),
            "5085" => CreateS5085(entry),
            "6067" => CreateS6067(entry),
            "8003" => new LegacyS8003SceneLoadRequest(
                await ResolveS8003Async(entry, cancellationToken).ConfigureAwait(false)),
            _ => throw new LegacySceneDataException(
                "The selected cut is not supported by the grid-market resolver.")
        };
    }

    /// <summary>
    /// Required runtime path for s5025. The trusted source is supplied by native
    /// composition/configuration, never by LegacySceneSelection. Reading and validating
    /// all five rows completes before any cue reaches DB or COM execution.
    /// </summary>
    public async Task<S5025SceneData> LoadTrustedS5025Async(
        LegacyPlaylistEntry entry,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(entry);
        var request = CreateS5025(entry);
        var source = _trustedManualSource ?? throw new LegacySceneDataException(
            "Trusted manual scene data is not configured.");
        try
        {
            return await new S5025SceneDataLoader(source)
                .LoadAsync(request, cancellationToken)
                .ConfigureAwait(false);
        }
        catch (LegacySceneDataException)
        {
            throw;
        }
        catch (Exception exception) when (exception is IOException or
                                                 UnauthorizedAccessException or
                                                 InvalidOperationException)
        {
            throw new LegacySceneDataException("Trusted manual scene data is unavailable.");
        }
    }

    private static S5023SceneLoadRequest CreateS5023(LegacyPlaylistEntry entry)
    {
        var selection = RequireSelection(entry);
        RequireIndexSubject(selection.Subject);
        RequireSubtype(selection.Subtype, SceneGraphicSubtype.Table);
        RequireNoDataCode(selection.DataCode);
        var market = ParseTradingTrendMarket(selection.GraphicType);
        RequireSameMarket(selection.GroupCode, market);
        return new S5023SceneLoadRequest(market);
    }

    private static S5024SceneLoadRequest CreateS5024(LegacyPlaylistEntry entry)
    {
        var selection = RequireSelection(entry);
        RequireIndexSubject(selection.Subject);
        RequireSubtype(selection.Subtype, SceneGraphicSubtype.Bar);
        RequireNoDataCode(selection.DataCode);
        var market = ParseTradingTrendMarket(selection.GraphicType);
        RequireSameMarket(selection.GroupCode, market);
        return new S5024SceneLoadRequest(market);
    }

    private S5025ManualSceneLoadRequest CreateS5025(LegacyPlaylistEntry entry)
    {
        var selection = RequireSelection(entry);
        RequireIndexSubject(selection.Subject);
        RequireManualNetSellGraphic(selection.GraphicType);
        RequireManualNetSellGraphic(selection.Subtype);
        RequireNoDataCode(selection.DataCode);
        if (_trustedManualSource is null)
        {
            throw new LegacySceneDataException("Trusted manual scene data is not configured.");
        }

        var audience = RequireText(selection.GroupCode) switch
        {
            "개인 순매도 상위(수동)" or "INDIVIDUAL" => S5025ManualAudience.Individual,
            "외국인 순매도 상위(수동)" or "FOREIGN" => S5025ManualAudience.Foreign,
            "기관 순매도 상위(수동)" or "INSTITUTION" => S5025ManualAudience.Institution,
            _ => throw InvalidSelection()
        };
        return new S5025ManualSceneLoadRequest(audience);
    }

    private static S5037SceneLoadRequest CreateS5037(LegacyPlaylistEntry entry)
    {
        var selection = RequireSelection(entry);
        RequireGraphic(selection.GraphicType, "거래원", "TRADER");
        RequireSubtype(selection.Subtype, SceneGraphicSubtype.Table);
        RequireOptionalStockCode(selection.DataCode);
        return new S5037SceneLoadRequest(
            ParseMarket(selection.GroupCode),
            RequireSelector(selection.Subject));
    }

    private static LegacyS5085SceneLoadRequest CreateS5085(LegacyPlaylistEntry entry)
    {
        var selection = RequireSelection(entry);
        RequireAllGroup(selection.GroupCode);
        RequireIndexSubject(selection.Subject);
        RequireGraphic(selection.GraphicType, "프로그램 매매", "PROGRAM_TRADING");
        RequireSubtype(selection.Subtype, SceneGraphicSubtype.Table);
        RequireNoDataCode(selection.DataCode);
        return new LegacyS5085SceneLoadRequest();
    }

    private static LegacyS6067SceneLoadRequest CreateS6067(LegacyPlaylistEntry entry)
    {
        var selection = RequireSelection(entry);
        RequireAllGroup(selection.GroupCode);
        RequireIndexSubject(selection.Subject);
        RequireGraphic(selection.GraphicType, "기관 순매수 현황", "INSTITUTION_NET_BUY");
        RequireSubtype(selection.Subtype, SceneGraphicSubtype.Table);
        RequireNoDataCode(selection.DataCode);
        return new LegacyS6067SceneLoadRequest();
    }

    private async Task<S8003SceneLoadRequest> ResolveS8003Async(
        LegacyPlaylistEntry entry,
        CancellationToken cancellationToken)
    {
        var selection = RequireSelection(entry);
        var expectedMarket = ParseMarket(selection.GroupCode);
        var stockName = RequireSelector(selection.Subject);
        RequireGraphic(selection.GraphicType, "호가창", "ORDER_BOOK");
        RequireSubtype(selection.Subtype, SceneGraphicSubtype.Table);
        var expectedCode = RequireOptionalStockCode(selection.DataCode);

        var kospi = await ResolveStockAsync(
            LegacyDomesticMarket.Kospi,
            stockName,
            cancellationToken).ConfigureAwait(false);
        var kosdaq = await ResolveStockAsync(
            LegacyDomesticMarket.Kosdaq,
            stockName,
            cancellationToken).ConfigureAwait(false);

        // MainForm iterated KOSPI first and KOSDAQ second, so the latter won when
        // an identical display name existed in both cached stock-master tables.
        var resolved = kosdaq ?? kospi ?? throw new LegacySceneDataException(
            "The order-book stock selection was not found.");
        if (resolved.Market != expectedMarket ||
            (expectedCode.Length > 0 &&
             !string.Equals(expectedCode, resolved.StockCode, StringComparison.Ordinal)))
        {
            throw InvalidSelection();
        }

        return new S8003SceneLoadRequest(resolved.Market, resolved.StockCode);
    }

    private async Task<ResolvedStock?> ResolveStockAsync(
        LegacyDomesticMarket market,
        string stockName,
        CancellationToken cancellationToken)
    {
        var (tableName, table) = market switch
        {
            LegacyDomesticMarket.Kospi => ("SCENE_RESOLVE_8003_KOSPI", "T_STOCK"),
            LegacyDomesticMarket.Kosdaq => ("SCENE_RESOLVE_8003_KOSDAQ", "T_KOSDAQ_STOCK"),
            _ => throw InvalidSelection()
        };
        var spec = LegacyTabularSceneLoaderReader.Spec(
            DataSourceKind.Oracle,
            $"""
            SELECT f_stock_code STOCK_CODE
            FROM {table}
            WHERE f_mkt_halt = 'N'
              AND f_stock_wanname = :stockName
            """,
            new DataQueryParameter("stockName", stockName, DbType.String));
        var result = await _executor.ExecuteAsync(
            DataSourceKind.Oracle,
            tableName,
            spec,
            cancellationToken).ConfigureAwait(false);
        if (result is null || result.Columns.Count != StockCodeColumns.Length ||
            !string.Equals(
                result.Columns[0].ColumnName,
                StockCodeColumns[0],
                StringComparison.OrdinalIgnoreCase) ||
            result.Rows.Count > 1)
        {
            throw new LegacySceneDataException(
                "An order-book stock-master lookup returned an invalid result.");
        }

        if (result.Rows.Count == 0)
        {
            return null;
        }

        if (result.Rows[0][0] is null or DBNull)
        {
            throw new LegacySceneDataException(
                "An order-book stock-master lookup returned an invalid result.");
        }

        var code = RequireStockCode(
            Convert.ToString(result.Rows[0][0], System.Globalization.CultureInfo.InvariantCulture));
        return new ResolvedStock(market, code);
    }

    private static LegacySceneSelection RequireSelection(LegacyPlaylistEntry entry) =>
        entry.Selection ?? throw new LegacySceneDataException(
            "The selected cut requires native lookup information.");

    private static LegacyDomesticMarket ParseTradingTrendMarket(string value) =>
        RequireText(value) switch
        {
            "코스피 매매동향" or "KOSPI_TRADING_TREND" => LegacyDomesticMarket.Kospi,
            "코스닥 매매동향" or "KOSDAQ_TRADING_TREND" => LegacyDomesticMarket.Kosdaq,
            _ => throw InvalidSelection()
        };

    private static LegacyDomesticMarket ParseMarket(string value) =>
        RequireText(value) switch
        {
            "코스피" or "KOSPI" => LegacyDomesticMarket.Kospi,
            "코스닥" or "KOSDAQ" => LegacyDomesticMarket.Kosdaq,
            _ => throw InvalidSelection()
        };

    private static void RequireSameMarket(string value, LegacyDomesticMarket expected)
    {
        if (ParseMarket(value) != expected)
        {
            throw InvalidSelection();
        }
    }

    private static void RequireIndexSubject(string value)
    {
        if (RequireText(value) is not ("지수" or "INDEX"))
        {
            throw InvalidSelection();
        }
    }

    private static void RequireAllGroup(string value)
    {
        if (RequireText(value) is not ("전체" or "ALL"))
        {
            throw InvalidSelection();
        }
    }

    private static void RequireSubtype(string value, SceneGraphicSubtype subtype)
    {
        var text = RequireText(value);
        var valid = subtype switch
        {
            SceneGraphicSubtype.Table => text is "표그래프" or "TABLE_GRAPH",
            SceneGraphicSubtype.Bar => text is "막대그래프" or "BAR_GRAPH",
            _ => false
        };
        if (!valid)
        {
            throw InvalidSelection();
        }
    }

    private static void RequireManualNetSellGraphic(string value)
    {
        if (RequireText(value) is not ("순매도 상위" or "MANUAL_NET_SELL"))
        {
            throw InvalidSelection();
        }
    }

    private static void RequireGraphic(string value, string korean, string stableCode)
    {
        var text = RequireText(value);
        if (!string.Equals(text, korean, StringComparison.Ordinal) &&
            !string.Equals(text, stableCode, StringComparison.Ordinal))
        {
            throw InvalidSelection();
        }
    }

    private static string RequireSelector(string value)
    {
        var text = RequireText(value);
        if (text.Length > 128)
        {
            throw InvalidSelection();
        }

        return text;
    }

    private static string RequireText(string? value)
    {
        if (string.IsNullOrWhiteSpace(value) || value.Length > 256 || value.Any(char.IsControl))
        {
            throw InvalidSelection();
        }

        return value.Trim();
    }

    private static void RequireNoDataCode(string? value)
    {
        if (value is null || value.Length != 0)
        {
            throw InvalidSelection();
        }
    }

    private static string RequireOptionalStockCode(string? value) =>
        value is { Length: 0 } ? string.Empty : RequireStockCode(value);

    private static string RequireStockCode(string? value)
    {
        if (string.IsNullOrWhiteSpace(value) || value.Length > 32 ||
            value.Any(character => !char.IsAsciiLetterOrDigit(character)))
        {
            throw InvalidSelection();
        }

        return value;
    }

    private static LegacySceneDataException InvalidSelection() =>
        new("The selected scene lookup fields are invalid.");

    private sealed record ResolvedStock(
        LegacyDomesticMarket Market,
        string StockCode);

    private enum SceneGraphicSubtype
    {
        Table,
        Bar
    }
}
