using System.Data;
using System.Globalization;
using System.Text;
using MBN_STOCK_WEBVIEW.Infrastructure;
using MBN_STOCK_WEBVIEW.LegacyApplication;
using MMoneyCoderSharp.Data;

internal static class ThemeSelectorCompatibilityAudit
{
    private const string KrxQueryName = "AUDIT_THEME_SELECTOR_RAW_KRX";
    private const string NxtQueryName = "AUDIT_THEME_SELECTOR_RAW_NXT";
    private const string NxtActiveCodesQueryName = "AUDIT_THEME_SELECTOR_ACTIVE_NXT_CODES";

    private const string KrxSql = """
        SELECT s.SB_TITLE THEME_TITLE,
               s.SB_CODE THEME_CODE,
               s.SB_MARKET THEME_MARKET
        FROM SB_LIST s
        WHERE s.SB_MARKET = 'KRX'
        """;

    private const string NxtSql = """
        SELECT s.SB_TITLE THEME_TITLE,
               s.SB_CODE THEME_CODE,
               s.SB_MARKET THEME_MARKET
        FROM SB_LIST s
        WHERE s.SB_MARKET = 'NXT'
        """;

    private const string NxtActiveCodesSql = """
        SELECT DISTINCT i.SB_M_CODE THEME_CODE
        FROM SB_ITEM i
        JOIN (
            SELECT CONCAT('X', s.F_STOCK_CODE) ITEM_CODE
            FROM N_STOCK s
            WHERE s.F_STOP_GUBUN = 'N'
            UNION ALL
            SELECT CONCAT('T', s.F_STOCK_CODE) ITEM_CODE
            FROM N_KOSDAQ_STOCK s
            WHERE s.F_STOP_GUBUN = 'N'
        ) current_nxt
          ON BINARY i.SB_I_CODE = BINARY current_nxt.ITEM_CODE
        """;

    public static async Task RunAsync(
        IDataQueryExecutor executor,
        LegacyThemeSelectionService service,
        ThemeSearchResult selector,
        LegacyThemeWorkflow workflow,
        LegacyThemeWorkflowSnapshot workflowSnapshot,
        CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(executor);
        ArgumentNullException.ThrowIfNull(service);
        ArgumentNullException.ThrowIfNull(selector);
        ArgumentNullException.ThrowIfNull(workflow);
        ArgumentNullException.ThrowIfNull(workflowSnapshot);

        var krx = await LoadRowsAsync(
            executor,
            DataSourceKind.Oracle,
            KrxQueryName,
            KrxSql,
            ThemeMarket.Krx,
            "KRX",
            cancellationToken).ConfigureAwait(false);
        var nxt = await LoadRowsAsync(
            executor,
            DataSourceKind.MariaDb,
            NxtQueryName,
            NxtSql,
            ThemeMarket.Nxt,
            "NXT",
            cancellationToken).ConfigureAwait(false);
        var raw = krx.Concat(nxt).ToArray();

        RequireUniqueCodes(raw);
        var rawKeys = raw.Select(static row => row.Key).ToHashSet();
        var selectorKeys = selector.Items
            .Select(static item => new ThemeKey(
                item.Identity.Market,
                item.Identity.ThemeCode,
                item.Identity.ThemeTitle))
            .ToHashSet();
        if (selector.IsTruncated || rawKeys.Count != raw.Length ||
            selectorKeys.Count != selector.Items.Count ||
            !rawKeys.SetEquals(selectorKeys))
        {
            throw new DatabaseConfigurationException(
                "The Theme selector did not preserve every safe raw SB_LIST identity.");
        }

        if (workflowSnapshot.SearchIsTruncated ||
            workflowSnapshot.SearchResults.Count != selector.Items.Count)
        {
            throw new DatabaseConfigurationException(
                "The Theme operator workflow did not preserve every raw selector identity.");
        }

        var krxEdgeSpace = krx.Count(static row => HasEdgeWhitespace(row.Title));
        var nxtEdgeSpace = nxt.Count(static row => HasEdgeWhitespace(row.Title));
        var duplicateTitleRows = raw
            .GroupBy(static row => (row.Market, row.Title))
            .Where(static group => group.Count() > 1)
            .Sum(static group => group.Count());
        var duplicateTitleGroups = raw
            .GroupBy(static row => (row.Market, row.Title))
            .Count(static group => group.Count() > 1);
        var krxSuffixRows = krx.Count(static row =>
            row.Title.EndsWith("(NXT)", StringComparison.Ordinal));
        if (krxEdgeSpace == 0 || nxtEdgeSpace == 0 ||
            duplicateTitleRows == 0 || krxSuffixRows == 0)
        {
            throw new DatabaseConfigurationException(
                "The deployed Theme compatibility fixtures are missing from SB_LIST.");
        }

        var suffixRow = workflowSnapshot.SearchResults.FirstOrDefault(static row =>
            row.Market == ThemeMarket.Krx &&
            row.DisplayTitle.EndsWith("(NXT)", StringComparison.Ordinal));
        if (suffixRow is null || suffixRow.Market != ThemeMarket.Krx ||
            !suffixRow.DisplayTitle.EndsWith("(NXT)", StringComparison.Ordinal))
        {
            throw new DatabaseConfigurationException(
                "A stored KRX '(NXT)' title lost its explicit KRX identity.");
        }

        var activeNxtCodes = await LoadActiveNxtCodesAsync(
            executor,
            cancellationToken).ConfigureAwait(false);
        var emptyNxt = selector.Items.FirstOrDefault(item =>
            item.Identity.Market == ThemeMarket.Nxt &&
            !activeNxtCodes.Contains(item.Identity.ThemeCode));
        if (emptyNxt is null)
        {
            throw new DatabaseConfigurationException(
                "The deployed empty-active NXT Theme compatibility fixture is missing.");
        }

        var emptyPreview = await service.PreviewAsync(
            emptyNxt.Identity,
            LegacyThemeSelectionService.MaximumPreviewItems,
            cancellationToken).ConfigureAwait(false);
        if (emptyPreview.IsTruncated || emptyPreview.Items.Count != 0 ||
            emptyPreview.Identity != emptyNxt.Identity)
        {
            throw new DatabaseConfigurationException(
                "The empty-active NXT Theme did not remain a correlated empty preview.");
        }

        var emptyIndex = Array.FindIndex(
            selector.Items.ToArray(),
            item => item == emptyNxt);
        if (emptyIndex < 0 || emptyIndex >= workflowSnapshot.SearchResults.Count)
        {
            throw new DatabaseConfigurationException(
                "The empty-active NXT Theme lost its opaque workflow correlation.");
        }
        var emptyWorkflowRow = workflowSnapshot.SearchResults[emptyIndex];
        var selectedEmpty = await workflow.SelectAsync(
            workflowSnapshot,
            emptyWorkflowRow.ResultId,
            cancellationToken).ConfigureAwait(false);
        if (!string.Equals(
                selectedEmpty.SelectedResultId,
                emptyWorkflowRow.ResultId,
                StringComparison.Ordinal) ||
            selectedEmpty.SelectedTheme?.Market != ThemeMarket.Nxt ||
            selectedEmpty.ActionStates.Any(static state =>
                state.IsAvailable ||
                !string.Equals(
                    state.UnavailableReason,
                    "theme-preview-is-empty",
                    StringComparison.Ordinal)))
        {
            throw new DatabaseConfigurationException(
                "An empty-active NXT Theme became eligible for playlist or playout action.");
        }

        Console.WriteLine(
            "THEME_SELECTOR_COMPATIBILITY: " +
            $"raw={raw.Length.ToString(CultureInfo.InvariantCulture)} " +
            $"visible={selector.Items.Count.ToString(CultureInfo.InvariantCulture)} " +
            $"krx={krx.Count.ToString(CultureInfo.InvariantCulture)} " +
            $"nxt={nxt.Count.ToString(CultureInfo.InvariantCulture)} " +
            $"krxEdgeSpace={krxEdgeSpace.ToString(CultureInfo.InvariantCulture)} " +
            $"nxtEdgeSpace={nxtEdgeSpace.ToString(CultureInfo.InvariantCulture)} " +
            $"duplicateTitleGroups={duplicateTitleGroups.ToString(CultureInfo.InvariantCulture)} " +
            $"duplicateTitleRows={duplicateTitleRows.ToString(CultureInfo.InvariantCulture)} " +
            $"krxStoredNxtSuffix={krxSuffixRows.ToString(CultureInfo.InvariantCulture)} " +
            "emptyNxtVisible=true emptyNxtActionsBlocked=true");
    }

    private static async Task<IReadOnlyList<RawThemeRow>> LoadRowsAsync(
        IDataQueryExecutor executor,
        DataSourceKind source,
        string queryName,
        string sql,
        ThemeMarket market,
        string databaseMarket,
        CancellationToken cancellationToken)
    {
        var spec = new DataQuerySpec(sql);
        spec.ValidateFor(source);
        var table = await executor.ExecuteAsync(
            source,
            queryName,
            spec,
            cancellationToken).ConfigureAwait(false);
        if (table is null || table.Columns.Count != 3 ||
            !string.Equals(table.Columns[0].ColumnName, "THEME_TITLE", StringComparison.Ordinal) ||
            !string.Equals(table.Columns[1].ColumnName, "THEME_CODE", StringComparison.Ordinal) ||
            !string.Equals(table.Columns[2].ColumnName, "THEME_MARKET", StringComparison.Ordinal) ||
            table.Columns.Cast<DataColumn>().Any(static column => column.DataType != typeof(string)))
        {
            throw new DatabaseConfigurationException(
                $"The {queryName} result has an invalid schema.");
        }

        var result = new RawThemeRow[table.Rows.Count];
        for (var index = 0; index < table.Rows.Count; index++)
        {
            var row = table.Rows[index];
            var title = RequireStoredTitle(row[0], queryName);
            var code = RequireCode(row[1], queryName);
            if (row[2] is not string actualMarket ||
                !string.Equals(actualMarket, databaseMarket, StringComparison.Ordinal))
            {
                throw new DatabaseConfigurationException(
                    $"The {queryName} result contains an invalid market identity.");
            }

            result[index] = new RawThemeRow(market, code, title);
        }

        return result;
    }

    private static async Task<HashSet<string>> LoadActiveNxtCodesAsync(
        IDataQueryExecutor executor,
        CancellationToken cancellationToken)
    {
        var spec = new DataQuerySpec(NxtActiveCodesSql);
        spec.ValidateFor(DataSourceKind.MariaDb);
        var table = await executor.ExecuteAsync(
            DataSourceKind.MariaDb,
            NxtActiveCodesQueryName,
            spec,
            cancellationToken).ConfigureAwait(false);
        if (table is null || table.Columns.Count != 1 ||
            !string.Equals(table.Columns[0].ColumnName, "THEME_CODE", StringComparison.Ordinal) ||
            table.Columns[0].DataType != typeof(string))
        {
            throw new DatabaseConfigurationException(
                $"The {NxtActiveCodesQueryName} result has an invalid schema.");
        }

        var result = new HashSet<string>(StringComparer.Ordinal);
        foreach (DataRow row in table.Rows)
        {
            if (row[0] is not string code || !result.Add(code))
            {
                throw new DatabaseConfigurationException(
                    $"The {NxtActiveCodesQueryName} result contains an invalid code.");
            }
        }

        return result;
    }

    private static string RequireStoredTitle(object value, string queryName)
    {
        if (value is not string title || title.Length is < 1 or > 128 ||
            string.IsNullOrWhiteSpace(title) || !IsSafeText(title))
        {
            throw new DatabaseConfigurationException(
                $"The {queryName} result contains an unsafe stored title.");
        }

        return title;
    }

    private static string RequireCode(object value, string queryName)
    {
        if (value is not string code || code.Length is < 3 or > 8 ||
            code.Any(static character => !char.IsAsciiDigit(character)))
        {
            throw new DatabaseConfigurationException(
                $"The {queryName} result contains an invalid theme code.");
        }

        return code;
    }

    private static void RequireUniqueCodes(IEnumerable<RawThemeRow> rows)
    {
        if (rows.GroupBy(static row => (row.Market, row.Code))
            .Any(static group => group.Count() != 1))
        {
            throw new DatabaseConfigurationException(
                "The deployed Theme catalog contains an ambiguous market/code identity.");
        }
    }

    private static bool HasEdgeWhitespace(string value) =>
        value.Length > 0 &&
        (char.IsWhiteSpace(value[0]) || char.IsWhiteSpace(value[^1]));

    private static bool IsSafeText(string value)
    {
        foreach (var rune in value.EnumerateRunes())
        {
            var category = Rune.GetUnicodeCategory(rune);
            if (Rune.IsControl(rune) || rune.Value == '\uFFFD' ||
                category is UnicodeCategory.Format or UnicodeCategory.Surrogate or
                    UnicodeCategory.LineSeparator or UnicodeCategory.ParagraphSeparator)
            {
                return false;
            }
        }

        return true;
    }

    private readonly record struct ThemeKey(
        ThemeMarket Market,
        string Code,
        string Title);

    private sealed record RawThemeRow(
        ThemeMarket Market,
        string Code,
        string Title)
    {
        public ThemeKey Key => new(Market, Code, Title);
    }
}
