using System.Collections.ObjectModel;
using System.Text;
using MMoneyCoderSharp.Data;

namespace MBN_STOCK_WEBVIEW.LegacyApplication;

public enum LegacyRuntimeUiCatalogFailure
{
    None,
    InvalidBaseDirectory,
    FileNotFound,
    FileUnavailable,
    FileTooLarge,
    InvalidEncoding,
    InvalidFormat,
    TooManySections,
    TooManyRows,
    UnknownEntry,
    DuplicateEntry,
    MissingEntry
}

public sealed record LegacyRuntimeUiCatalogWarning(
    string FileName,
    LegacyRuntimeUiCatalogFailure Failure);

public sealed class LegacyIndustryActionLayout
{
    private readonly IReadOnlyDictionary<IndustryMarket,
        IReadOnlyList<LegacyIndustryActionDefinition>> _actions;

    private LegacyIndustryActionLayout(
        IReadOnlyDictionary<IndustryMarket,
            IReadOnlyList<LegacyIndustryActionDefinition>> actions)
    {
        _actions = actions;
    }

    public static LegacyIndustryActionLayout Default { get; } = Create(
        LegacyIndustryActionCatalog.GetActions(IndustryMarket.Kospi),
        LegacyIndustryActionCatalog.GetActions(IndustryMarket.Kosdaq));

    public IReadOnlyList<LegacyIndustryActionDefinition> GetActions(IndustryMarket market) =>
        _actions.TryGetValue(market, out var actions)
            ? actions
            : throw new ArgumentOutOfRangeException(
                nameof(market), market, "Unsupported industry market.");

    internal static LegacyIndustryActionLayout Create(
        IReadOnlyList<LegacyIndustryActionDefinition> kospi,
        IReadOnlyList<LegacyIndustryActionDefinition> kosdaq)
    {
        Validate(IndustryMarket.Kospi, kospi);
        Validate(IndustryMarket.Kosdaq, kosdaq);
        return new LegacyIndustryActionLayout(
            new ReadOnlyDictionary<IndustryMarket,
                IReadOnlyList<LegacyIndustryActionDefinition>>(
                new Dictionary<IndustryMarket,
                    IReadOnlyList<LegacyIndustryActionDefinition>>
                {
                    [IndustryMarket.Kospi] = Array.AsReadOnly(kospi.ToArray()),
                    [IndustryMarket.Kosdaq] = Array.AsReadOnly(kosdaq.ToArray())
                }));
    }

    private static void Validate(
        IndustryMarket market,
        IReadOnlyList<LegacyIndustryActionDefinition> actions)
    {
        ArgumentNullException.ThrowIfNull(actions);
        var expected = LegacyIndustryActionCatalog.GetActions(market);
        if (actions.Count != expected.Count ||
            actions.Any(action => action is null || action.Market != market) ||
            !actions.Select(action => action.ActionId).ToHashSet(StringComparer.Ordinal)
                .SetEquals(expected.Select(action => action.ActionId)))
        {
            throw new InvalidDataException("The runtime industry action layout is invalid.");
        }
    }
}

internal sealed record LegacyComparisonActionPlacement(string ActionId, string Section);

public sealed class LegacyComparisonUiLayout
{
    private readonly IReadOnlyList<string> _fixedTargetIds;
    private readonly IReadOnlyList<LegacyComparisonActionPlacement> _actionPlacements;

    private LegacyComparisonUiLayout(
        IReadOnlyList<string> fixedTargetIds,
        IReadOnlyList<LegacyComparisonActionPlacement> actionPlacements)
    {
        _fixedTargetIds = fixedTargetIds;
        _actionPlacements = actionPlacements;
    }

    public static LegacyComparisonUiLayout Default { get; } = Create(
        LegacyComparisonWorkflowCatalog.FixedTargets.Select(target => target.TargetId),
        LegacyComparisonWorkflowCatalog.Actions.Select(action =>
            new LegacyComparisonActionPlacement(action.ActionId, action.Section)));

    internal IReadOnlyList<LegacyComparisonTarget> FixedTargets => _fixedTargetIds
        .Select(id => LegacyComparisonWorkflowCatalog.GetFixedTarget(id) ??
            throw new InvalidOperationException(
                "The runtime comparison layout contains an unknown target."))
        .ToArray();

    internal IReadOnlyList<LegacyComparisonActionView> CreateActionViews(
        LegacyComparisonTarget? first,
        LegacyComparisonTarget? second) =>
        LegacyComparisonWorkflowCatalog.CreateActionViews(
            first,
            second,
            _actionPlacements);

    internal static LegacyComparisonUiLayout Create(
        IEnumerable<string> fixedTargetIds,
        IEnumerable<LegacyComparisonActionPlacement> actionPlacements)
    {
        ArgumentNullException.ThrowIfNull(fixedTargetIds);
        ArgumentNullException.ThrowIfNull(actionPlacements);
        var targetIds = fixedTargetIds.ToArray();
        var placements = actionPlacements.ToArray();
        var expectedTargetIds = LegacyComparisonWorkflowCatalog.FixedTargets
            .Select(target => target.TargetId)
            .ToHashSet(StringComparer.Ordinal);
        var expectedActionIds = LegacyComparisonWorkflowCatalog.Actions
            .Select(action => action.ActionId)
            .ToHashSet(StringComparer.Ordinal);
        if (targetIds.Length != expectedTargetIds.Count ||
            targetIds.Distinct(StringComparer.Ordinal).Count() != targetIds.Length ||
            !targetIds.ToHashSet(StringComparer.Ordinal).SetEquals(expectedTargetIds) ||
            placements.Length != expectedActionIds.Count ||
            placements.Any(placement =>
                placement is null ||
                string.IsNullOrWhiteSpace(placement.Section) ||
                placement.Section.Any(char.IsControl)) ||
            placements.Select(placement => placement.ActionId)
                .Distinct(StringComparer.Ordinal).Count() != placements.Length ||
            !placements.Select(placement => placement.ActionId)
                .ToHashSet(StringComparer.Ordinal).SetEquals(expectedActionIds))
        {
            throw new InvalidDataException("The runtime comparison UI layout is invalid.");
        }

        return new LegacyComparisonUiLayout(
            Array.AsReadOnly(targetIds),
            Array.AsReadOnly(placements));
    }
}

public sealed record LegacyRuntimeUiCatalogComposition(
    LegacyFixedActionCatalog FixedCatalog,
    LegacyIndustryActionLayout IndustryLayout,
    LegacyComparisonUiLayout ComparisonLayout,
    IReadOnlyList<LegacyRuntimeUiCatalogWarning> Warnings)
{
    public string? WarningMessage => Warnings.Count == 0
        ? null
        : "런타임 UI INI 일부를 읽지 못해 검증된 내장 목록을 사용합니다: " +
          string.Join(", ", Warnings.Select(warning =>
              $"{warning.FileName} ({warning.Failure})"));
}

/// <summary>
/// Reads only the six non-secret CP949 UI catalogs next to the executable. Raw INI text
/// controls section and row order, while every executable action remains joined to one
/// pre-registered C# identity. A malformed file can therefore change neither scene aliases
/// nor mutation data and falls back independently to the embedded catalog.
/// </summary>
public static class LegacyRuntimeUiCatalogLoader
{
    public const int MaximumFileBytes = 64 * 1024;
    public const int MaximumSections = 20;
    public const int MaximumRowsPerSection = 50;
    public const int MaximumLineLength = 256;

    private static readonly Encoding Cp949 = CreateCp949Encoding();

    public static LegacyRuntimeUiCatalogComposition ResolveFromBaseDirectory(
        string? baseDirectory)
    {
        var warnings = new List<LegacyRuntimeUiCatalogWarning>();
        if (!TryCreateResourceDirectory(baseDirectory, out var resourceDirectory))
        {
            warnings.Add(new LegacyRuntimeUiCatalogWarning(
                "Res",
                LegacyRuntimeUiCatalogFailure.InvalidBaseDirectory));
            return CreateFallback(warnings);
        }

        return ResolveFromValidatedResourceDirectory(resourceDirectory);
    }

    /// <summary>
    /// Resolves the six non-secret UI catalogs from an already selected resource
    /// directory. The directory must be selected and validated by native code; no
    /// database credentials or playout configuration are read by this loader.
    /// </summary>
    public static LegacyRuntimeUiCatalogComposition ResolveFromResourceDirectory(
        string? resourceDirectory)
    {
        var warnings = new List<LegacyRuntimeUiCatalogWarning>();
        if (!TryNormalizeResourceDirectory(resourceDirectory, out var normalizedDirectory))
        {
            warnings.Add(new LegacyRuntimeUiCatalogWarning(
                "Res",
                LegacyRuntimeUiCatalogFailure.InvalidBaseDirectory));
            return CreateFallback(warnings);
        }

        return ResolveFromValidatedResourceDirectory(normalizedDirectory);
    }

    private static LegacyRuntimeUiCatalogComposition ResolveFromValidatedResourceDirectory(
        string resourceDirectory)
    {
        var warnings = new List<LegacyRuntimeUiCatalogWarning>();

        var fixedCatalog = LegacyFixedActionCatalog.Default;
        var fixedOrders = new Dictionary<LegacyFixedMarket,
            IReadOnlyList<LegacyFixedAction>>();
        LoadFixed(
            resourceDirectory,
            "해외.ini",
            LegacyFixedMarket.Overseas,
            fixedCatalog,
            fixedOrders,
            warnings);
        LoadFixed(
            resourceDirectory,
            "환율.ini",
            LegacyFixedMarket.Exchange,
            fixedCatalog,
            fixedOrders,
            warnings);
        LoadFixed(
            resourceDirectory,
            "지수.ini",
            LegacyFixedMarket.Index,
            fixedCatalog,
            fixedOrders,
            warnings);
        fixedCatalog = fixedCatalog.WithRuntimeOrder(fixedOrders);

        var kospi = LoadIndustry(
            resourceDirectory,
            "업종_코스피.ini",
            IndustryMarket.Kospi,
            warnings);
        var kosdaq = LoadIndustry(
            resourceDirectory,
            "업종_코스닥.ini",
            IndustryMarket.Kosdaq,
            warnings);
        var industryLayout = LegacyIndustryActionLayout.Create(kospi, kosdaq);

        var comparisonLayout = LoadComparison(
            resourceDirectory,
            "종목비교.ini",
            warnings);
        return new LegacyRuntimeUiCatalogComposition(
            fixedCatalog,
            industryLayout,
            comparisonLayout,
            Array.AsReadOnly(warnings.ToArray()));
    }

    private static LegacyRuntimeUiCatalogComposition CreateFallback(
        IReadOnlyList<LegacyRuntimeUiCatalogWarning> warnings) => new(
            LegacyFixedActionCatalog.Default,
            LegacyIndustryActionLayout.Default,
            LegacyComparisonUiLayout.Default,
            warnings);

    private static void LoadFixed(
        string resourceDirectory,
        string fileName,
        LegacyFixedMarket market,
        LegacyFixedActionCatalog catalog,
        IDictionary<LegacyFixedMarket, IReadOnlyList<LegacyFixedAction>> orders,
        ICollection<LegacyRuntimeUiCatalogWarning> warnings)
    {
        if (TryReadDocument(
                Path.Combine(resourceDirectory, fileName),
                out var document,
                out var failure) &&
            TryJoinRows(
                document,
                catalog.GetActions(market),
                action => action.Section,
                action => action.Label,
                out var actions,
                out failure))
        {
            orders.Add(market, actions);
            return;
        }

        orders.Add(market, catalog.GetActions(market));
        warnings.Add(new LegacyRuntimeUiCatalogWarning(fileName, failure));
    }

    private static IReadOnlyList<LegacyIndustryActionDefinition> LoadIndustry(
        string resourceDirectory,
        string fileName,
        IndustryMarket market,
        ICollection<LegacyRuntimeUiCatalogWarning> warnings)
    {
        var embedded = LegacyIndustryActionCatalog.GetActions(market);
        if (!TryReadDocument(
                Path.Combine(resourceDirectory, fileName),
                out var document,
                out var failure))
        {
            warnings.Add(new LegacyRuntimeUiCatalogWarning(fileName, failure));
            return embedded;
        }

        var expectedSection = market == IndustryMarket.Kospi ? "코스피" : "코스닥";
        if (document.Sections.Count != 1 ||
            !string.Equals(
                document.Sections[0].Name,
                expectedSection,
                StringComparison.Ordinal))
        {
            warnings.Add(new LegacyRuntimeUiCatalogWarning(
                fileName,
                LegacyRuntimeUiCatalogFailure.InvalidFormat));
            return embedded;
        }

        var expected = embedded.Select(action => new IndustryJoinRow(
            expectedSection,
            action.Label,
            action)).ToArray();
        if (!TryJoinRows(
                document,
                expected,
                row => row.Section,
                row => row.Label,
                out var ordered,
                out failure))
        {
            warnings.Add(new LegacyRuntimeUiCatalogWarning(fileName, failure));
            return embedded;
        }

        return Array.AsReadOnly(ordered
            .Select(row => row.Action with { Section = expectedSection })
            .ToArray());
    }

    private static LegacyComparisonUiLayout LoadComparison(
        string resourceDirectory,
        string fileName,
        ICollection<LegacyRuntimeUiCatalogWarning> warnings)
    {
        if (!TryReadDocument(
                Path.Combine(resourceDirectory, fileName),
                out var document,
                out var failure) ||
            !TryCreateComparisonLayout(document, out var layout, out failure))
        {
            warnings.Add(new LegacyRuntimeUiCatalogWarning(fileName, failure));
            return LegacyComparisonUiLayout.Default;
        }

        return layout;
    }

    private static bool TryCreateComparisonLayout(
        IniDocument document,
        out LegacyComparisonUiLayout layout,
        out LegacyRuntimeUiCatalogFailure failure)
    {
        layout = LegacyComparisonUiLayout.Default;
        if (document.Sections.Count != 6 ||
            !string.Equals(document.Sections[0].Name, "지수", StringComparison.Ordinal))
        {
            failure = LegacyRuntimeUiCatalogFailure.InvalidFormat;
            return false;
        }

        var targetByLabel = LegacyComparisonWorkflowCatalog.FixedTargets
            .ToDictionary(target => target.DisplayName, StringComparer.Ordinal);
        var targetIds = new List<string>(targetByLabel.Count);
        var targetLabels = new HashSet<string>(StringComparer.Ordinal);
        foreach (var label in document.Sections[0].Rows)
        {
            if (!targetLabels.Add(label))
            {
                failure = LegacyRuntimeUiCatalogFailure.DuplicateEntry;
                return false;
            }

            if (!targetByLabel.TryGetValue(label, out var target))
            {
                failure = LegacyRuntimeUiCatalogFailure.UnknownEntry;
                return false;
            }

            targetIds.Add(target.TargetId);
        }

        if (targetIds.Count != targetByLabel.Count)
        {
            failure = LegacyRuntimeUiCatalogFailure.MissingEntry;
            return false;
        }

        var actions = LegacyComparisonWorkflowCatalog.Actions;
        var directActions = actions
            .Where(action => action.Kind != LegacyComparisonActionKind.Return)
            .ToDictionary(action => action.Label, StringComparer.Ordinal);
        var returnActions = actions
            .Where(action => action.Kind == LegacyComparisonActionKind.Return)
            .ToArray();
        var returnSectionNames = returnActions
            .Select(action => action.Section)
            .Distinct(StringComparer.Ordinal)
            .ToArray();
        if (returnSectionNames.Length != 1)
        {
            failure = LegacyRuntimeUiCatalogFailure.InvalidFormat;
            return false;
        }

        var expectedSectionNames = directActions.Keys
            .Append(returnSectionNames[0])
            .ToHashSet(StringComparer.Ordinal);
        var actualSectionNames = document.Sections.Skip(1)
            .Select(section => section.Name)
            .ToHashSet(StringComparer.Ordinal);
        if (actualSectionNames.Count != expectedSectionNames.Count ||
            !actualSectionNames.SetEquals(expectedSectionNames))
        {
            failure = LegacyRuntimeUiCatalogFailure.UnknownEntry;
            return false;
        }

        var placements = new List<LegacyComparisonActionPlacement>(actions.Count);
        var actionIds = new HashSet<string>(StringComparer.Ordinal);
        foreach (var section in document.Sections.Skip(1))
        {
            if (directActions.TryGetValue(section.Name, out var directAction))
            {
                if (section.Rows.Count != 0)
                {
                    failure = LegacyRuntimeUiCatalogFailure.InvalidFormat;
                    return false;
                }

                if (!actionIds.Add(directAction.ActionId))
                {
                    failure = LegacyRuntimeUiCatalogFailure.DuplicateEntry;
                    return false;
                }

                placements.Add(new LegacyComparisonActionPlacement(
                    directAction.ActionId,
                    section.Name));
                continue;
            }

            if (!string.Equals(
                    section.Name,
                    returnSectionNames[0],
                    StringComparison.Ordinal))
            {
                failure = LegacyRuntimeUiCatalogFailure.UnknownEntry;
                return false;
            }

            var expectedReturnRows = returnActions.Select(action =>
                new ComparisonJoinRow(section.Name, action.Label, action)).ToArray();
            var returnDocument = new IniDocument(
                Array.AsReadOnly(new[] { section }));
            if (!TryJoinRows(
                    returnDocument,
                    expectedReturnRows,
                    row => row.Section,
                    row => row.Label,
                    out var orderedReturns,
                    out failure))
            {
                return false;
            }

            foreach (var row in orderedReturns)
            {
                actionIds.Add(row.Action.ActionId);
                placements.Add(new LegacyComparisonActionPlacement(
                    row.Action.ActionId,
                    section.Name));
            }
        }

        if (placements.Count != actions.Count || actionIds.Count != actions.Count)
        {
            failure = LegacyRuntimeUiCatalogFailure.MissingEntry;
            return false;
        }

        try
        {
            layout = LegacyComparisonUiLayout.Create(targetIds, placements);
            failure = LegacyRuntimeUiCatalogFailure.None;
            return true;
        }
        catch (InvalidDataException)
        {
            failure = LegacyRuntimeUiCatalogFailure.InvalidFormat;
            return false;
        }
    }

    private static bool TryJoinRows<T>(
        IniDocument document,
        IReadOnlyList<T> expected,
        Func<T, string> sectionSelector,
        Func<T, string> labelSelector,
        out IReadOnlyList<T> ordered,
        out LegacyRuntimeUiCatalogFailure failure)
    {
        ordered = Array.Empty<T>();
        var expectedSections = expected.Select(sectionSelector)
            .ToHashSet(StringComparer.Ordinal);
        var actualSections = document.Sections.Select(section => section.Name)
            .ToHashSet(StringComparer.Ordinal);
        if (document.Sections.Count != expectedSections.Count ||
            !actualSections.SetEquals(expectedSections))
        {
            failure = LegacyRuntimeUiCatalogFailure.InvalidFormat;
            return false;
        }

        var expectedOccurrences = new Dictionary<EntryKey, T>();
        var expectedCounts = new Dictionary<EntryPair, int>();
        foreach (var value in expected)
        {
            var pair = new EntryPair(sectionSelector(value), labelSelector(value));
            expectedCounts.TryGetValue(pair, out var occurrence);
            expectedCounts[pair] = occurrence + 1;
            expectedOccurrences.Add(
                new EntryKey(pair.Section, pair.Label, occurrence),
                value);
        }

        var result = new List<T>(expected.Count);
        var actualCounts = new Dictionary<EntryPair, int>();
        foreach (var section in document.Sections)
        {
            foreach (var label in section.Rows)
            {
                var pair = new EntryPair(section.Name, label);
                actualCounts.TryGetValue(pair, out var occurrence);
                actualCounts[pair] = occurrence + 1;
                if (!expectedOccurrences.TryGetValue(
                        new EntryKey(section.Name, label, occurrence),
                        out var value))
                {
                    failure = expectedCounts.ContainsKey(pair)
                        ? LegacyRuntimeUiCatalogFailure.DuplicateEntry
                        : LegacyRuntimeUiCatalogFailure.UnknownEntry;
                    return false;
                }

                result.Add(value);
            }
        }

        if (result.Count != expected.Count)
        {
            failure = LegacyRuntimeUiCatalogFailure.MissingEntry;
            return false;
        }

        ordered = Array.AsReadOnly(result.ToArray());
        failure = LegacyRuntimeUiCatalogFailure.None;
        return true;
    }

    private static bool TryReadDocument(
        string path,
        out IniDocument document,
        out LegacyRuntimeUiCatalogFailure failure)
    {
        document = IniDocument.Empty;
        byte[] bytes;
        try
        {
            using var stream = new FileStream(
                path,
                FileMode.Open,
                FileAccess.Read,
                FileShare.Read,
                4096,
                FileOptions.SequentialScan);
            if (stream.Length > MaximumFileBytes)
            {
                failure = LegacyRuntimeUiCatalogFailure.FileTooLarge;
                return false;
            }

            bytes = new byte[(int)stream.Length];
            stream.ReadExactly(bytes);
        }
        catch (Exception exception) when (
            exception is FileNotFoundException or DirectoryNotFoundException)
        {
            failure = LegacyRuntimeUiCatalogFailure.FileNotFound;
            return false;
        }
        catch (Exception exception) when (
            exception is IOException or UnauthorizedAccessException or
            ArgumentException or NotSupportedException or
            System.Security.SecurityException)
        {
            failure = LegacyRuntimeUiCatalogFailure.FileUnavailable;
            return false;
        }

        string text;
        try
        {
            text = Cp949.GetString(bytes);
        }
        catch (DecoderFallbackException)
        {
            failure = LegacyRuntimeUiCatalogFailure.InvalidEncoding;
            return false;
        }

        return TryParse(text, out document, out failure);
    }

    private static bool TryParse(
        string text,
        out IniDocument document,
        out LegacyRuntimeUiCatalogFailure failure)
    {
        document = IniDocument.Empty;
        var sections = new List<IniSection>();
        var sectionNames = new HashSet<string>(StringComparer.Ordinal);
        List<string>? currentRows = null;
        using var reader = new StringReader(text);
        while (reader.ReadLine() is { } sourceLine)
        {
            var line = sourceLine.Trim();
            if (line.Length == 0)
            {
                continue;
            }

            if (line.Length > MaximumLineLength ||
                line.Any(character => char.IsControl(character) || char.IsSurrogate(character)))
            {
                failure = LegacyRuntimeUiCatalogFailure.InvalidFormat;
                return false;
            }

            if (line[0] == '[')
            {
                if (line.Length < 3 || line[^1] != ']' ||
                    line.AsSpan(1, line.Length - 2).Contains('[') ||
                    line.AsSpan(1, line.Length - 2).Contains(']'))
                {
                    failure = LegacyRuntimeUiCatalogFailure.InvalidFormat;
                    return false;
                }

                var name = line[1..^1].Trim();
                if (name.Length == 0 || !sectionNames.Add(name))
                {
                    failure = name.Length == 0
                        ? LegacyRuntimeUiCatalogFailure.InvalidFormat
                        : LegacyRuntimeUiCatalogFailure.DuplicateEntry;
                    return false;
                }

                if (sections.Count >= MaximumSections)
                {
                    failure = LegacyRuntimeUiCatalogFailure.TooManySections;
                    return false;
                }

                currentRows = [];
                sections.Add(new IniSection(name, currentRows));
                continue;
            }

            if (currentRows is null)
            {
                failure = LegacyRuntimeUiCatalogFailure.InvalidFormat;
                return false;
            }

            if (currentRows.Count >= MaximumRowsPerSection)
            {
                failure = LegacyRuntimeUiCatalogFailure.TooManyRows;
                return false;
            }

            currentRows.Add(line);
        }

        if (sections.Count == 0)
        {
            failure = LegacyRuntimeUiCatalogFailure.InvalidFormat;
            return false;
        }

        document = new IniDocument(Array.AsReadOnly(sections.ToArray()));
        failure = LegacyRuntimeUiCatalogFailure.None;
        return true;
    }

    private static bool TryCreateResourceDirectory(
        string? baseDirectory,
        out string resourceDirectory)
    {
        resourceDirectory = string.Empty;
        if (string.IsNullOrWhiteSpace(baseDirectory) ||
            baseDirectory.Any(char.IsControl) ||
            !Path.IsPathFullyQualified(baseDirectory))
        {
            return false;
        }

        try
        {
            resourceDirectory = Path.Combine(Path.GetFullPath(baseDirectory), "Res");
            return true;
        }
        catch (Exception exception) when (
            exception is ArgumentException or IOException or NotSupportedException or
            System.Security.SecurityException)
        {
            return false;
        }
    }

    private static bool TryNormalizeResourceDirectory(
        string? candidate,
        out string resourceDirectory)
    {
        resourceDirectory = string.Empty;
        if (string.IsNullOrWhiteSpace(candidate) ||
            candidate.Any(char.IsControl) ||
            !Path.IsPathFullyQualified(candidate))
        {
            return false;
        }

        try
        {
            resourceDirectory = Path.GetFullPath(candidate);
            return true;
        }
        catch (Exception exception) when (
            exception is ArgumentException or IOException or NotSupportedException or
            System.Security.SecurityException)
        {
            return false;
        }
    }

    private static Encoding CreateCp949Encoding()
    {
        Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
        return Encoding.GetEncoding(
            949,
            EncoderFallback.ExceptionFallback,
            DecoderFallback.ExceptionFallback);
    }

    private sealed record IniSection(string Name, IReadOnlyList<string> Rows);

    private sealed record IniDocument(IReadOnlyList<IniSection> Sections)
    {
        public static IniDocument Empty { get; } = new(Array.Empty<IniSection>());
    }

    private sealed record IndustryJoinRow(
        string Section,
        string Label,
        LegacyIndustryActionDefinition Action);

    private sealed record ComparisonJoinRow(
        string Section,
        string Label,
        LegacyComparisonActionDefinition Action);

    private readonly record struct EntryPair(string Section, string Label);

    private readonly record struct EntryKey(string Section, string Label, int Occurrence);
}
