#nullable enable

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

namespace MMoneyCoderSharp.Data;

public enum IndustryMarket
{
    Kospi,
    Kosdaq
}

public sealed record IndustrySelection(
    IndustryMarket Market,
    string IndustryName,
    string IndustryCode);

public interface IIndustrySelectionService
{
    Task<IReadOnlyList<IndustrySelection>> GetAsync(
        IndustryMarket market,
        CancellationToken cancellationToken = default);
}

public sealed class IndustrySelectionDataException : Exception
{
    public IndustrySelectionDataException(string message) : base(message)
    {
    }
}

/// <summary>
/// Reads the two industry selectors used by legacy UC2. The Web layer receives only
/// normalized names and codes; it never chooses a table or supplies SQL text.
/// </summary>
public sealed partial class LegacyIndustrySelectionService : IIndustrySelectionService
{
    public const int MaximumResults = 500;
    public const int MaximumNameLength = 128;

    private readonly IDataQueryExecutor _executor;

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

    public async Task<IReadOnlyList<IndustrySelection>> GetAsync(
        IndustryMarket market,
        CancellationToken cancellationToken = default)
    {
        var query = QueryFor(market);
        var table = await _executor.ExecuteAsync(
            DataSourceKind.Oracle,
            query.TableName,
            query.Spec,
            cancellationToken).ConfigureAwait(false);
        return Map(table, market);
    }

    private static IndustryQuery QueryFor(IndustryMarket market) => market switch
    {
        IndustryMarket.Kospi => new IndustryQuery(
            "OPERATOR_KOSPI_INDUSTRIES",
            new DataQuerySpec(
                "SELECT F_PART_NAME INDUSTRY_NAME, F_PART_CODE INDUSTRY_CODE " +
                "FROM T_PART WHERE F_PART_CODE <> '001' ORDER BY F_PART_NAME")),
        IndustryMarket.Kosdaq => new IndustryQuery(
            "OPERATOR_KOSDAQ_INDUSTRIES",
            new DataQuerySpec(
                "SELECT DISTINCT A.F_PART_NAME INDUSTRY_NAME, A.F_PART_CODE INDUSTRY_CODE " +
                "FROM T_KOSDAQ_PART A INNER JOIN T_KOSDAQ_INDEX B " +
                "ON A.F_PART_CODE = B.F_PART_CODE " +
                "WHERE A.F_PART_CODE <> '001' ORDER BY A.F_PART_CODE")),
        _ => throw new ArgumentOutOfRangeException(nameof(market), market, "Unsupported industry market.")
    };

    private static IReadOnlyList<IndustrySelection> Map(DataTable? table, IndustryMarket market)
    {
        if (table is null || table.Columns.Count != 2 ||
            !HasColumn(table, "INDUSTRY_NAME") || !HasColumn(table, "INDUSTRY_CODE") ||
            table.Rows.Count > MaximumResults)
        {
            throw new IndustrySelectionDataException("The industry selector returned an invalid schema.");
        }

        var results = new List<IndustrySelection>(table.Rows.Count);
        foreach (DataRow row in table.Rows)
        {
            var name = ReadRequiredText(row, "INDUSTRY_NAME", MaximumNameLength);
            var code = ReadRequiredText(row, "INDUSTRY_CODE", 32);
            if (!IndustryCodePattern().IsMatch(code))
            {
                throw new IndustrySelectionDataException("The industry selector returned an invalid code.");
            }

            // UC2 copied every query row into the ListBox in query order. Preserve
            // duplicate names/codes here; the later exact-one-row scene loader still
            // fails closed if a selected name is ambiguous for playout data.
            results.Add(new IndustrySelection(market, name, code));
        }

        return Array.AsReadOnly(results.ToArray());
    }

    private static bool HasColumn(DataTable table, string name) =>
        table.Columns.Cast<DataColumn>().Any(column =>
            string.Equals(column.ColumnName, name, StringComparison.OrdinalIgnoreCase));

    private static string ReadRequiredText(DataRow row, string column, int maximumLength)
    {
        var value = row[column];
        if (value is null or DBNull)
        {
            throw new IndustrySelectionDataException("The industry selector returned a missing value.");
        }

        var text = Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture)?.Trim();
        if (string.IsNullOrWhiteSpace(text) || text.Length > maximumLength ||
            text.Any(character => char.IsControl(character)))
        {
            throw new IndustrySelectionDataException("The industry selector returned an invalid value.");
        }

        return text;
    }

    [GeneratedRegex("^[A-Za-z0-9]{1,32}$", RegexOptions.CultureInvariant)]
    private static partial Regex IndustryCodePattern();

    private sealed record IndustryQuery(string TableName, DataQuerySpec Spec);
}
