#nullable enable

using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;

namespace MMoneyCoderSharp.Data;

/// <summary>
/// Closed kinds understood by the named-playlist manual restore workflow. The
/// candidate deliberately has a non-data-bearing ToString implementation so an
/// audit failure cannot accidentally print an operator stock name or VI list.
/// </summary>
public enum LegacyNamedManualRestoreKind
{
    Financial,
    NetSell,
    Vi
}

public enum LegacyNamedManualFreshProofFailure
{
    None,
    FinancialSnapshotChanged,
    FinancialStockSearchTruncated,
    FinancialStockIdentityNotUnique,
    FinancialSelectionMismatch,
    NetSellSnapshotInvalid,
    ViSnapshotInvalid,
    ViSnapshotVersionChanged,
    ViHistoricalNamesChanged,
    ViPageCountChanged
}

public sealed class LegacyNamedManualRestoreCandidate
{
    internal LegacyNamedManualRestoreCandidate(
        int itemIndex,
        LegacyNamedManualRestoreKind kind,
        bool isHistorical,
        ManualFinancialScreenKind? financialScreen = null,
        StockMarket? market = null,
        string? stockName = null,
        S5025ManualAudience? audience = null,
        IReadOnlyList<string>? historicalViNames = null,
        string? expectedViVersion = null,
        int expectedPageCount = 1)
    {
        ItemIndex = itemIndex;
        Kind = kind;
        IsHistorical = isHistorical;
        FinancialScreen = financialScreen;
        Market = market;
        StockName = stockName;
        Audience = audience;
        HistoricalViNames = historicalViNames;
        ExpectedViVersion = expectedViVersion;
        ExpectedPageCount = expectedPageCount;
    }

    public int ItemIndex { get; }

    public LegacyNamedManualRestoreKind Kind { get; }

    public bool IsHistorical { get; }

    public ManualFinancialScreenKind? FinancialScreen { get; }

    public StockMarket? Market { get; }

    public string? StockName { get; }

    public S5025ManualAudience? Audience { get; }

    public IReadOnlyList<string>? HistoricalViNames { get; }

    public string? ExpectedViVersion { get; }

    public int ExpectedPageCount { get; }

    public override string ToString() => $"Named manual restore candidate ({Kind})";
}

/// <summary>
/// C# equivalent of Web/named-manual-restore-workflow.js classify(). It accepts
/// only the historical seven-field PList grammar and the canonical grammar
/// emitted after a fresh materialization. It performs no I/O.
/// </summary>
public static class LegacyNamedManualRestoreClassifier
{
    private const int ViPageSize = 5;
    private const int MaximumViItems = 100;

    public static LegacyNamedManualRestoreCandidate? Classify(LegacyPListAuditRow row)
    {
        ArgumentNullException.ThrowIfNull(row);
        return ClassifyNetSell(row) ?? ClassifyVi(row) ?? ClassifyFinancial(row);
    }

    private static LegacyNamedManualRestoreCandidate? ClassifyNetSell(
        LegacyPListAuditRow row)
    {
        if (row.PageTextForDisplay != "1/1" || row.DataCode.Length != 0)
        {
            return null;
        }

        if (TryCanonicalAudience(row.GroupCode, out var canonical) &&
            row.Subject == "INDEX" &&
            row.GraphicType == "MANUAL_NET_SELL" &&
            row.Subtype == "MANUAL_NET_SELL")
        {
            return new LegacyNamedManualRestoreCandidate(
                row.ItemIndex,
                LegacyNamedManualRestoreKind.NetSell,
                isHistorical: false,
                audience: canonical);
        }

        if (TryHistoricalAudience(row.GroupCode, out var historical) &&
            row.Subject == "지수" &&
            row.GraphicType == "순매도 상위" &&
            row.Subtype == "순매도 상위")
        {
            return new LegacyNamedManualRestoreCandidate(
                row.ItemIndex,
                LegacyNamedManualRestoreKind.NetSell,
                isHistorical: true,
                audience: historical);
        }

        return null;
    }

    private static LegacyNamedManualRestoreCandidate? ClassifyVi(
        LegacyPListAuditRow row)
    {
        if (!TryPageCount(row.PageTextForDisplay, out var pageCount))
        {
            return null;
        }

        if (row.GroupCode == "PAGED_VI" &&
            row.Subject == TrustedViManualReference.SubjectSentinel &&
            row.GraphicType == "INPUT_ORDER" &&
            row.Subtype == "CURRENT" &&
            TrustedViManualReference.IsRowVersion(row.DataCode))
        {
            return new LegacyNamedManualRestoreCandidate(
                row.ItemIndex,
                LegacyNamedManualRestoreKind.Vi,
                isHistorical: false,
                expectedViVersion: row.DataCode,
                expectedPageCount: pageCount);
        }

        if (row.GroupCode.Length != 0 ||
            row.GraphicType != "5단 표그래프" ||
            row.Subtype != "VI 발동(수동)" ||
            row.DataCode.Length != 0 ||
            !TryHistoricalViNames(row.Subject, out var names) ||
            pageCount != DivideRoundUp(names.Count, ViPageSize))
        {
            return null;
        }

        return new LegacyNamedManualRestoreCandidate(
            row.ItemIndex,
            LegacyNamedManualRestoreKind.Vi,
            isHistorical: true,
            historicalViNames: names,
            expectedPageCount: pageCount);
    }

    private static LegacyNamedManualRestoreCandidate? ClassifyFinancial(
        LegacyPListAuditRow row)
    {
        if (row.PageTextForDisplay != "1/1" ||
            row.Subtype.Length != 0 ||
            row.DataCode.Length != 0 ||
            !IsTrimmedVisible(row.Subject, 200))
        {
            return null;
        }

        var canonicalMarket = TryCanonicalMarket(row.GroupCode, out var canonical);
        var historicalMarket = TryHistoricalMarket(row.GroupCode, out var historical);
        foreach (var contract in ManualFinancialCutContracts.All)
        {
            if (canonicalMarket && row.GraphicType == contract.CanonicalGraphicType)
            {
                return Financial(row, contract.Screen, canonical, isHistorical: false);
            }

            if (historicalMarket && row.GraphicType == contract.OriginalGraphicType)
            {
                return Financial(row, contract.Screen, historical, isHistorical: true);
            }
        }

        return null;
    }

    private static LegacyNamedManualRestoreCandidate Financial(
        LegacyPListAuditRow row,
        ManualFinancialScreenKind screen,
        StockMarket market,
        bool isHistorical) =>
        new(
            row.ItemIndex,
            LegacyNamedManualRestoreKind.Financial,
            isHistorical,
            financialScreen: screen,
            market: market,
            stockName: row.Subject);

    private static bool TryCanonicalAudience(
        string value,
        out S5025ManualAudience audience)
    {
        audience = value switch
        {
            "INDIVIDUAL" => S5025ManualAudience.Individual,
            "FOREIGN" => S5025ManualAudience.Foreign,
            "INSTITUTION" => S5025ManualAudience.Institution,
            _ => default
        };
        return value is "INDIVIDUAL" or "FOREIGN" or "INSTITUTION";
    }

    private static bool TryHistoricalAudience(
        string value,
        out S5025ManualAudience audience)
    {
        audience = value switch
        {
            "개인 순매도 상위(수동)" => S5025ManualAudience.Individual,
            "외국인 순매도 상위(수동)" => S5025ManualAudience.Foreign,
            "기관 순매도 상위(수동)" => S5025ManualAudience.Institution,
            _ => default
        };
        return value is
            "개인 순매도 상위(수동)" or
            "외국인 순매도 상위(수동)" or
            "기관 순매도 상위(수동)";
    }

    private static bool TryCanonicalMarket(string value, out StockMarket market)
    {
        market = value switch
        {
            "KOSPI" => StockMarket.Kospi,
            "KOSDAQ" => StockMarket.Kosdaq,
            "NXT_KOSPI" => StockMarket.NxtKospi,
            "NXT_KOSDAQ" => StockMarket.NxtKosdaq,
            _ => default
        };
        return value is "KOSPI" or "KOSDAQ" or "NXT_KOSPI" or "NXT_KOSDAQ";
    }

    private static bool TryHistoricalMarket(string value, out StockMarket market)
    {
        market = value switch
        {
            "코스피" => StockMarket.Kospi,
            "코스닥" => StockMarket.Kosdaq,
            "코스피_NXT" => StockMarket.NxtKospi,
            "코스닥_NXT" => StockMarket.NxtKosdaq,
            _ => default
        };
        return value is "코스피" or "코스닥" or "코스피_NXT" or "코스닥_NXT";
    }

    private static bool TryPageCount(string value, out int pageCount)
    {
        pageCount = 0;
        if (!value.StartsWith("1/", StringComparison.Ordinal) ||
            !int.TryParse(value.AsSpan(2), out var parsed) ||
            parsed is < 1 or > 20 ||
            value != $"1/{parsed}")
        {
            return false;
        }

        pageCount = parsed;
        return true;
    }

    private static bool TryHistoricalViNames(
        string value,
        out IReadOnlyList<string> names)
    {
        names = Array.Empty<string>();
        if (!IsTrimmedVisible(value, LegacyPListReadAuditParser.MaximumListTextLength))
        {
            return false;
        }

        var values = value.Split(',', StringSplitOptions.None);
        if (values.Length is < 1 or > MaximumViItems ||
            values.Any(name => !IsTrimmedVisible(name, 200)))
        {
            return false;
        }

        names = Array.AsReadOnly(values);
        return true;
    }

    private static bool IsTrimmedVisible(string value, int maximumLength) =>
        value.Length is > 0 &&
        value.Length <= maximumLength &&
        value == value.Trim() &&
        !value.Any(char.IsControl) &&
        !value.Contains('\uFFFD');

    private static int DivideRoundUp(int value, int divisor) =>
        checked((value + divisor - 1) / divisor);
}

/// <summary>
/// Pure checks for the evidence gathered through production read boundaries.
/// No method opens a database or file and no result contains source text.
/// </summary>
public static class LegacyNamedManualFreshProof
{
    private const int ViPageSize = 5;

    public static string FinancialStockSearchQuery(
        LegacyNamedManualRestoreCandidate candidate)
    {
        RequireKind(candidate, LegacyNamedManualRestoreKind.Financial);
        var stockName = candidate.StockName!;
        if (candidate.Market is not (StockMarket.NxtKospi or StockMarket.NxtKosdaq))
        {
            return stockName;
        }

        const string suffix = "(NXT)";
        if (!stockName.EndsWith(suffix, StringComparison.Ordinal) ||
            stockName.Length == suffix.Length)
        {
            throw new ArgumentException(
                "An NXT manual-financial identity requires its original display suffix.",
                nameof(candidate));
        }

        var query = stockName[..^suffix.Length];
        if (query.Length == 0 || query != query.Trim() || query.Contains(suffix))
        {
            throw new ArgumentException(
                "An NXT manual-financial identity has an invalid search identity.",
                nameof(candidate));
        }

        return query;
    }

    public static LegacyNamedManualFreshProofFailure EvaluateFinancial(
        LegacyNamedManualRestoreCandidate candidate,
        ManualFinancialSnapshot firstRead,
        ManualFinancialSnapshot secondRead,
        StockSearchResult stockSearch)
    {
        RequireKind(candidate, LegacyNamedManualRestoreKind.Financial);
        ArgumentNullException.ThrowIfNull(firstRead);
        ArgumentNullException.ThrowIfNull(secondRead);
        ArgumentNullException.ThrowIfNull(stockSearch);

        var identity = new ManualFinancialIdentity(
            candidate.FinancialScreen!.Value,
            candidate.StockName!);
        if (firstRead.Record.Identity != identity ||
            secondRead.Record.Identity != identity ||
            !string.Equals(firstRead.RowVersion, secondRead.RowVersion, StringComparison.Ordinal))
        {
            return LegacyNamedManualFreshProofFailure.FinancialSnapshotChanged;
        }

        if (stockSearch.IsTruncated ||
            !string.Equals(
                stockSearch.Query,
                FinancialStockSearchQuery(candidate),
                StringComparison.Ordinal))
        {
            return LegacyNamedManualFreshProofFailure.FinancialStockSearchTruncated;
        }

        var matches = stockSearch.Items
            .Where(item =>
                item.Market == candidate.Market &&
                string.Equals(item.Name, candidate.StockName, StringComparison.Ordinal))
            .ToArray();
        if (matches.Length != 1)
        {
            return LegacyNamedManualFreshProofFailure.FinancialStockIdentityNotUnique;
        }

        try
        {
            var verified = ManualFinancialCutContracts.VerifyStock(
                identity,
                stockSearch.Items,
                candidate.Market!.Value,
                matches[0].Code);
            var selection = ManualFinancialCutContracts.CreateSelection(secondRead, verified);
            var contract = ManualFinancialCutContracts.Get(identity.Screen);
            if (selection.BuilderKey != contract.BuilderKey ||
                selection.CutCode != contract.CutCode ||
                selection.CurrentPage != 1 ||
                selection.TotalPages != 1 ||
                selection.Selection.GroupCode != CanonicalMarket(candidate.Market.Value) ||
                selection.Selection.Subject != candidate.StockName ||
                selection.Selection.GraphicType != contract.CanonicalGraphicType ||
                selection.Selection.Subtype.Length != 0 ||
                selection.Selection.DataCode.Length != 0)
            {
                return LegacyNamedManualFreshProofFailure.FinancialSelectionMismatch;
            }
        }
        catch (ArgumentException)
        {
            return LegacyNamedManualFreshProofFailure.FinancialStockIdentityNotUnique;
        }

        return LegacyNamedManualFreshProofFailure.None;
    }

    public static LegacyNamedManualFreshProofFailure EvaluateNetSell(
        LegacyNamedManualRestoreCandidate candidate,
        IReadOnlyList<S5025TrustedManualRow> rows)
    {
        RequireKind(candidate, LegacyNamedManualRestoreKind.NetSell);
        if (rows is null || rows.Count != 5 || rows.Any(row =>
                row is null ||
                !ValidManualCell(row.LeftName) ||
                !ValidManualCell(row.LeftAmount) ||
                !ValidManualCell(row.RightName) ||
                !ValidManualCell(row.RightAmount)))
        {
            return LegacyNamedManualFreshProofFailure.NetSellSnapshotInvalid;
        }

        return LegacyNamedManualFreshProofFailure.None;
    }

    public static LegacyNamedManualFreshProofFailure EvaluateVi(
        LegacyNamedManualRestoreCandidate candidate,
        TrustedViStockNameSnapshot snapshot)
    {
        RequireKind(candidate, LegacyNamedManualRestoreKind.Vi);
        if (snapshot is null ||
            !TrustedViManualReference.IsRowVersion(snapshot.RowVersion) ||
            snapshot.StockNames is null ||
            snapshot.StockNames.Count is < 1 or > TrustedViManualReference.MaximumItemCount ||
            snapshot.StockNames.Any(name =>
                name is null ||
                name.Length is < 1 or > 200 ||
                name != name.Trim() ||
                name.Contains(',') ||
                name.Any(char.IsControl)))
        {
            return LegacyNamedManualFreshProofFailure.ViSnapshotInvalid;
        }

        if (candidate.ExpectedViVersion is not null &&
            !string.Equals(
                candidate.ExpectedViVersion,
                snapshot.RowVersion,
                StringComparison.Ordinal))
        {
            return LegacyNamedManualFreshProofFailure.ViSnapshotVersionChanged;
        }

        if (candidate.HistoricalViNames is not null &&
            !candidate.HistoricalViNames.SequenceEqual(
                snapshot.StockNames,
                StringComparer.Ordinal))
        {
            return LegacyNamedManualFreshProofFailure.ViHistoricalNamesChanged;
        }

        var pageCount = checked((snapshot.StockNames.Count + ViPageSize - 1) / ViPageSize);
        return pageCount == candidate.ExpectedPageCount
            ? LegacyNamedManualFreshProofFailure.None
            : LegacyNamedManualFreshProofFailure.ViPageCountChanged;
    }

    private static void RequireKind(
        LegacyNamedManualRestoreCandidate candidate,
        LegacyNamedManualRestoreKind expected)
    {
        ArgumentNullException.ThrowIfNull(candidate);
        if (candidate.Kind != expected)
        {
            throw new ArgumentException("The manual restore candidate kind is invalid.", nameof(candidate));
        }
    }

    private static bool ValidManualCell(string value) =>
        value is not null &&
        value.Length <= 256 &&
        !value.Any(char.IsControl);

    private static string CanonicalMarket(StockMarket market) => market switch
    {
        StockMarket.Kospi => "KOSPI",
        StockMarket.Kosdaq => "KOSDAQ",
        StockMarket.NxtKospi => "NXT_KOSPI",
        StockMarket.NxtKosdaq => "NXT_KOSDAQ",
        _ => throw new ArgumentOutOfRangeException(nameof(market))
    };
}
