#nullable enable

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

namespace MBN_STOCK_WEBVIEW.Core.Playout.Scenes;

/// <summary>
/// Closed result for MainForm's s5026/s5029/s5086/s50860/s5087 dispatch.
/// </summary>
public abstract record LegacyComparisonAndYieldSceneLoadRequest(string BuilderKey);

public sealed record LegacyS5026SceneLoadRequest(ComparisonPairSceneLoadRequest Request)
    : LegacyComparisonAndYieldSceneLoadRequest("s5026");

public sealed record LegacyS5029SceneLoadRequest(ComparisonPairSceneLoadRequest Request)
    : LegacyComparisonAndYieldSceneLoadRequest("s5029");

public sealed record LegacyS5086SceneLoadRequest(YieldSceneLoadRequest Request)
    : LegacyComparisonAndYieldSceneLoadRequest("s5086");

public sealed record LegacyS50860SceneLoadRequest(YieldSceneLoadRequest Request)
    : LegacyComparisonAndYieldSceneLoadRequest("s50860");

public sealed record LegacyS5087SceneLoadRequest(ComparisonPairSceneLoadRequest Request)
    : LegacyComparisonAndYieldSceneLoadRequest("s5087");

/// <summary>
/// Converts the legacy playlist fields to the closed comparison/yield loader requests.
/// LegacySceneSelection maps to MainForm as GroupCode=code, Subject=jongmok,
/// GraphicType=forCutInfo, Subtype=sub and DataCode=dataCode. The five source
/// constructors use only code, jongmok and sub.
/// </summary>
public sealed class ComparisonAndYieldLegacyRequestResolver
{
    private readonly IDataQueryExecutor _executor;

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

    public async Task<LegacyComparisonAndYieldSceneLoadRequest> ResolveAsync(
        LegacyPlaylistEntry entry,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(entry);
        if (entry.CutCode is not ("5026" or "5029" or "5086" or "50860" or "5087"))
        {
            throw InvalidSelection();
        }

        var selection = entry.Selection ?? throw InvalidSelection();
        var subject = Required(selection.Subject);

        return entry.CutCode switch
        {
            "5026" => new LegacyS5026SceneLoadRequest(
                await ResolvePairAsync(
                    selection,
                    ComparisonGraphPeriod.FiveDays,
                    cancellationToken).ConfigureAwait(false)),
            "5029" => new LegacyS5029SceneLoadRequest(
                await ResolvePairAsync(
                    selection,
                    ParseComparisonPeriod(selection.Subtype),
                    cancellationToken).ConfigureAwait(false)),
            "5087" => new LegacyS5087SceneLoadRequest(
                await ResolvePairAsync(
                    selection,
                    ComparisonGraphPeriod.FiveDays,
                    cancellationToken).ConfigureAwait(false)),
            "5086" => new LegacyS5086SceneLoadRequest(
                await ResolveYieldAsync(
                    selection,
                    subject,
                    requireQuotedFx: false,
                    cancellationToken).ConfigureAwait(false)),
            "50860" => new LegacyS50860SceneLoadRequest(
                await ResolveYieldAsync(
                    selection,
                    subject,
                    requireQuotedFx: true,
                    cancellationToken).ConfigureAwait(false)),
            _ => throw InvalidSelection()
        };
    }

    private async Task<ComparisonPairSceneLoadRequest> ResolvePairAsync(
        LegacySceneSelection selection,
        ComparisonGraphPeriod period,
        CancellationToken cancellationToken)
    {
        var stockNames = SplitPair(Required(selection.Subject));
        var exactIdentities = ParseExactPairIdentities(selection.DataCode);
        var first = await ResolveExactStockAsync(
            stockNames[0], exactIdentities[0], cancellationToken).ConfigureAwait(false);
        var second = await ResolveExactStockAsync(
            stockNames[1], exactIdentities[1], cancellationToken).ConfigureAwait(false);
        return new ComparisonPairSceneLoadRequest(period, first, second);
    }

    private async Task<ComparisonStockSelection> ResolveExactStockAsync(
        string rawName,
        ExactStockIdentity identity,
        CancellationToken cancellationToken)
    {
        var stockName = Required(rawName);
        var tableName = identity.Market == ComparisonEquityMarket.Kospi
            ? "T_STOCK"
            : "T_KOSDAQ_STOCK";
        var queryName = identity.Market == ComparisonEquityMarket.Kospi
            ? "SCENE_COMPARISON_RESOLVE_EXACT_KOSPI_STOCK"
            : "SCENE_COMPARISON_RESOLVE_EXACT_KOSDAQ_STOCK";
        var spec = new DataQuerySpec(
            $"""
            SELECT STOCK_NAME, STOCK_CODE
            FROM (
                SELECT F_STOCK_WANNAME STOCK_NAME, F_STOCK_CODE STOCK_CODE
                FROM {tableName}
                WHERE F_MKT_HALT = 'N'
                  AND F_STOCK_WANNAME = :stockName
                  AND F_STOCK_CODE = :stockCode
                ORDER BY F_STOCK_CODE
            )
            WHERE ROWNUM <= 2
            """,
            [
                new DataQueryParameter("stockName", stockName, DbType.String),
                new DataQueryParameter("stockCode", identity.StockCode, DbType.String)
            ]);
        var table = await _executor.ExecuteAsync(
            DataSourceKind.Oracle,
            queryName,
            spec,
            cancellationToken).ConfigureAwait(false);
        if (table is null || table.Columns.Count != 2 || table.Rows.Count > 2 ||
            !string.Equals(table.Columns[0].ColumnName, "STOCK_NAME", StringComparison.Ordinal) ||
            table.Columns[0].DataType != typeof(string) ||
            !string.Equals(table.Columns[1].ColumnName, "STOCK_CODE", StringComparison.Ordinal) ||
            table.Columns[1].DataType != typeof(string))
        {
            throw InvalidResult();
        }

        if (table.Rows.Count != 1)
        {
            throw InvalidSelection();
        }

        if (table.Rows[0][0] is not string actualName ||
            table.Rows[0][1] is not string actualCode ||
            !string.Equals(actualName, stockName, StringComparison.Ordinal) ||
            !string.Equals(actualCode, identity.StockCode, StringComparison.Ordinal))
        {
            throw InvalidResult();
        }

        return new ComparisonStockSelection(identity.Market, actualName, actualCode);
    }

    private async Task<YieldSceneLoadRequest> ResolveYieldAsync(
        LegacySceneSelection selection,
        string subject,
        bool requireQuotedFx,
        CancellationToken cancellationToken)
    {
        var period = ParseYieldPeriod(selection.Subtype);
        var fxKind = TryParseFx(subject);
        if (fxKind.HasValue)
        {
            return new YieldSceneLoadRequest(
                period,
                new YieldGraphInstrument(fxKind.Value));
        }

        // The source s50860 reads quote cells which exist only in the FX result shape.
        // Reject every other legacy branch before performing a stock-master lookup.
        if (requireQuotedFx)
        {
            throw InvalidSelection();
        }

        var groupCode = selection.GroupCode?.Trim() ?? string.Empty;
        if (subject is "지수" or "INDEX")
        {
            var index = groupCode switch
            {
                "코스피" or "KOSPI" or "KOSPI_INDEX" =>
                    YieldGraphInstrumentKind.KospiIndex,
                "코스닥" or "KOSDAQ" or "KOSDAQ_INDEX" =>
                    YieldGraphInstrumentKind.KosdaqIndex,
                "코스피200" or "KOSPI200" or "KOSPI200_INDEX" =>
                    YieldGraphInstrumentKind.Kospi200Index,
                "KRX100" or "KRX100_INDEX" => YieldGraphInstrumentKind.Krx100Index,
                _ => throw InvalidSelection()
            };
            return new YieldSceneLoadRequest(period, new YieldGraphInstrument(index));
        }

        if (groupCode.Contains("업종", StringComparison.Ordinal) ||
            groupCode.StartsWith("INDUSTRY_", StringComparison.Ordinal))
        {
            var industry = groupCode.Contains("코스피", StringComparison.Ordinal) ||
                           groupCode.Contains("KOSPI", StringComparison.Ordinal)
                ? YieldGraphInstrumentKind.KospiIndustry
                : YieldGraphInstrumentKind.KosdaqIndustry;
            return new YieldSceneLoadRequest(
                period,
                new YieldGraphInstrument(industry, subject));
        }

        var stockMarket = await ResolveStockMarketAsync(subject, cancellationToken).ConfigureAwait(false);
        var stockKind = stockMarket switch
        {
            ComparisonEquityMarket.Kospi => YieldGraphInstrumentKind.KospiStock,
            ComparisonEquityMarket.Kosdaq => YieldGraphInstrumentKind.KosdaqStock,
            _ => throw InvalidSelection()
        };
        return new YieldSceneLoadRequest(
            period,
            new YieldGraphInstrument(stockKind, subject));
    }

    private async Task<ComparisonEquityMarket> ResolveStockMarketAsync(
        string rawName,
        CancellationToken cancellationToken)
    {
        var stockName = Required(rawName);
        ComparisonEquityMarket? market = null;
        if (await StockExistsAsync(
                ComparisonEquityMarket.Kospi,
                stockName,
                cancellationToken).ConfigureAwait(false))
        {
            market = ComparisonEquityMarket.Kospi;
        }

        // MainForm scans the KOSDAQ cache after the KOSPI cache, so a later
        // KOSDAQ match intentionally takes precedence for an overlapping name.
        if (await StockExistsAsync(
                ComparisonEquityMarket.Kosdaq,
                stockName,
                cancellationToken).ConfigureAwait(false))
        {
            market = ComparisonEquityMarket.Kosdaq;
        }

        return market ?? throw InvalidSelection();
    }

    private async Task<bool> StockExistsAsync(
        ComparisonEquityMarket market,
        string stockName,
        CancellationToken cancellationToken)
    {
        var query = StockMasterQuery(market, stockName);
        var table = await _executor.ExecuteAsync(
            DataSourceKind.Oracle,
            query.TableName,
            query.Spec,
            cancellationToken).ConfigureAwait(false);
        if (table is null || table.Columns.Count != 1 || table.Rows.Count != 1 ||
            !string.Equals(
                table.Columns[0].ColumnName,
                "MATCH_COUNT",
                StringComparison.OrdinalIgnoreCase) ||
            table.Rows[0][0] is null or DBNull)
        {
            throw InvalidResult();
        }

        decimal count;
        try
        {
            count = Convert.ToDecimal(table.Rows[0][0], CultureInfo.InvariantCulture);
        }
        catch (Exception exception) when (exception is
            FormatException or InvalidCastException or OverflowException)
        {
            _ = exception;
            throw InvalidResult();
        }

        if (count != decimal.Truncate(count))
        {
            throw InvalidResult();
        }

        return count switch
        {
            0m => false,
            1m => true,
            _ => throw InvalidResult()
        };
    }

    private static StockMasterLookup StockMasterQuery(
        ComparisonEquityMarket market,
        string stockName)
    {
        var parameter = new DataQueryParameter("stockName", stockName, DbType.String);
        return market switch
        {
            ComparisonEquityMarket.Kospi => new StockMasterLookup(
                "SCENE_COMPARISON_RESOLVE_KOSPI_STOCK",
                new DataQuerySpec(
                    "SELECT COUNT(*) MATCH_COUNT FROM T_STOCK " +
                    "WHERE F_MKT_HALT = 'N' AND F_STOCK_WANNAME = :stockName",
                    [parameter])),
            ComparisonEquityMarket.Kosdaq => new StockMasterLookup(
                "SCENE_COMPARISON_RESOLVE_KOSDAQ_STOCK",
                new DataQuerySpec(
                    "SELECT COUNT(*) MATCH_COUNT FROM T_KOSDAQ_STOCK " +
                    "WHERE F_MKT_HALT = 'N' AND F_STOCK_WANNAME = :stockName",
                    [parameter])),
            _ => throw InvalidSelection()
        };
    }

    private static ComparisonGraphPeriod ParseComparisonPeriod(string value)
    {
        var required = Required(value);
        if (Enum.TryParse<ComparisonGraphPeriod>(required, ignoreCase: false, out var canonical) &&
            Enum.IsDefined(canonical))
        {
            return canonical;
        }

        return required switch
        {
            "일봉" => ComparisonGraphPeriod.Daily,
            "5일" => ComparisonGraphPeriod.FiveDays,
            "1개월" => ComparisonGraphPeriod.OneMonth,
            "3개월" => ComparisonGraphPeriod.ThreeMonths,
            "6개월" => ComparisonGraphPeriod.SixMonths,
            "12개월" => ComparisonGraphPeriod.TwelveMonths,
            _ => throw InvalidSelection()
        };
    }

    private static YieldGraphPeriod ParseYieldPeriod(string value)
    {
        var required = Required(value);
        if (Enum.TryParse<YieldGraphPeriod>(required, ignoreCase: false, out var canonical) &&
            Enum.IsDefined(canonical))
        {
            return canonical;
        }

        return required switch
        {
            "5일" => YieldGraphPeriod.FiveDays,
            "20일" => YieldGraphPeriod.TwentyDays,
            "60일" => YieldGraphPeriod.SixtyDays,
            "120일" => YieldGraphPeriod.OneHundredTwentyDays,
            "240일" => YieldGraphPeriod.TwoHundredFortyDays,
            _ => throw InvalidSelection()
        };
    }

    private static YieldGraphInstrumentKind? TryParseFx(string subject)
    {
        var canonical = subject switch
        {
            nameof(YieldGraphInstrumentKind.WonDollar) => YieldGraphInstrumentKind.WonDollar,
            nameof(YieldGraphInstrumentKind.WonYen) => YieldGraphInstrumentKind.WonYen,
            nameof(YieldGraphInstrumentKind.WonYuan) => YieldGraphInstrumentKind.WonYuan,
            nameof(YieldGraphInstrumentKind.WonEuro) => YieldGraphInstrumentKind.WonEuro,
            _ => (YieldGraphInstrumentKind?)null
        };
        if (canonical.HasValue)
        {
            return canonical;
        }

        if (subject.Contains("원달러", StringComparison.Ordinal))
        {
            return YieldGraphInstrumentKind.WonDollar;
        }

        if (subject.Contains("원엔", StringComparison.Ordinal))
        {
            return YieldGraphInstrumentKind.WonYen;
        }

        if (subject.Contains("원위엔", StringComparison.Ordinal))
        {
            return YieldGraphInstrumentKind.WonYuan;
        }

        if (subject.Contains("원유로", StringComparison.Ordinal))
        {
            return YieldGraphInstrumentKind.WonEuro;
        }

        return null;
    }

    private static string[] SplitPair(string subject)
    {
        var values = subject.Split(',', StringSplitOptions.TrimEntries);
        if (values.Length != 2 || values.Any(string.IsNullOrWhiteSpace))
        {
            throw InvalidSelection();
        }

        return values;
    }

    private static ExactStockIdentity[] ParseExactPairIdentities(string? value)
    {
        if (string.IsNullOrEmpty(value))
        {
            throw InvalidSelection();
        }

        var parts = value.Split('|', StringSplitOptions.None);
        if (parts.Length != 2)
        {
            throw InvalidSelection();
        }

        return [ParseExactStockIdentity(parts[0]), ParseExactStockIdentity(parts[1])];
    }

    private static ExactStockIdentity ParseExactStockIdentity(string value)
    {
        var separator = value.IndexOf(':');
        if (separator <= 0 || separator != value.LastIndexOf(':') ||
            separator == value.Length - 1)
        {
            throw InvalidSelection();
        }

        var market = value[..separator] switch
        {
            "KOSPI" => ComparisonEquityMarket.Kospi,
            "KOSDAQ" => ComparisonEquityMarket.Kosdaq,
            _ => throw InvalidSelection()
        };
        var stockCode = value[(separator + 1)..];
        if (stockCode.Length is < 1 or > 32 ||
            stockCode.Any(static character =>
                !(char.IsAsciiLetterOrDigit(character) || character is '.' or '_' or '-')))
        {
            throw InvalidSelection();
        }

        return new ExactStockIdentity(market, stockCode);
    }

    private static string Required(string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            throw InvalidSelection();
        }

        return value.Trim();
    }

    private static LegacySceneDataException InvalidSelection() =>
        new("The comparison or yield legacy selection is unsupported.");

    private static LegacySceneDataException InvalidResult() =>
        new("A comparison stock-master lookup returned an invalid result.");

    private sealed record StockMasterLookup(string TableName, DataQuerySpec Spec);

    private sealed record ExactStockIdentity(
        ComparisonEquityMarket Market,
        string StockCode);
}
