using System.Text;

namespace MBN_STOCK_WEBVIEW.LegacyApplication;

public enum LegacyRuntimeStockCutMenuFailure
{
    None,
    InvalidPath,
    FileNotFound,
    FileUnavailable,
    FileTooLarge,
    InvalidEncoding,
    InvalidFormat,
    TooManyRows,
    UnknownCut,
    EmptyMenu
}

public enum LegacyRuntimeStockCutMenuSource
{
    ExternalOverride,
    ConfiguredResourceDirectory,
    Packaged,
    BuiltIn
}

public sealed record LegacyRuntimeStockCutMenuComposition(
    IReadOnlyList<LegacyCutCatalogRow> Rows,
    LegacyRuntimeStockCutMenuSource Source,
    LegacyRuntimeStockCutMenuFailure Failure,
    string? WarningMessage)
{
    public bool UsesBuiltInCatalog => Source == LegacyRuntimeStockCutMenuSource.BuiltIn;
}

public static class LegacyRuntimeStockCutMenuLoader
{
    public const int MaximumRows = 50;
    public const int MaximumFileBytes = 64 * 1024;
    public const string StockSectionHeader = "[종목]";
    public const string FallbackWarningMessage =
        "런타임 컷 메뉴를 읽지 못해 내장 컷 목록을 사용합니다.";

    private static readonly Encoding Cp949 = CreateCp949Encoding();
    private static readonly HashSet<string> KnownCutLabels = LegacyStockCutCatalog.Rows
        .Where(row => !row.IsSeparator)
        .Select(row => row.RawLabel)
        .ToHashSet(StringComparer.Ordinal);

    public static LegacyRuntimeStockCutMenuComposition ResolveFromBaseDirectory(
        string? baseDirectory)
    {
        if (!TryCreatePackagedCandidatePath(baseDirectory, out var path))
        {
            return CreateFallbackComposition(
                LegacyRuntimeStockCutMenuFailure.InvalidPath);
        }

        if (TryLoad(path, out var rows, out var failure))
        {
            return CreateRuntimeComposition(
                rows,
                LegacyRuntimeStockCutMenuSource.Packaged);
        }

        return CreateFallbackComposition(failure);
    }

    /// <summary>
    /// Resolves the stock-cut menu from an already selected resource directory.
    /// Unlike <see cref="ResolveFromCandidates"/>, this entry point never treats a
    /// web-supplied file path as the protected local override. Callers are expected
    /// to pass a directory selected and validated by native code.
    /// </summary>
    public static LegacyRuntimeStockCutMenuComposition ResolveFromResourceDirectory(
        string? resourceDirectory)
    {
        if (!TryCreateResourceCandidatePath(resourceDirectory, out var path))
        {
            return CreateFallbackComposition(
                LegacyRuntimeStockCutMenuFailure.InvalidPath);
        }

        if (TryLoad(path, out var rows, out var failure))
        {
            return CreateRuntimeComposition(
                rows,
                LegacyRuntimeStockCutMenuSource.ConfiguredResourceDirectory);
        }

        return CreateFallbackComposition(failure);
    }

    public static LegacyRuntimeStockCutMenuComposition ResolveFromCandidates(
        string? externalOverridePath,
        string? packagedBaseDirectory)
    {
        if (externalOverridePath is not null)
        {
            if (!TryNormalizeExternalOverridePath(
                    externalOverridePath,
                    out var normalizedExternalPath))
            {
                return CreateFallbackComposition(
                    LegacyRuntimeStockCutMenuFailure.InvalidPath);
            }

            if (TryLoad(
                    normalizedExternalPath,
                    out var externalRows,
                    out var externalFailure))
            {
                return CreateRuntimeComposition(
                    externalRows,
                    LegacyRuntimeStockCutMenuSource.ExternalOverride);
            }

            if (externalFailure != LegacyRuntimeStockCutMenuFailure.FileNotFound)
            {
                return CreateFallbackComposition(externalFailure);
            }
        }

        return ResolveFromBaseDirectory(packagedBaseDirectory);
    }

    public static bool TryLoad(
        string? path,
        out IReadOnlyList<LegacyCutCatalogRow> rows,
        out LegacyRuntimeStockCutMenuFailure failure)
    {
        rows = Array.Empty<LegacyCutCatalogRow>();
        if (string.IsNullOrWhiteSpace(path) ||
            path.Any(char.IsControl) ||
            !Path.IsPathFullyQualified(path))
        {
            failure = LegacyRuntimeStockCutMenuFailure.InvalidPath;
            return false;
        }

        try
        {
            using var stream = new FileStream(
                path,
                FileMode.Open,
                FileAccess.Read,
                FileShare.Read,
                bufferSize: 4096,
                FileOptions.SequentialScan);
            if (stream.Length > MaximumFileBytes)
            {
                failure = LegacyRuntimeStockCutMenuFailure.FileTooLarge;
                return false;
            }

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

    public static bool TryParse(
        ReadOnlySpan<byte> cp949Bytes,
        out IReadOnlyList<LegacyCutCatalogRow> rows,
        out LegacyRuntimeStockCutMenuFailure failure)
    {
        rows = Array.Empty<LegacyCutCatalogRow>();
        if (cp949Bytes.Length > MaximumFileBytes)
        {
            failure = LegacyRuntimeStockCutMenuFailure.FileTooLarge;
            return false;
        }

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

        return TryParseDecodedText(text, out rows, out failure);
    }

    private static bool TryParseDecodedText(
        string text,
        out IReadOnlyList<LegacyCutCatalogRow> rows,
        out LegacyRuntimeStockCutMenuFailure failure)
    {
        rows = Array.Empty<LegacyCutCatalogRow>();
        var parsedRows = new List<LegacyCutCatalogRow>();
        var sawSection = false;
        var displayOrdinal = 0;

        using var reader = new StringReader(text);
        while (reader.ReadLine() is { } sourceLine)
        {
            var line = sourceLine.Trim();
            if (line.Length == 0)
            {
                continue;
            }

            if (!sawSection)
            {
                if (!string.Equals(line, StockSectionHeader, StringComparison.Ordinal))
                {
                    failure = LegacyRuntimeStockCutMenuFailure.InvalidFormat;
                    return false;
                }

                sawSection = true;
                continue;
            }

            if (line.StartsWith("[", StringComparison.Ordinal))
            {
                failure = LegacyRuntimeStockCutMenuFailure.InvalidFormat;
                return false;
            }

            if (parsedRows.Count >= MaximumRows)
            {
                failure = LegacyRuntimeStockCutMenuFailure.TooManyRows;
                return false;
            }

            var isSeparator = string.Equals(line, "-", StringComparison.Ordinal);
            if (!isSeparator && !KnownCutLabels.Contains(line))
            {
                failure = LegacyRuntimeStockCutMenuFailure.UnknownCut;
                return false;
            }

            parsedRows.Add(new LegacyCutCatalogRow(
                parsedRows.Count,
                isSeparator ? null : ++displayOrdinal,
                isSeparator ? string.Empty : line,
                isSeparator));
        }

        if (!sawSection)
        {
            failure = LegacyRuntimeStockCutMenuFailure.InvalidFormat;
            return false;
        }

        if (displayOrdinal == 0)
        {
            failure = LegacyRuntimeStockCutMenuFailure.EmptyMenu;
            return false;
        }

        rows = parsedRows.AsReadOnly();
        failure = LegacyRuntimeStockCutMenuFailure.None;
        return true;
    }

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

    private static LegacyRuntimeStockCutMenuComposition CreateFallbackComposition(
        LegacyRuntimeStockCutMenuFailure failure) =>
        new(
            LegacyStockCutCatalog.Rows,
            LegacyRuntimeStockCutMenuSource.BuiltIn,
            failure,
            FallbackWarningMessage);

    private static LegacyRuntimeStockCutMenuComposition CreateRuntimeComposition(
        IReadOnlyList<LegacyCutCatalogRow> rows,
        LegacyRuntimeStockCutMenuSource source) =>
        new(
            rows,
            source,
            LegacyRuntimeStockCutMenuFailure.None,
            WarningMessage: null);

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

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

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

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

    private static bool TryNormalizeExternalOverridePath(
        string path,
        out string normalizedPath)
    {
        normalizedPath = string.Empty;
        if (string.IsNullOrWhiteSpace(path) ||
            path.Any(char.IsControl) ||
            !Path.IsPathFullyQualified(path))
        {
            return false;
        }

        try
        {
            normalizedPath = Path.GetFullPath(path);
            var resourceDirectory = Path.GetDirectoryName(normalizedPath);
            var applicationDirectory = resourceDirectory is null
                ? null
                : Path.GetDirectoryName(resourceDirectory);
            return string.Equals(
                    Path.GetFileName(normalizedPath),
                    "종목.ini",
                    StringComparison.OrdinalIgnoreCase) &&
                string.Equals(
                    resourceDirectory is null
                        ? null
                        : Path.GetFileName(resourceDirectory),
                    "Res",
                    StringComparison.OrdinalIgnoreCase) &&
                string.Equals(
                    applicationDirectory is null
                        ? null
                        : Path.GetFileName(applicationDirectory),
                    "MBN_STOCK_WEBVIEW",
                    StringComparison.OrdinalIgnoreCase);
        }
        catch (Exception exception) when (
            exception is ArgumentException or
            IOException or
            NotSupportedException or
            System.Security.SecurityException)
        {
            normalizedPath = string.Empty;
            return false;
        }
    }
}
