using System.Data;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using MBN_STOCK_WEBVIEW.Infrastructure;
using MMoneyCoderSharp.Data;

internal static class NxtThemeRestoreDbAudit
{
    private const string ActiveRowsQueryName = "AUDIT_ACTIVE_NXT_THEME_ROWS";

    private const string ActiveRowsSql = """
        SELECT LIST_TEXT
        FROM (
            SELECT p.LIST_TEXT
            FROM DC_LIST d
            JOIN PLAY_LIST p ON p.PG_CODE = d.DC_CODE
            WHERE p.LIST_TEXT LIKE '1^테마_NXT^%'
            ORDER BY d.DC_CODE, TO_NUMBER(p.ITEM_INDEX)
        )
        WHERE ROWNUM <= :row_limit
        """;

    internal static async Task RunAsync(
        IDataQueryExecutor executor,
        MariaDbDatabaseOptions currentOptions,
        MariaDbEndpointReference referenceOptions,
        CancellationToken cancellationToken)
    {
        await RunEndpointAuditAsync(
                executor,
                currentOptions,
                referenceOptions,
                cancellationToken)
            .ConfigureAwait(false);

        const int maximumRows = 1_000;
        var spec = new DataQuerySpec(
            ActiveRowsSql,
            [new DataQueryParameter("row_limit", maximumRows + 1, DbType.Int32)]);
        spec.ValidateFor(DataSourceKind.Oracle);
        var table = await executor.ExecuteAsync(
            DataSourceKind.Oracle,
            ActiveRowsQueryName,
            spec,
            cancellationToken);
        cancellationToken.ThrowIfCancellationRequested();

        if (table.Columns.Count != 1 ||
            table.Columns[0].ColumnName != "LIST_TEXT" ||
            table.Columns[0].DataType != typeof(string) ||
            table.Rows.Count > maximumRows)
        {
            throw new InvalidDataException("The active NXT-theme audit boundary is invalid.");
        }

        var rows = new List<LegacyNamedNxtThemeRestoreRow>(table.Rows.Count);
        var grammar = new Dictionary<string, int>(StringComparer.Ordinal);
        foreach (DataRow dataRow in table.Rows)
        {
            if (dataRow[0] is not string text)
            {
                throw new InvalidDataException("An active NXT-theme row is not text.");
            }

            var fields = text.Split('^');
            if (fields.Length != 7 || fields[0] != "1" || fields[1] != "테마_NXT")
            {
                throw new InvalidDataException("An active NXT-theme row has an invalid shape.");
            }

            var pageText = fields[5].Replace(" ", string.Empty, StringComparison.Ordinal);
            rows.Add(new LegacyNamedNxtThemeRestoreRow(
                rows.Count,
                IsEnabled: true,
                fields[1],
                fields[2],
                fields[3],
                fields[4],
                pageText,
                fields[6]));
            var key = string.Join('|', fields[3], fields[4]);
            grammar[key] = grammar.GetValueOrDefault(key) + 1;
        }

        if (rows.Count == 0)
        {
            Console.WriteLine("NXT_THEME_RESTORE_AUDIT: active=0");
            return;
        }

        var result = await new LegacyNamedNxtThemeRestoreService(executor)
            .ResolveAsync(rows, cancellationToken);
        var resolved = result.Rows.Count(row => row.IsResolved);
        var identityMapped = result.Rows.Count(row => row.ObservedCurrentThemeCode is not null);
        var remapped = result.Rows
            .Select((row, index) =>
                row.ObservedCurrentThemeCode is not null &&
                !string.Equals(
                    row.ObservedCurrentThemeCode,
                    rows[index].DataCode,
                    StringComparison.Ordinal))
            .Count(static value => value);
        var failures = result.Rows
            .Where(row => row.Failure.HasValue)
            .GroupBy(row => row.Failure!.Value)
            .OrderBy(group => group.Key)
            .Select(group => $"{group.Key}={group.Count().ToString(CultureInfo.InvariantCulture)}")
            .ToArray();
        var actions = result.Rows
            .Where(row => row.IsResolved)
            .GroupBy(row => row.Identity!.ActionId)
            .OrderBy(group => group.Key, StringComparer.Ordinal)
            .Select(group => $"{group.Key}={group.Count().ToString(CultureInfo.InvariantCulture)}")
            .ToArray();
        var liveCounts = result.Rows
            .Where(row => row.IsResolved)
            .Select(row => row.Identity!.LiveItemCount)
            .ToArray();

        Console.WriteLine(
            "NXT_THEME_RESTORE_AUDIT: " +
            $"active={rows.Count.ToString(CultureInfo.InvariantCulture)} " +
            $"distinctTitles={rows.Select(row => row.Subject).Distinct(StringComparer.Ordinal).Count().ToString(CultureInfo.InvariantCulture)} " +
            $"grammarVariants={grammar.Count.ToString(CultureInfo.InvariantCulture)} " +
            $"identityMapped={identityMapped.ToString(CultureInfo.InvariantCulture)} " +
            $"resolved={resolved.ToString(CultureInfo.InvariantCulture)} " +
            $"remapped={remapped.ToString(CultureInfo.InvariantCulture)} " +
            $"blocked={(rows.Count - resolved).ToString(CultureInfo.InvariantCulture)} " +
            $"session={LegacyNamedNxtThemeRestoreService.SessionForLocalHour(DateTime.Now.Hour)}");
        Console.WriteLine(
            "NXT_THEME_RESTORE_GRAMMAR: " +
            $"variants={grammar.Count.ToString(CultureInfo.InvariantCulture)} " +
            $"rows={grammar.Values.Sum().ToString(CultureInfo.InvariantCulture)} " +
            "rawValuesPrinted=false");
        Console.WriteLine("NXT_THEME_RESTORE_ACTIONS: " +
            (actions.Length == 0 ? "none" : string.Join(",", actions)));
        Console.WriteLine("NXT_THEME_RESTORE_FAILURES: " +
            (failures.Length == 0 ? "none" : string.Join(",", failures)));
        Console.WriteLine("NXT_THEME_RESTORE_LIVE_ITEMS: " +
            (liveCounts.Length == 0
                ? "none"
                : $"min={liveCounts.Min().ToString(CultureInfo.InvariantCulture)}," +
                  $"max={liveCounts.Max().ToString(CultureInfo.InvariantCulture)}," +
                  $"sum={liveCounts.Sum().ToString(CultureInfo.InvariantCulture)}"));

        await RunJoinDiagnosticAsync(executor, rows, result, cancellationToken)
            .ConfigureAwait(false);
    }

    private static async Task RunJoinDiagnosticAsync(
        IDataQueryExecutor executor,
        IReadOnlyList<LegacyNamedNxtThemeRestoreRow> sourceRows,
        LegacyNamedNxtThemeRestoreResult restore,
        CancellationToken cancellationToken)
    {
        var codes = new Dictionary<string, string>(StringComparer.Ordinal);
        for (var index = 0; index < restore.Rows.Count; index++)
        {
            var current = restore.Rows[index].ObservedCurrentThemeCode;
            if (current is null)
            {
                continue;
            }

            var stored = sourceRows[index].DataCode;
            if (codes.TryGetValue(current, out var previous) &&
                !string.Equals(previous, stored, StringComparison.Ordinal))
            {
                throw new InvalidDataException(
                    "The NXT join audit found conflicting stored codes for one current identity.");
            }

            codes[current] = stored;
        }

        if (codes.Count == 0)
        {
            Console.WriteLine("NXT_THEME_JOIN_DIAGNOSTIC: currentThemes=0");
            return;
        }

        // Sort only to make repeated aggregate runs deterministic. Neither key
        // is printed or included in an exception.
        var identities = codes
            .OrderBy(static pair => pair.Key, StringComparer.Ordinal)
            .Select(static pair => new LegacyNxtThemeJoinAuditIdentity(pair.Key, pair.Value))
            .ToArray();
        var audit = await new LegacyNxtThemeJoinAuditService(executor)
            .AuditAsync(identities, cancellationToken)
            .ConfigureAwait(false);
        cancellationToken.ThrowIfCancellationRequested();

        var evidence = audit.Themes;
        var rawCurrent = evidence.Sum(static row => row.RawCurrentItemCount);
        var rawStored = evidence.Sum(static row => row.RawStoredItemCount);
        var currentOriginal = evidence.Sum(static row => row.CurrentOriginalJoinCount);
        var currentNonStopped = evidence.Sum(static row => row.CurrentNonStoppedJoinCount);
        var currentLive = evidence.Sum(static row => row.CurrentLiveJoinCount);
        var storedOriginal = evidence.Sum(static row => row.StoredOriginalJoinCount);
        var storedNonStopped = evidence.Sum(static row => row.StoredNonStoppedJoinCount);
        var storedLive = evidence.Sum(static row => row.StoredLiveJoinCount);
        var exactCandidate = evidence.Sum(static row => row.CurrentExactJoinCount);
        var stripBothCandidate = evidence.Sum(static row => row.CurrentStripBothJoinCount);
        var rightSixCandidate = evidence.Sum(static row => row.CurrentRightSixJoinCount);
        var validCodes = evidence.Sum(static row => row.CurrentValidItemCodeCount);
        var kospiMaster = evidence.Sum(static row => row.CurrentKospiMasterJoinCount);
        var kosdaqMaster = evidence.Sum(static row => row.CurrentKosdaqMasterJoinCount);
        var kospiOnline = evidence.Sum(static row => row.CurrentKospiOnlineJoinCount);
        var kosdaqOnline = evidence.Sum(static row => row.CurrentKosdaqOnlineJoinCount);
        var rawCurrentZero = evidence.Count(static row => row.RawCurrentItemCount == 0);
        var rawStoredPositive = evidence.Count(static row => row.RawStoredItemCount > 0);
        var originalCurrentPositive = evidence.Count(static row => row.CurrentOriginalJoinCount > 0);

        Console.WriteLine(
            "NXT_THEME_JOIN_DIAGNOSTIC: " +
            $"currentThemes={evidence.Count.ToString(CultureInfo.InvariantCulture)} " +
            $"remappedThemes={identities.Count(identity => !string.Equals(identity.CurrentThemeCode, identity.StoredThemeCode, StringComparison.Ordinal)).ToString(CultureInfo.InvariantCulture)} " +
            $"rawCurrent={rawCurrent.ToString(CultureInfo.InvariantCulture)} " +
            $"rawStored={rawStored.ToString(CultureInfo.InvariantCulture)} " +
            $"currentOriginalJoin={currentOriginal.ToString(CultureInfo.InvariantCulture)} " +
            $"currentNonStopped={currentNonStopped.ToString(CultureInfo.InvariantCulture)} " +
            $"currentLive={currentLive.ToString(CultureInfo.InvariantCulture)} " +
            $"storedOriginalJoin={storedOriginal.ToString(CultureInfo.InvariantCulture)} " +
            $"storedNonStopped={storedNonStopped.ToString(CultureInfo.InvariantCulture)} " +
            $"storedLive={storedLive.ToString(CultureInfo.InvariantCulture)} " +
            $"currentValidItemCodes={validCodes.ToString(CultureInfo.InvariantCulture)}");
        Console.WriteLine(
            "NXT_THEME_MASTER_JOIN_DIAGNOSTIC: " +
            $"kospiMaster={kospiMaster.ToString(CultureInfo.InvariantCulture)} " +
            $"kosdaqMaster={kosdaqMaster.ToString(CultureInfo.InvariantCulture)} " +
            $"kospiOnline={kospiOnline.ToString(CultureInfo.InvariantCulture)} " +
            $"kosdaqOnline={kosdaqOnline.ToString(CultureInfo.InvariantCulture)}");
        Console.WriteLine(
            "NXT_THEME_JOIN_CANDIDATES_DIAGNOSTIC_ONLY: " +
            $"exact={exactCandidate.ToString(CultureInfo.InvariantCulture)} " +
            $"stripBoth={stripBothCandidate.ToString(CultureInfo.InvariantCulture)} " +
            $"rightSix={rightSixCandidate.ToString(CultureInfo.InvariantCulture)}");
        Console.WriteLine(
            "NXT_THEME_JOIN_CLASSIFICATION: " +
            $"currentRawZeroThemes={rawCurrentZero.ToString(CultureInfo.InvariantCulture)} " +
            $"storedRawPositiveThemes={rawStoredPositive.ToString(CultureInfo.InvariantCulture)} " +
            $"currentOriginalPositiveThemes={originalCurrentPositive.ToString(CultureInfo.InvariantCulture)} " +
            $"root={ClassifyRoot(evidence)} " +
            "action=read-only-review-no-fallback");
    }

    private static string ClassifyRoot(
        IReadOnlyList<LegacyNxtThemeJoinAuditEvidence> evidence)
    {
        if (evidence.All(static row => row.RawCurrentItemCount == 0) &&
            evidence.All(static row => row.RawStoredItemCount > 0))
        {
            return "CurrentThemeCodeHasNoSbItemRowsStoredCodeHasRows";
        }

        if (evidence.Any(static row => row.RawCurrentItemCount > 0) &&
            evidence.All(static row => row.CurrentOriginalJoinCount == 0) &&
            evidence.All(static row =>
                row.CurrentKospiMasterJoinCount == 0 &&
                row.CurrentKosdaqMasterJoinCount == 0))
        {
            return "CurrentItemsMissingFromNxtStockMasters";
        }

        if (evidence.Any(static row => row.RawCurrentItemCount > 0) &&
            evidence.All(static row => row.CurrentOriginalJoinCount == 0) &&
            evidence.Any(static row =>
                row.CurrentKospiMasterJoinCount > 0 ||
                row.CurrentKosdaqMasterJoinCount > 0))
        {
            return "VAllStockExcludesCurrentNxtMasterRows";
        }

        if (evidence.Any(static row => row.RawCurrentItemCount > 0) &&
            evidence.All(static row => row.CurrentOriginalJoinCount == 0) &&
            evidence.Any(static row =>
                row.CurrentExactJoinCount > 0 ||
                row.CurrentStripBothJoinCount > 0 ||
                row.CurrentRightSixJoinCount > 0))
        {
            return "ItemCodeJoinShapeMismatch";
        }

        if (evidence.Any(static row => row.RawCurrentItemCount > 0) &&
            evidence.All(static row => row.CurrentOriginalJoinCount == 0))
        {
            return "CurrentItemsHaveNoStockIdentityJoin";
        }

        return "MixedOrUnclassifiedReadOnlyEvidence";
    }

    private static async Task RunEndpointAuditAsync(
        IDataQueryExecutor executor,
        MariaDbDatabaseOptions currentOptions,
        MariaDbEndpointReference referenceOptions,
        CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(executor);
        ArgumentNullException.ThrowIfNull(currentOptions);
        ArgumentNullException.ThrowIfNull(referenceOptions);
        cancellationToken.ThrowIfCancellationRequested();

        var original = NormalizeEndpoint(
            referenceOptions.Host,
            referenceOptions.Port,
            referenceOptions.Database);
        var current = NormalizeEndpoint(
            currentOptions.Host,
            currentOptions.Port,
            currentOptions.Database);
        var spec = new DataQuerySpec(
            "SELECT DATABASE() CURRENT_DATABASE, VERSION() SERVER_VERSION");
        spec.ValidateFor(DataSourceKind.MariaDb);
        var table = await executor.ExecuteAsync(
                DataSourceKind.MariaDb,
                "AUDIT_NXT_ENDPOINT_METADATA",
                spec,
                cancellationToken)
            .ConfigureAwait(false);
        cancellationToken.ThrowIfCancellationRequested();

        if (table is null || table.Columns.Count != 2 || table.Rows.Count != 1 ||
            !HasStringColumn(table.Columns[0], "CURRENT_DATABASE") ||
            !HasStringColumn(table.Columns[1], "SERVER_VERSION") ||
            table.Rows[0][0] is not string actualDatabase ||
            table.Rows[0][1] is not string serverVersion ||
            string.IsNullOrWhiteSpace(actualDatabase) ||
            string.IsNullOrWhiteSpace(serverVersion))
        {
            throw new InvalidDataException(
                "The NXT endpoint metadata audit returned an invalid closed result.");
        }

        var originalHash = HashEndpoint(original);
        var currentHash = HashEndpoint(current);
        var configuredEqual = string.Equals(original, current, StringComparison.Ordinal);
        var actualDatabaseMatches = string.Equals(
            NormalizeDatabase(actualDatabase),
            NormalizeDatabase(currentOptions.Database),
            StringComparison.Ordinal);
        var serverFamily = serverVersion.Contains(
            "MariaDB",
            StringComparison.OrdinalIgnoreCase)
            ? "MariaDB"
            : "MySqlCompatible";
        var versionParts = serverVersion.Split('.', '-', '+');
        var versionMajorMinor = versionParts.Length >= 2 &&
            int.TryParse(versionParts[0], NumberStyles.None, CultureInfo.InvariantCulture, out var major) &&
            int.TryParse(versionParts[1], NumberStyles.None, CultureInfo.InvariantCulture, out var minor)
                ? $"{major.ToString(CultureInfo.InvariantCulture)}.{minor.ToString(CultureInfo.InvariantCulture)}"
                : "unclassified";

        Console.WriteLine(
            "NXT_ENDPOINT_AUDIT: " +
            $"originalSha256={originalHash} " +
            $"currentSha256={currentHash} " +
            $"configuredEqual={configuredEqual.ToString().ToLowerInvariant()} " +
            $"actualDatabaseMatchesCurrent={actualDatabaseMatches.ToString().ToLowerInvariant()} " +
            $"serverFamily={serverFamily} " +
            $"serverMajorMinor={versionMajorMinor}");
    }

    private static string NormalizeEndpoint(string? host, int port, string? database)
    {
        var normalizedHost = (host ?? string.Empty)
            .Trim()
            .TrimEnd('.')
            .ToLowerInvariant();
        var normalizedDatabase = NormalizeDatabase(database);
        if (normalizedHost.Length == 0 || normalizedHost.Length > 253 ||
            normalizedDatabase.Length == 0 || normalizedDatabase.Length > 128 ||
            port is < 1 or > 65_535 ||
            normalizedHost.Any(static value => char.IsControl(value)) ||
            normalizedDatabase.Any(static value => char.IsControl(value)))
        {
            throw new InvalidDataException(
                "A MariaDB endpoint is invalid at the closed audit boundary.");
        }

        return string.Join(
            '\u001f',
            normalizedHost,
            port.ToString(CultureInfo.InvariantCulture),
            normalizedDatabase);
    }

    private static string NormalizeDatabase(string? database) =>
        (database ?? string.Empty).Trim().Normalize(NormalizationForm.FormC);

    private static string HashEndpoint(string endpoint) =>
        Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(endpoint)));

    private static bool HasStringColumn(DataColumn column, string name) =>
        string.Equals(column.ColumnName, name, StringComparison.Ordinal) &&
        column.DataType == typeof(string);
}

internal sealed record MariaDbEndpointReference(
    string? Host,
    int Port,
    string? Database)
{
    internal static MariaDbEndpointReference From(MariaDbDatabaseOptions options)
    {
        ArgumentNullException.ThrowIfNull(options);
        return new MariaDbEndpointReference(options.Host, options.Port, options.Database);
    }
}
