using System.Collections.ObjectModel;
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;

namespace MBN_STOCK_WEBVIEW.LegacyApplication;

public enum LegacyOverseasActionSource
{
    FixedIndex,
    Industry,
    Stock
}

public enum LegacyOverseasActionKind
{
    Current,
    Candle
}

public sealed class LegacyOverseasFixedIndexTarget
{
    internal LegacyOverseasFixedIndexTarget(
        string targetId,
        string displayName,
        string inputName,
        string symbol,
        string nationCode)
    {
        TargetId = targetId;
        DisplayName = displayName;
        InputName = inputName;
        Symbol = symbol;
        NationCode = nationCode;
    }

    public string TargetId { get; }

    public string DisplayName { get; }

    public string InputName { get; }

    public string Symbol { get; }

    public string NationCode { get; }

    public string ForeignDomesticCode => "0";
}

public sealed class LegacyOverseasActionDefinition
{
    internal LegacyOverseasActionDefinition(
        string actionId,
        string label,
        LegacyOverseasActionSource source,
        LegacyOverseasActionKind kind,
        string builderKey,
        string sceneAlias,
        int? periodDays)
    {
        ActionId = actionId;
        Label = label;
        Source = source;
        Kind = kind;
        BuilderKey = builderKey;
        SceneAlias = sceneAlias;
        Aliases = Array.AsReadOnly([sceneAlias]);
        PeriodDays = periodDays;
    }

    public string ActionId { get; }

    public string Label { get; }

    public LegacyOverseasActionSource Source { get; }

    public LegacyOverseasActionKind Kind { get; }

    public string BuilderKey { get; }

    public string SceneAlias { get; }

    public IReadOnlyList<string> Aliases { get; }

    public int PageSize => 0;

    public int? PeriodDays { get; }

    public bool SupportsMovingAverages => Kind == LegacyOverseasActionKind.Candle;
}

/// <summary>
/// Closed UC5 metadata. The original control exposed ten fixed world indices and twelve
/// buttons: five index candles, one US-industry current plate, and six US/TW stock actions.
/// Scene identities never come from Web input.
/// </summary>
public static class LegacyOverseasActionCatalog
{
    public const int FixedIndexTargetCount = 10;
    public const int ActionCount = 12;

    private static readonly IReadOnlyList<LegacyOverseasFixedIndexTarget> RegisteredTargets =
        Array.AsReadOnly<LegacyOverseasFixedIndexTarget>(
        [
            Target("dow-jones", "다우존스", "DJI@DJI", "US"),
            Target("nasdaq", "나스닥", "NAS@IXIC", "US"),
            Target("sp500", "S&P500", "SPI@SPX", "US"),
            Target("germany-dax-30", "독일 DAX 30", "XTR@DAX30", "DE"),
            Target("united-kingdom-ftse-100", "영국 FTSE 100", "LNS@FTSE100", "GB"),
            Target("france-cac-40", "프랑스 CAC 40", "PAS@CAC40", "FR"),
            Target("japan-nikkei", "일본 니케이", "NII@NI225", "JP"),
            Target("china-shanghai-composite", "중국 상해종합", "SHS@000001", "CN"),
            Target("hong-kong-hang-seng", "홍콩 항셍", "HSI@HSI", "HK"),
            Target("taiwan-weighted", "대만 가권", "TWS@TI01", "TW")
        ]);

    private static readonly IReadOnlyList<LegacyOverseasActionDefinition> RegisteredActions =
        Array.AsReadOnly(CreateActions());

    private static readonly IReadOnlyDictionary<string, LegacyOverseasFixedIndexTarget>
        TargetsById = new ReadOnlyDictionary<string, LegacyOverseasFixedIndexTarget>(
            RegisteredTargets.ToDictionary(target => target.TargetId, StringComparer.Ordinal));

    private static readonly IReadOnlyDictionary<string, LegacyOverseasActionDefinition>
        ActionsById = new ReadOnlyDictionary<string, LegacyOverseasActionDefinition>(
            RegisteredActions.ToDictionary(action => action.ActionId, StringComparer.Ordinal));

    static LegacyOverseasActionCatalog()
    {
        ValidateTargets();
        ValidateActions();
    }

    public static IReadOnlyList<LegacyOverseasFixedIndexTarget> FixedIndexTargets =>
        RegisteredTargets;

    public static IReadOnlyList<LegacyOverseasActionDefinition> Actions => RegisteredActions;

    public static IReadOnlyList<LegacyOverseasActionDefinition> GetActions(
        LegacyOverseasActionSource source)
    {
        if (!Enum.IsDefined(source))
        {
            throw new ArgumentOutOfRangeException(nameof(source), source, "Unknown overseas source.");
        }

        return Array.AsReadOnly(
            RegisteredActions.Where(action => action.Source == source).ToArray());
    }

    public static LegacyOverseasFixedIndexTarget? FindFixedIndexTarget(string? targetId) =>
        targetId is not null && TargetsById.TryGetValue(targetId, out var target)
            ? target
            : null;

    public static LegacyOverseasFixedIndexTarget GetFixedIndexTarget(string targetId)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(targetId);
        return TargetsById.TryGetValue(targetId, out var target)
            ? target
            : throw new KeyNotFoundException("The overseas fixed-index target is not registered.");
    }

    public static LegacyOverseasActionDefinition? FindAction(string? actionId) =>
        actionId is not null && ActionsById.TryGetValue(actionId, out var action)
            ? action
            : null;

    public static LegacyOverseasActionDefinition GetAction(string actionId)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(actionId);
        return ActionsById.TryGetValue(actionId, out var action)
            ? action
            : throw new KeyNotFoundException("The overseas action is not registered.");
    }

    private static LegacyOverseasFixedIndexTarget Target(
        string targetId,
        string inputName,
        string symbol,
        string nationCode) =>
        new(targetId, inputName, inputName, symbol, nationCode);

    private static LegacyOverseasActionDefinition[] CreateActions() =>
    [
        Candle("index-candle-5d", "해외지수 캔들 5일", LegacyOverseasActionSource.FixedIndex, 5, "8061"),
        Candle("index-candle-20d", "해외지수 캔들 20일", LegacyOverseasActionSource.FixedIndex, 20, "8040"),
        Candle("index-candle-60d", "해외지수 캔들 60일", LegacyOverseasActionSource.FixedIndex, 60, "8046"),
        Candle("index-candle-120d", "해외지수 캔들 120일", LegacyOverseasActionSource.FixedIndex, 120, "8051"),
        Candle("index-candle-240d", "해외지수 캔들 240일", LegacyOverseasActionSource.FixedIndex, 240, "8056"),
        new(
            "industry-current",
            "미국 해외업종 현재가 1열판",
            LegacyOverseasActionSource.Industry,
            LegacyOverseasActionKind.Current,
            "s5001",
            "5001",
            periodDays: null),
        new(
            "stock-current",
            "해외종목 현재가 1열판",
            LegacyOverseasActionSource.Stock,
            LegacyOverseasActionKind.Current,
            "s5001",
            "5001",
            periodDays: null),
        Candle("stock-candle-5d", "해외종목 캔들 5일", LegacyOverseasActionSource.Stock, 5, "8061"),
        Candle("stock-candle-20d", "해외종목 캔들 20일", LegacyOverseasActionSource.Stock, 20, "8040"),
        Candle("stock-candle-60d", "해외종목 캔들 60일", LegacyOverseasActionSource.Stock, 60, "8046"),
        Candle("stock-candle-120d", "해외종목 캔들 120일", LegacyOverseasActionSource.Stock, 120, "8051"),
        Candle("stock-candle-240d", "해외종목 캔들 240일", LegacyOverseasActionSource.Stock, 240, "8056")
    ];

    private static LegacyOverseasActionDefinition Candle(
        string actionId,
        string label,
        LegacyOverseasActionSource source,
        int periodDays,
        string sceneAlias) =>
        new(
            actionId,
            label,
            source,
            LegacyOverseasActionKind.Candle,
            "s8010",
            sceneAlias,
            periodDays);

    private static void ValidateTargets()
    {
        if (RegisteredTargets.Count != FixedIndexTargetCount ||
            RegisteredTargets.Select(target => target.TargetId)
                .Distinct(StringComparer.Ordinal).Count() != FixedIndexTargetCount ||
            RegisteredTargets.Select(target => target.DisplayName)
                .Distinct(StringComparer.Ordinal).Count() != FixedIndexTargetCount ||
            RegisteredTargets.Select(target => target.Symbol)
                .Distinct(StringComparer.Ordinal).Count() != FixedIndexTargetCount ||
            RegisteredTargets.Any(target =>
                !IsSafeId(target.TargetId) ||
                !IsSafeCanonicalText(target.DisplayName, 200) ||
                !string.Equals(target.DisplayName, target.InputName, StringComparison.Ordinal) ||
                !IsSafeSymbol(target.Symbol) ||
                target.NationCode.Length != 2 ||
                target.NationCode.Any(character => character is < 'A' or > 'Z')))
        {
            throw new InvalidOperationException("The legacy overseas fixed-index catalog is invalid.");
        }
    }

    private static void ValidateActions()
    {
        if (RegisteredActions.Count != ActionCount ||
            RegisteredActions.Select(action => action.ActionId)
                .Distinct(StringComparer.Ordinal).Count() != ActionCount ||
            RegisteredActions.Any(action => !IsSafeId(action.ActionId)) ||
            RegisteredActions.Any(action => action.PageSize != 0) ||
            RegisteredActions.Any(action =>
                action.Kind == LegacyOverseasActionKind.Candle != action.PeriodDays.HasValue) ||
            RegisteredActions.Any(action =>
                action.Source == LegacyOverseasActionSource.Industry &&
                action.Kind != LegacyOverseasActionKind.Current) ||
            RegisteredActions.Any(action =>
                !LegacySceneCatalog.FindByCutAlias(action.SceneAlias).Any(scene =>
                    string.Equals(scene.BuilderKey, action.BuilderKey, StringComparison.Ordinal) &&
                    (int)scene.PageSize == action.PageSize)))
        {
            throw new InvalidOperationException("The legacy overseas action catalog is invalid.");
        }
    }

    private static bool IsSafeId(string value) =>
        value.Length is > 0 and <= 64 &&
        value.All(character => char.IsAsciiLetterOrDigit(character) || character == '-');

    private static bool IsSafeCanonicalText(string value, int maximumLength) =>
        value.Length is > 0 &&
        value.Length <= maximumLength &&
        string.Equals(value, value.Trim(), StringComparison.Ordinal) &&
        !value.Any(character => char.IsControl(character) || char.IsSurrogate(character));

    private static bool IsSafeSymbol(string value) =>
        IsSafeCanonicalText(value, 64) &&
        value.All(character =>
            char.IsAsciiLetterOrDigit(character) || character is '@' or '.' or '_' or '$' or ':' or ' ' or '-');
}
