#nullable enable

using System.Globalization;
using System.Text;

namespace MMoneyCoderSharp.Data;

/// <summary>
/// The exact seven values written by the original PList.data_save method.
/// RawListText and Fields are retained so a read-only audit can prove that
/// parsing did not normalize any operator text. PageTextForDisplay is the one
/// original load-time transformation: ASCII spaces are removed from field 5.
/// </summary>
public sealed record LegacyPListAuditRow(
    string ProgramCode,
    string ProgramTitle,
    int ItemIndex,
    bool IsEnabled,
    IReadOnlyList<string> Fields,
    string RawListText,
    string PageTextForDisplay)
{
    public string GroupCode => Fields[1];

    public string Subject => Fields[2];

    public string GraphicType => Fields[3];

    public string Subtype => Fields[4];

    public string RawPageText => Fields[5];

    public string DataCode => Fields[6];

    public bool IsLossless =>
        string.Equals(string.Join('^', Fields), RawListText, StringComparison.Ordinal);
}

public enum LegacyPListAuditParseFailure
{
    InvalidProgramCode,
    InvalidProgramTitle,
    InvalidItemIndex,
    InvalidListText,
    InvalidFieldCount,
    InvalidEnabledField,
    UnsafeText,
    ReplacementCharacter,
    MojibakeSignal
}

public enum LegacyPListAuditRoute
{
    ExistingClosedMapper,
    IndustryFixedClosedMapper,
    ComparisonFreshIdentity,
    ForeignIndexCandleFreshIdentity,
    NxtThemeFreshIdentity,
    KrxThemeFreshIdentity,
    ForeignStockFreshIdentity,
    ManualFreshMaterialization,
    Unmapped
}

public sealed record LegacyPListAuditParseResult(
    LegacyPListAuditRow? Row,
    LegacyPListAuditParseFailure? Failure)
{
    public bool IsSuccess => Row is not null && Failure is null;
}

/// <summary>
/// Parser used only for the read-only production-data audit. It intentionally
/// accepts the original PList grammar before any current scene mapping is
/// attempted and never includes source text in exception or diagnostic output.
/// </summary>
public static class LegacyPListReadAuditParser
{
    public const int MaximumProgramTitleLength = 128;
    public const int MaximumListTextLength = 4_000;
    public const int MaximumItems = LegacyNamedPlaylistPersistenceService.MaximumItems;

    public static LegacyPListAuditParseResult Parse(
        string? programCode,
        string? programTitle,
        string? itemIndex,
        string? listText)
    {
        if (programCode is null || programCode.Length != 8 ||
            programCode.Any(static value => !char.IsAsciiDigit(value)))
        {
            return Failed(LegacyPListAuditParseFailure.InvalidProgramCode);
        }

        if (!IsCanonicalText(programTitle, MaximumProgramTitleLength, allowEmpty: false))
        {
            return Failed(programTitle?.Contains('\uFFFD') == true
                ? LegacyPListAuditParseFailure.ReplacementCharacter
                : ContainsMojibakeSignal(programTitle)
                    ? LegacyPListAuditParseFailure.MojibakeSignal
                    : LegacyPListAuditParseFailure.InvalidProgramTitle);
        }

        if (itemIndex is null ||
            !int.TryParse(
                itemIndex,
                NumberStyles.None,
                CultureInfo.InvariantCulture,
                out var parsedIndex) ||
            parsedIndex is < 0 or >= MaximumItems ||
            !string.Equals(
                itemIndex,
                parsedIndex.ToString(CultureInfo.InvariantCulture),
                StringComparison.Ordinal))
        {
            return Failed(LegacyPListAuditParseFailure.InvalidItemIndex);
        }

        if (listText is null || listText.Length is < 1 or > MaximumListTextLength)
        {
            return Failed(LegacyPListAuditParseFailure.InvalidListText);
        }

        if (listText.Contains('\uFFFD'))
        {
            return Failed(LegacyPListAuditParseFailure.ReplacementCharacter);
        }

        if (ContainsMojibakeSignal(listText))
        {
            return Failed(LegacyPListAuditParseFailure.MojibakeSignal);
        }

        if (!IsSafeText(listText))
        {
            return Failed(LegacyPListAuditParseFailure.UnsafeText);
        }

        var fields = listText.Split('^', StringSplitOptions.None);
        if (fields.Length != 7)
        {
            return Failed(LegacyPListAuditParseFailure.InvalidFieldCount);
        }

        if (fields[0] is not ("0" or "1"))
        {
            return Failed(LegacyPListAuditParseFailure.InvalidEnabledField);
        }

        // A caret cannot occur inside a field after an exact seven-way split.
        // Retain empty strings and every code unit exactly as the original row.
        if (fields.Any(static field => !IsSafeText(field)))
        {
            return Failed(LegacyPListAuditParseFailure.UnsafeText);
        }

        var frozenFields = Array.AsReadOnly(fields);
        var row = new LegacyPListAuditRow(
            programCode,
            programTitle!,
            parsedIndex,
            fields[0] == "1",
            frozenFields,
            listText,
            fields[5].Replace(" ", string.Empty, StringComparison.Ordinal));
        return row.IsLossless
            ? new LegacyPListAuditParseResult(row, Failure: null)
            : Failed(LegacyPListAuditParseFailure.InvalidListText);
    }

    public static bool ContainsMojibakeSignal(string? value)
    {
        if (string.IsNullOrEmpty(value))
        {
            return false;
        }

        // U+FFFD is reported separately. These code points and byte-order
        // marker are reliable signs of UTF-8/legacy-codepage damage in the
        // Korean operator data. Literal question marks are not rejected because
        // they are valid operator text and are not proof of corruption alone.
        foreach (var character in value)
        {
            if (character is '\u00C2' or '\u00C3' or '\u00EC' or '\u00ED' or
                '\u00EA' or '\u00EB' or '\uFEFF')
            {
                return true;
            }
        }

        return value.Contains("â€", StringComparison.Ordinal) ||
            value.Contains("ï»¿", StringComparison.Ordinal);
    }

    private static LegacyPListAuditParseResult Failed(
        LegacyPListAuditParseFailure failure) =>
        new(Row: null, failure);

    private static bool IsCanonicalText(
        string? value,
        int maximumLength,
        bool allowEmpty)
    {
        if (value is null || value.Length > maximumLength ||
            (!allowEmpty && value.Length == 0) ||
            !string.Equals(value, value.Trim(), StringComparison.Ordinal) ||
            value.Contains('\uFFFD') || ContainsMojibakeSignal(value))
        {
            return false;
        }

        return IsSafeText(value);
    }

    private static bool IsSafeText(string value)
    {
        for (var index = 0; index < value.Length; index++)
        {
            if (char.IsHighSurrogate(value[index]))
            {
                if (index + 1 >= value.Length || !char.IsLowSurrogate(value[index + 1]))
                {
                    return false;
                }

                index++;
            }
            else if (char.IsLowSurrogate(value[index]))
            {
                return false;
            }
        }

        foreach (var rune in value.EnumerateRunes())
        {
            var category = Rune.GetUnicodeCategory(rune);
            if (Rune.IsControl(rune) || category is
                UnicodeCategory.Format or
                UnicodeCategory.Surrogate or
                UnicodeCategory.LineSeparator or
                UnicodeCategory.ParagraphSeparator)
            {
                return false;
            }
        }

        return true;
    }
}

/// <summary>
/// Side-effect-free evidence checks shared by the database smoke audit and its
/// fixtures. This is deliberately a route classifier, not a resolver: routes
/// that require current database or trusted-file evidence remain unplayable
/// until the corresponding service reports a unique materialization result.
/// </summary>
public static class LegacyPListReadAuditEvidence
{
    public static bool HasContiguousIndexes(IReadOnlyList<LegacyPListAuditRow> rows)
    {
        ArgumentNullException.ThrowIfNull(rows);
        for (var index = 0; index < rows.Count; index++)
        {
            if (rows[index] is null || rows[index].ItemIndex != index)
            {
                return false;
            }
        }

        return true;
    }

    public static bool MatchesPersistenceResult(
        LegacyPListAuditRow raw,
        NamedPlaylistStoredItem stored)
    {
        ArgumentNullException.ThrowIfNull(raw);
        ArgumentNullException.ThrowIfNull(stored);
        return raw.ItemIndex == stored.ItemIndex &&
            raw.IsEnabled == stored.IsEnabled &&
            string.Equals(raw.GroupCode, stored.Selection.GroupCode, StringComparison.Ordinal) &&
            string.Equals(raw.Subject, stored.Selection.Subject, StringComparison.Ordinal) &&
            string.Equals(raw.GraphicType, stored.Selection.GraphicType, StringComparison.Ordinal) &&
            string.Equals(raw.Subtype, stored.Selection.Subtype, StringComparison.Ordinal) &&
            string.Equals(raw.DataCode, stored.Selection.DataCode, StringComparison.Ordinal) &&
            string.Equals(
                raw.PageTextForDisplay,
                stored.Page?.ToString() ?? string.Empty,
                StringComparison.Ordinal);
    }

    public static LegacyPListAuditRoute ClassifyRoute(LegacyPListAuditRow row)
    {
        ArgumentNullException.ThrowIfNull(row);
        var comparison = ToComparisonRow(row);
        if (LegacyNamedComparisonRestoreService.IsCandidate(comparison))
        {
            return LegacyPListAuditRoute.ComparisonFreshIdentity;
        }

        var foreignIndex = ToForeignIndexCandleRow(row);
        if (LegacyForeignIndexCandleRestoreService.IsCandidate(foreignIndex))
        {
            return LegacyPListAuditRoute.ForeignIndexCandleFreshIdentity;
        }

        var nxt = ToNxtThemeRow(row);
        if (LegacyNamedNxtThemeRestoreService.IsCandidate(nxt))
        {
            return LegacyPListAuditRoute.NxtThemeFreshIdentity;
        }

        if (IsIndustryFixed(row))
        {
            return LegacyPListAuditRoute.IndustryFixedClosedMapper;
        }

        if (string.Equals(row.GroupCode, "테마", StringComparison.Ordinal))
        {
            return LegacyPListAuditRoute.KrxThemeFreshIdentity;
        }

        if (row.GroupCode == "해외종목")
        {
            return LegacyPListAuditRoute.ForeignStockFreshIdentity;
        }

        // The identity-bearing canonical format is already handled by the
        // current overseas workflow. Only its one unambiguous current-price
        // action is a closed seven-field mapping; the five candle periods share
        // the same selection fields and therefore remain deliberately unmapped.
        if (row.GroupCode == "FOREIGN_STOCK")
        {
            return IsCanonicalForeignStockCurrent(row)
                ? LegacyPListAuditRoute.ExistingClosedMapper
                : LegacyPListAuditRoute.Unmapped;
        }

        if (IsManualCandidate(row))
        {
            return LegacyPListAuditRoute.ManualFreshMaterialization;
        }

        return IsExistingClosedMapper(row)
            ? LegacyPListAuditRoute.ExistingClosedMapper
            : LegacyPListAuditRoute.Unmapped;
    }

    public static LegacyNamedComparisonRestoreRow ToComparisonRow(LegacyPListAuditRow row) =>
        new(
            row.ItemIndex,
            row.IsEnabled,
            row.GroupCode,
            row.Subject,
            row.GraphicType,
            row.Subtype,
            row.PageTextForDisplay,
            row.DataCode);

    public static LegacyForeignIndexCandleRestoreRow ToForeignIndexCandleRow(
        LegacyPListAuditRow row) =>
        new(
            row.ItemIndex,
            row.IsEnabled,
            row.GroupCode,
            row.Subject,
            row.GraphicType,
            row.Subtype,
            row.PageTextForDisplay,
            row.DataCode);

    public static LegacyNamedNxtThemeRestoreRow ToNxtThemeRow(LegacyPListAuditRow row) =>
        new(
            row.ItemIndex,
            row.IsEnabled,
            row.GroupCode,
            row.Subject,
            row.GraphicType,
            row.Subtype,
            row.PageTextForDisplay,
            row.DataCode);

    private static bool IsIndustryFixed(LegacyPListAuditRow row)
    {
        if (row.Subtype.Length != 0 || row.DataCode.Length != 0 ||
            row.GraphicType is not ("5단 표그래프" or "네모그래프" or "섹터지수"))
        {
            return false;
        }

        return row.GroupCode switch
        {
            "업종_코스피" => string.Equals(row.Subject, "코스피", StringComparison.Ordinal),
            "업종_코스닥" => string.Equals(row.Subject, "코스닥", StringComparison.Ordinal),
            _ => false
        };
    }

    private static bool IsManualCandidate(LegacyPListAuditRow row) =>
        LegacyNamedManualRestoreClassifier.Classify(row) is not null;

    private static bool IsCanonicalForeignStockCurrent(LegacyPListAuditRow row)
    {
        if (row.PageTextForDisplay != "1/1" ||
            row.Subject.Length is < 1 or > 200 ||
            row.Subject != row.Subject.Trim() ||
            row.Subject.Contains('|') ||
            row.GraphicType != "1열판기본" ||
            row.Subtype != "CURRENT")
        {
            return false;
        }

        var parts = row.DataCode.Split('|', StringSplitOptions.None);
        return parts.Length == 3 &&
            LegacyNamedForeignStockRestoreService.IsCanonicalBoundSymbol(parts[0]) &&
            parts[1] is "US" or "TW" &&
            parts[2] == "1";
    }

    private static bool IsExistingClosedMapper(LegacyPListAuditRow row) =>
        IsLegacyStock(row) || IsLegacyExpert(row) || IsCanonicalDefault(row);

    private static bool IsLegacyStock(LegacyPListAuditRow row)
    {
        if (row.PageTextForDisplay != "1/1" || row.Subject.Length is < 1 or > 128 ||
            row.DataCode.Length is < 1 or > 32 ||
            row.DataCode.Any(static value => !char.IsAsciiLetterOrDigit(value)))
        {
            return false;
        }

        var isNxt = row.GroupCode is "코스피_NXT" or "코스닥_NXT";
        if (row.GroupCode is not ("코스피" or "코스닥" or "코스피_NXT" or "코스닥_NXT"))
        {
            return false;
        }

        var label = row.Subtype.Length == 0
            ? row.GraphicType
            : string.Concat(row.GraphicType, "_", row.Subtype);
        if (isNxt)
        {
            return label == "1열판기본_현재가";
        }

        return StockLegacyLabels.Contains(label);
    }

    private static bool IsLegacyExpert(LegacyPListAuditRow row) =>
        row.PageTextForDisplay.StartsWith("1/", StringComparison.Ordinal) &&
        row.GroupCode == "전문가 추천" && row.Subject.Length is > 0 and <= 128 &&
        row.GraphicType == "5단 표그래프" && row.Subtype == "현재가" &&
        row.DataCode.Length == 4 && row.DataCode.All(char.IsAsciiDigit);

    private static bool IsCanonicalDefault(LegacyPListAuditRow row) =>
        CanonicalDefaults.Contains(new SelectionKey(
            row.GroupCode,
            row.Subject,
            row.GraphicType,
            row.Subtype,
            row.DataCode));

    private static readonly HashSet<string> StockLegacyLabels = new(StringComparer.Ordinal)
    {
        "1열판기본_예상체결가",
        "1열판기본_현재가",
        "1열판기본_시간외단일가",
        "1열판상세_시가",
        "1열판상세_액면가",
        "1열판상세_PBR",
        "1열판상세_거래량",
        "수익률그래프_5일",
        "수익률그래프_20일",
        "수익률그래프_60일",
        "수익률그래프_120일",
        "수익률그래프_240일",
        "캔들그래프_일봉",
        "캔들그래프_5일",
        "캔들그래프_20일",
        "캔들그래프_60일",
        "캔들그래프_120일",
        "캔들그래프_240일",
        "캔들그래프(거래량)_일봉",
        "캔들그래프(거래량)_5일",
        "캔들그래프(거래량)_20일",
        "캔들그래프(거래량)_60일",
        "캔들그래프(거래량)_120일",
        "캔들그래프(거래량)_240일",
        "캔들그래프(예상체결가)_5일",
        "캔들그래프(예상체결가)_20일",
        "캔들그래프(예상체결가)_60일",
        "캔들그래프(예상체결가)_120일",
        "캔들그래프(예상체결가)_240일",
        "호가창_표그래프",
        "거래원_표그래프"
    };

    private static readonly HashSet<SelectionKey> CanonicalDefaults = new()
    {
        new("ALL", "INDEX", string.Empty, "UnitedStatesIndices", string.Empty),
        new("ALL", "INDEX", string.Empty, "Cotton", string.Empty),
        new("KOSPI", "INDEX", "KOSPI_TRADING_TREND", "TABLE_GRAPH", string.Empty),
        new("KOSPI", "INDEX", "KOSPI_TRADING_TREND", "BAR_GRAPH", string.Empty),
        new("INDEX", "FUTURES,Kospi", string.Empty, "CURRENT", string.Empty),
        new("FOREIGN_INDEX", "DOW", "US_SECTOR_INDEX", string.Empty, string.Empty),
        new("ALL", "INDEX", string.Empty, string.Empty, string.Empty),
        new("ALL", "INDEX", "INDIVIDUAL_TRADING_TREND", "LINE_GRAPH", string.Empty),
        new("KOSPI", "INDEX", "KOSPI_TRADING_TREND", "LINE_GRAPH", string.Empty),
        new("ALL", "INDEX", "PROGRAM_TRADING", "TABLE_GRAPH", string.Empty),
        new("EXCHANGE_RATE", "원달러", string.Empty, "20일", string.Empty),
        new("FOREIGN_INDEX", "DOW", string.Empty, "DowJones", string.Empty),
        new("ALL", "INDEX", "INSTITUTION_NET_BUY", "TABLE_GRAPH", string.Empty),
        new("ALL", "KOSPI", string.Empty, string.Empty, string.Empty),
        new("INDEX", "Kospi,Kosdaq", string.Empty, "CURRENT", string.Empty)
    };

    private readonly record struct SelectionKey(
        string GroupCode,
        string Subject,
        string GraphicType,
        string Subtype,
        string DataCode);
}
