#nullable enable

using System.Data;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;

namespace MMoneyCoderSharp.Data;

public enum LegacyNamedKrxThemeRestoreFailure
{
    InvalidRow,
    UnsupportedAction,
    IdentityNotFound,
    IdentityAmbiguous,
    PreviewEmpty,
    DatabaseDataInvalid
}

/// <summary>
/// One untrusted seven-field PLAY_LIST row routed to the KRX-theme verifier.
/// Every original field remains evidence; a successful restore uses the
/// current Oracle identity for scene selection.
/// </summary>
public sealed record LegacyNamedKrxThemeRestoreRow(
    int ItemIndex,
    bool IsEnabled,
    string GroupCode,
    string Subject,
    string GraphicType,
    string Subtype,
    string PageText,
    string DataCode);

/// <summary>
/// A closed current KRX identity. StoredThemeCode is evidence only and must
/// never be used by PREPARE after this identity has been resolved.
/// </summary>
public sealed record LegacyNamedKrxThemeRestoreIdentity(
    string ActionId,
    string BuilderKey,
    string CutCode,
    int PageSize,
    string Sort,
    string ThemeTitle,
    string StoredThemeCode,
    string CurrentThemeCode,
    int PreviewItemCount,
    bool PreviewIsTruncated)
{
    public bool CodeWasRemapped =>
        !string.Equals(StoredThemeCode, CurrentThemeCode, StringComparison.Ordinal);
}

public sealed record LegacyNamedKrxThemeRestoreOutcome(
    int ItemIndex,
    LegacyNamedKrxThemeRestoreIdentity? Identity,
    LegacyNamedKrxThemeRestoreFailure? Failure,
    string? ObservedCurrentThemeCode = null)
{
    public bool IsResolved => Identity is not null && Failure is null;
}

public sealed record LegacyNamedKrxThemeRestoreResult(
    DateTimeOffset RetrievedAt,
    IReadOnlyList<LegacyNamedKrxThemeRestoreOutcome> Rows);

/// <summary>
/// Rehydrates legacy `테마` rows through exact, bound Oracle reads. The old
/// s5074/s5077/s5088 constructors looked the code up by exact title immediately
/// before loading data. This service closes that lookup to one title and one
/// code, validates the selected items through LegacyThemeSelectionService, and
/// retains the stale stored code solely for lossless PLAY_LIST persistence.
/// </summary>
public sealed partial class LegacyNamedKrxThemeRestoreService
{
    public const int MaximumRows = LegacyNamedPlaylistPersistenceService.MaximumItems;
    public const string IdentityQueryName = "LEGACY_NAMED_KRX_THEME_IDENTITY";

    private const string ExpectedGroupCode = "테마";
    private const int MaximumTitleLength = 128;

    private static readonly IReadOnlyDictionary<string, SubtypeProfile> Subtypes =
        new Dictionary<string, SubtypeProfile>(StringComparer.Ordinal)
        {
            ["테마-현재가(입력순)"] = new("CURRENT", "INPUT_ORDER"),
            ["테마-현재가(상승률순)"] = new("CURRENT", "GAIN_DESC"),
            ["테마-현재가(하락률순)"] = new("CURRENT", "GAIN_ASC"),
            // All three original scene constructors fall through to their
            // final loss-rate branch when no parenthesized token is present.
            ["테마-현재가"] = new("CURRENT", "GAIN_ASC"),
            ["테마-예상체결가(입력순)"] = new("EXPECTED", "INPUT_ORDER"),
            ["테마-예상체결가(상승률순)"] = new("EXPECTED", "GAIN_DESC"),
            ["테마-예상체결가(하락률순)"] = new("EXPECTED", "GAIN_ASC"),
            ["테마-예상체결가"] = new("EXPECTED", "GAIN_ASC")
        };

    private static readonly IReadOnlyDictionary<string, ActionProfile> Actions =
        new Dictionary<string, ActionProfile>(StringComparer.Ordinal)
        {
            [ActionKey("5단 표그래프", "CURRENT")] =
                new("five-row-current", "s5074", "5074", 5),
            [ActionKey("5단 표그래프", "EXPECTED")] =
                new("five-row-expected", "s5074", "5074", 5),
            [ActionKey("6종목 현재가", "CURRENT")] =
                new("six-row-current", "s5077", "5077", 6),
            [ActionKey("12종목 현재가", "CURRENT")] =
                new("twelve-row-current", "s5088", "5088", 12)
        };

    private const string IdentitySql = """
        SELECT THEME_TITLE, THEME_CODE, THEME_MARKET, CODE_IDENTITY_COUNT
        FROM (
            SELECT s.SB_TITLE THEME_TITLE,
                   s.SB_CODE THEME_CODE,
                   s.SB_MARKET THEME_MARKET,
                   TO_CHAR((SELECT COUNT(*)
                            FROM SB_LIST d
                            WHERE d.SB_MARKET = 'KRX'
                              AND d.SB_CODE = s.SB_CODE), 'FM999999990')
                       CODE_IDENTITY_COUNT
            FROM SB_LIST s
            WHERE s.SB_MARKET = 'KRX'
              AND s.SB_TITLE = :theme_title
            ORDER BY s.SB_CODE
        )
        WHERE ROWNUM <= 2
        """;

    private static readonly string[] IdentityColumns =
        ["THEME_TITLE", "THEME_CODE", "THEME_MARKET", "CODE_IDENTITY_COUNT"];

    private readonly IDataQueryExecutor _executor;
    private readonly LegacyThemeSelectionService _themeSelectionService;
    private readonly TimeProvider _timeProvider;

    public LegacyNamedKrxThemeRestoreService(IDataQueryExecutor executor)
        : this(executor, TimeProvider.System)
    {
    }

    internal LegacyNamedKrxThemeRestoreService(
        IDataQueryExecutor executor,
        TimeProvider timeProvider)
    {
        _executor = executor ?? throw new ArgumentNullException(nameof(executor));
        _themeSelectionService = new LegacyThemeSelectionService(_executor);
        _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
    }

    /// <summary>
    /// Routes every exact `테마` group row, including malformed grammar, so
    /// native verification returns an explicit closed result rather than
    /// allowing the stale code through the generic mapper.
    /// </summary>
    public static bool IsCandidate(LegacyNamedKrxThemeRestoreRow? row) =>
        row is not null &&
        row.ItemIndex is >= 0 and < MaximumRows &&
        string.Equals(row.GroupCode, ExpectedGroupCode, StringComparison.Ordinal);

    public async Task<LegacyNamedKrxThemeRestoreResult> ResolveAsync(
        IReadOnlyList<LegacyNamedKrxThemeRestoreRow> rows,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(rows);
        if (rows.Count is < 1 or > MaximumRows)
        {
            throw new ArgumentOutOfRangeException(
                nameof(rows),
                $"A KRX-theme restore must contain 1 through {MaximumRows} rows.");
        }

        var indexes = new HashSet<int>();
        foreach (var row in rows)
        {
            if (row is null || row.ItemIndex is < 0 or >= MaximumRows ||
                !indexes.Add(row.ItemIndex))
            {
                throw new ArgumentException(
                    "KRX-theme restore rows must have unique bounded item indexes.",
                    nameof(rows));
            }
        }

        var verifications = new Dictionary<string, ThemeVerification>(StringComparer.Ordinal);
        var outcomes = new List<LegacyNamedKrxThemeRestoreOutcome>(rows.Count);
        foreach (var row in rows)
        {
            cancellationToken.ThrowIfCancellationRequested();
            outcomes.Add(await ResolveRowAsync(row, verifications, cancellationToken)
                .ConfigureAwait(false));
        }

        return new LegacyNamedKrxThemeRestoreResult(
            _timeProvider.GetLocalNow(),
            outcomes.AsReadOnly());
    }

    private async Task<LegacyNamedKrxThemeRestoreOutcome> ResolveRowAsync(
        LegacyNamedKrxThemeRestoreRow row,
        IDictionary<string, ThemeVerification> verifications,
        CancellationToken cancellationToken)
    {
        var classification = Classify(row);
        if (classification.Failure.HasValue)
        {
            return Failed(row.ItemIndex, classification.Failure.Value);
        }

        var title = classification.Title!;
        if (!verifications.TryGetValue(title, out var verification))
        {
            verification = await VerifyThemeAsync(title, cancellationToken).ConfigureAwait(false);
            verifications.Add(title, verification);
        }

        if (verification.Failure.HasValue)
        {
            return Failed(
                row.ItemIndex,
                verification.Failure.Value,
                verification.CurrentThemeCode);
        }

        var profile = classification.Action!;
        return new LegacyNamedKrxThemeRestoreOutcome(
            row.ItemIndex,
            new LegacyNamedKrxThemeRestoreIdentity(
                profile.ActionId,
                profile.BuilderKey,
                profile.CutCode,
                profile.PageSize,
                classification.Sort!,
                title,
                row.DataCode,
                verification.CurrentThemeCode!,
                verification.PreviewItemCount,
                verification.PreviewIsTruncated),
            Failure: null,
            ObservedCurrentThemeCode: verification.CurrentThemeCode);
    }

    private static RowClassification Classify(LegacyNamedKrxThemeRestoreRow row)
    {
        if (!string.Equals(row.GroupCode, ExpectedGroupCode, StringComparison.Ordinal) ||
            !IsSafeCanonicalText(row.Subject, MaximumTitleLength) ||
            row.Subject.Contains("(NXT)", StringComparison.Ordinal) ||
            !StoredThemeCodePattern().IsMatch(row.DataCode ?? string.Empty) ||
            !IsValidStoredPage(row.PageText))
        {
            return new RowClassification(Failure: LegacyNamedKrxThemeRestoreFailure.InvalidRow);
        }

        if (!Subtypes.TryGetValue(row.Subtype ?? string.Empty, out var subtype))
        {
            return new RowClassification(
                Failure: LegacyNamedKrxThemeRestoreFailure.UnsupportedAction);
        }

        if (!Actions.TryGetValue(
                ActionKey(row.GraphicType ?? string.Empty, subtype.ValueKind),
                out var action))
        {
            return new RowClassification(
                Failure: LegacyNamedKrxThemeRestoreFailure.UnsupportedAction);
        }

        return new RowClassification(row.Subject, action, subtype.Sort, Failure: null);
    }

    private async Task<ThemeVerification> VerifyThemeAsync(
        string title,
        CancellationToken cancellationToken)
    {
        var spec = new DataQuerySpec(
            IdentitySql,
            [new DataQueryParameter("theme_title", title, DbType.String)]);
        spec.ValidateFor(DataSourceKind.Oracle);
        var table = await _executor.ExecuteAsync(
                DataSourceKind.Oracle,
                IdentityQueryName,
                spec,
                cancellationToken)
            .ConfigureAwait(false);
        cancellationToken.ThrowIfCancellationRequested();

        var identity = ValidateIdentityTable(table, title);
        if (identity.Failure.HasValue)
        {
            return identity;
        }

        var selectedIdentity = new ThemeSelectionIdentity(
            ThemeMarket.Krx,
            ThemeSession.NotApplicable,
            identity.CurrentThemeCode!,
            title);
        ThemePreviewResult preview;
        try
        {
            preview = await _themeSelectionService.PreviewAsync(
                    selectedIdentity,
                    LegacyThemeSelectionService.MaximumPreviewItems,
                    cancellationToken)
                .ConfigureAwait(false);
        }
        catch (ThemeSelectionDataException)
        {
            return InvalidDatabaseData(identity.CurrentThemeCode);
        }

        if (!Equals(preview.Identity, selectedIdentity) ||
            preview.Items.Count > LegacyThemeSelectionService.MaximumPreviewItems ||
            preview.IsTruncated &&
                preview.Items.Count != LegacyThemeSelectionService.MaximumPreviewItems)
        {
            return InvalidDatabaseData(identity.CurrentThemeCode);
        }

        if (preview.Items.Count == 0)
        {
            return identity with
            {
                Failure = LegacyNamedKrxThemeRestoreFailure.PreviewEmpty
            };
        }

        return identity with
        {
            PreviewItemCount = preview.Items.Count,
            PreviewIsTruncated = preview.IsTruncated
        };
    }

    private static ThemeVerification ValidateIdentityTable(DataTable? table, string title)
    {
        if (table is null || table.Columns.Count != IdentityColumns.Length ||
            table.Rows.Count > 2)
        {
            return InvalidDatabaseData();
        }

        for (var index = 0; index < IdentityColumns.Length; index++)
        {
            if (!string.Equals(
                    table.Columns[index].ColumnName,
                    IdentityColumns[index],
                    StringComparison.Ordinal) ||
                table.Columns[index].DataType != typeof(string))
            {
                return InvalidDatabaseData();
            }
        }

        if (table.Rows.Count == 0)
        {
            return new ThemeVerification(
                Failure: LegacyNamedKrxThemeRestoreFailure.IdentityNotFound);
        }

        if (table.Rows.Count > 1)
        {
            return new ThemeVerification(
                Failure: LegacyNamedKrxThemeRestoreFailure.IdentityAmbiguous);
        }

        var row = table.Rows[0];
        if (row[0] is not string actualTitle ||
            row[1] is not string code ||
            row[2] is not string market ||
            !string.Equals(actualTitle, title, StringComparison.Ordinal) ||
            !string.Equals(market, "KRX", StringComparison.Ordinal) ||
            !StoredThemeCodePattern().IsMatch(code) ||
            !TryReadCount(row[3], out var codeIdentityCount))
        {
            return InvalidDatabaseData();
        }

        if (codeIdentityCount != 1)
        {
            return new ThemeVerification(
                CurrentThemeCode: code,
                Failure: LegacyNamedKrxThemeRestoreFailure.IdentityAmbiguous);
        }

        return new ThemeVerification(CurrentThemeCode: code, Failure: null);
    }

    private static bool TryReadCount(object value, out int result)
    {
        result = 0;
        return value is string text &&
            int.TryParse(text, NumberStyles.None, CultureInfo.InvariantCulture, out result) &&
            result is >= 0 and <= 1_000_000 &&
            string.Equals(
                text,
                result.ToString(CultureInfo.InvariantCulture),
                StringComparison.Ordinal);
    }

    private static bool IsValidStoredPage(string value)
    {
        var match = StoredPagePattern().Match(value ?? string.Empty);
        if (!match.Success ||
            !int.TryParse(
                match.Groups[1].Value,
                NumberStyles.None,
                CultureInfo.InvariantCulture,
                out var current) ||
            !int.TryParse(
                match.Groups[2].Value,
                NumberStyles.None,
                CultureInfo.InvariantCulture,
                out var total))
        {
            return false;
        }

        return current is >= 1 and <= LegacyNamedPlaylistPersistenceService.MaximumStoredPageCount &&
            total is >= 0 and <= LegacyNamedPlaylistPersistenceService.MaximumStoredPageCount &&
            (total == 0 ? current == 1 : current <= total);
    }

    private static bool IsSafeCanonicalText(string? value, int maximumLength)
    {
        if (string.IsNullOrEmpty(value) || value.Length > maximumLength ||
            !string.Equals(value, value.Trim(), StringComparison.Ordinal) ||
            value.Contains('^'))
        {
            return false;
        }

        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 static string ActionKey(string graphicType, string valueKind) =>
        string.Concat(graphicType, "\u001F", valueKind);

    private static LegacyNamedKrxThemeRestoreOutcome Failed(
        int itemIndex,
        LegacyNamedKrxThemeRestoreFailure failure,
        string? observedCurrentThemeCode = null) =>
        new(itemIndex, Identity: null, failure, observedCurrentThemeCode);

    private static ThemeVerification InvalidDatabaseData(string? currentThemeCode = null) =>
        new(
            CurrentThemeCode: currentThemeCode,
            Failure: LegacyNamedKrxThemeRestoreFailure.DatabaseDataInvalid);

    private sealed record ActionProfile(
        string ActionId,
        string BuilderKey,
        string CutCode,
        int PageSize);

    private sealed record SubtypeProfile(string ValueKind, string Sort);

    private sealed record RowClassification(
        string? Title = null,
        ActionProfile? Action = null,
        string? Sort = null,
        LegacyNamedKrxThemeRestoreFailure? Failure = null);

    private sealed record ThemeVerification(
        string? CurrentThemeCode = null,
        int PreviewItemCount = 0,
        bool PreviewIsTruncated = false,
        LegacyNamedKrxThemeRestoreFailure? Failure = null);

    [GeneratedRegex("^[0-9]{3,8}$", RegexOptions.CultureInvariant)]
    private static partial Regex StoredThemeCodePattern();

    [GeneratedRegex("^([1-9][0-9]{0,3})/([0-9]{1,4})$", RegexOptions.CultureInvariant)]
    private static partial Regex StoredPagePattern();
}
