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

namespace MBN_STOCK_WEBVIEW.LegacyApplication;

/// <summary>
/// Compatibility search for the operator screen. Unlike the modern lookup it intentionally
/// keeps blank-enter, LIKE wildcard, provider ordering and unlimited-result semantics.
/// Values remain bound parameters so parity does not reintroduce SQL concatenation.
/// </summary>
public sealed class LegacyParityStockSearchService : ILegacyStockLookup
{
    public const int LegacyTextBoxMaximumLength = 32_767;

    private static readonly SearchProfile[] Profiles =
    [
        new(
            LegacyDomesticStockMarket.Kospi,
            DataSourceKind.Oracle,
            "LEGACY_PARITY_KOSPI",
            "SELECT F_STOCK_WANNAME AS STOCK_NAME, F_STOCK_CODE AS STOCK_CODE FROM T_STOCK WHERE F_MKT_HALT = 'N' AND UPPER(F_STOCK_WANNAME) LIKE '%' || :search_term || '%'",
            "SELECT F_STOCK_WANNAME AS STOCK_NAME, F_STOCK_CODE AS STOCK_CODE FROM T_STOCK WHERE F_MKT_HALT = 'N'"),
        new(
            LegacyDomesticStockMarket.Kosdaq,
            DataSourceKind.Oracle,
            "LEGACY_PARITY_KOSDAQ",
            "SELECT F_STOCK_WANNAME AS STOCK_NAME, F_STOCK_CODE AS STOCK_CODE FROM T_KOSDAQ_STOCK WHERE F_MKT_HALT = 'N' AND UPPER(F_STOCK_WANNAME) LIKE '%' || :search_term || '%'",
            "SELECT F_STOCK_WANNAME AS STOCK_NAME, F_STOCK_CODE AS STOCK_CODE FROM T_KOSDAQ_STOCK WHERE F_MKT_HALT = 'N'"),
        new(
            LegacyDomesticStockMarket.NxtKospi,
            DataSourceKind.MariaDb,
            "LEGACY_PARITY_NXT_KOSPI",
            "SELECT CONCAT(a.F_STOCK_NAME, '(NXT)') AS STOCK_NAME, a.F_STOCK_CODE AS STOCK_CODE FROM N_STOCK a, N_ONLINE b WHERE a.F_STOP_GUBUN = 'N' AND a.F_STOCK_CODE = b.F_STOCK_CODE AND UPPER(a.F_STOCK_NAME) LIKE CONCAT('%', @search_term, '%')",
            "SELECT CONCAT(F_STOCK_NAME, '(NXT)') AS STOCK_NAME, F_STOCK_CODE AS STOCK_CODE FROM N_STOCK WHERE F_STOP_GUBUN = 'N'"),
        new(
            LegacyDomesticStockMarket.NxtKosdaq,
            DataSourceKind.MariaDb,
            "LEGACY_PARITY_NXT_KOSDAQ",
            "SELECT CONCAT(a.F_STOCK_NAME, '(NXT)') AS STOCK_NAME, a.F_STOCK_CODE AS STOCK_CODE FROM N_KOSDAQ_STOCK a, N_KOSDAQ_ONLINE b WHERE a.F_STOP_GUBUN = 'N' AND a.F_STOCK_CODE = b.F_STOCK_CODE AND UPPER(a.F_STOCK_NAME) LIKE CONCAT('%', @search_term, '%')",
            "SELECT CONCAT(F_STOCK_NAME, '(NXT)') AS STOCK_NAME, F_STOCK_CODE AS STOCK_CODE FROM N_KOSDAQ_STOCK WHERE F_STOP_GUBUN = 'N'")
    ];

    private readonly IDataQueryExecutor _executor;
    private readonly SemaphoreSlim _masterLoadGate = new(1, 1);
    private IReadOnlyList<LegacyStockIdentity>? _masterCatalog;

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

    public async Task<LegacyStockSearchData> SearchAsync(
        string query,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(query);
        if (query.Length > LegacyTextBoxMaximumLength)
        {
            throw new ArgumentOutOfRangeException(
                nameof(query),
                $"The legacy search text cannot exceed {LegacyTextBoxMaximumLength} characters.");
        }

        var masterCatalog = await GetMasterCatalogAsync(cancellationToken).ConfigureAwait(false);
        var isInitialSearch = HangulSearchMatcher.IsInitialConsonantQuery(query);
        // Keep the candidate set identical to the legacy provider searches.
        // In particular, NXT search joins the online tables while the complete
        // identity master intentionally does not. The master must therefore
        // never be used as the visible result set for an initial-only query.
        var searchTerm = isInitialSearch
            ? string.Empty
            : query.ToUpper(CultureInfo.CurrentCulture);
        var names = new List<string>();

        foreach (var profile in Profiles)
        {
            cancellationToken.ThrowIfCancellationRequested();
            var parameter = new DataQueryParameter("search_term", searchTerm, DbType.String);
            var spec = new DataQuerySpec(profile.SearchSql, [parameter]);
            var table = await _executor
                .ExecuteAsync(profile.Source, profile.SearchTableName, spec, cancellationToken)
                .ConfigureAwait(false);
            AppendNames(table, profile.SearchTableName, names);
        }

        var displayNames = isInitialSearch
            ? names.Where(name => HangulSearchMatcher.IsMatch(name, query))
            : names;
        return new LegacyStockSearchData(query, displayNames, masterCatalog);
    }

    private async Task<IReadOnlyList<LegacyStockIdentity>> GetMasterCatalogAsync(
        CancellationToken cancellationToken)
    {
        var cached = Volatile.Read(ref _masterCatalog);
        if (cached is not null)
        {
            return cached;
        }

        await _masterLoadGate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            cached = Volatile.Read(ref _masterCatalog);
            if (cached is not null)
            {
                return cached;
            }

            var items = new List<LegacyStockIdentity>();
            foreach (var profile in Profiles)
            {
                cancellationToken.ThrowIfCancellationRequested();
                var table = await _executor
                    .ExecuteAsync(
                        profile.Source,
                        $"{profile.SearchTableName}_MASTER",
                        new DataQuerySpec(profile.MasterSql),
                        cancellationToken)
                    .ConfigureAwait(false);
                AppendMaster(table, profile, items);
            }

            cached = Array.AsReadOnly(items.ToArray());
            Volatile.Write(ref _masterCatalog, cached);
            return cached;
        }
        finally
        {
            _masterLoadGate.Release();
        }
    }

    private static void AppendNames(DataTable? table, string sourceName, ICollection<string> target)
    {
        ValidateTable(table, sourceName);
        foreach (DataRow row in table!.Rows)
        {
            target.Add(Convert.ToString(row[0], CultureInfo.CurrentCulture) ?? string.Empty);
        }
    }

    private static void AppendMaster(
        DataTable? table,
        SearchProfile profile,
        ICollection<LegacyStockIdentity> target)
    {
        ValidateTable(table, $"{profile.SearchTableName}_MASTER");
        foreach (DataRow row in table!.Rows)
        {
            target.Add(new LegacyStockIdentity(
                profile.Market,
                Convert.ToString(row[0], CultureInfo.CurrentCulture) ?? string.Empty,
                Convert.ToString(row[1], CultureInfo.CurrentCulture) ?? string.Empty));
        }
    }

    private static void ValidateTable(DataTable? table, string sourceName)
    {
        if (table is null || table.Columns.Count < 2)
        {
            throw new InvalidOperationException(
                $"Legacy stock data from {sourceName} does not contain two columns.");
        }
    }

    private sealed record SearchProfile(
        LegacyDomesticStockMarket Market,
        DataSourceKind Source,
        string SearchTableName,
        string SearchSql,
        string MasterSql);
}
