using System.Globalization; using MBN_STOCK_WEBVIEW.Core.Playout.Scenes; namespace MBN_STOCK_WEBVIEW.LegacyApplication; public enum LegacyComparisonTargetKind { MarketTarget, KrxStock, NxtStock, WorldStock } public enum LegacyComparisonDomesticMarket { Kospi, Kosdaq } public enum LegacyComparisonSearchStatus { Idle, Loading, Ready, Error } public enum LegacyComparisonPairMoveDirection { Up, Down } /// /// Validated native comparison identity. Web code never constructs this type; adapters /// create it from trusted DB results and the controller only exposes a display projection. /// public sealed record LegacyComparisonTarget { private LegacyComparisonTarget( LegacyComparisonTargetKind kind, string targetId, string displayName, string? marketTarget = null, LegacyComparisonDomesticMarket? domesticMarket = null, string? stockName = null, string? stockCode = null, string? inputName = null, string? symbol = null, string? nation = null) { Kind = kind; TargetId = targetId; DisplayName = displayName; MarketTarget = marketTarget; DomesticMarket = domesticMarket; StockName = stockName; StockCode = stockCode; InputName = inputName; Symbol = symbol; Nation = nation; } public LegacyComparisonTargetKind Kind { get; } public string TargetId { get; } public string DisplayName { get; } public string? MarketTarget { get; } public LegacyComparisonDomesticMarket? DomesticMarket { get; } public string? StockName { get; } public string? StockCode { get; } public string? InputName { get; } public string? Symbol { get; } public string? Nation { get; } public static LegacyComparisonTarget CreateMarketTarget( string targetId, string displayName, string marketTarget) { LegacyComparisonValueGuard.RequireIdentifier(targetId, 64, nameof(targetId)); LegacyComparisonValueGuard.RequireText(displayName, 200, nameof(displayName)); LegacyComparisonValueGuard.RequireIdentifier(marketTarget, 64, nameof(marketTarget)); return new LegacyComparisonTarget( LegacyComparisonTargetKind.MarketTarget, targetId, displayName, marketTarget: marketTarget); } public static LegacyComparisonTarget CreateKrxStock( LegacyComparisonDomesticMarket market, string stockName, string stockCode) { ValidateDomesticStock(stockName, stockCode); var marketToken = market == LegacyComparisonDomesticMarket.Kospi ? "kospi" : "kosdaq"; return new LegacyComparisonTarget( LegacyComparisonTargetKind.KrxStock, $"krx:{marketToken}:{stockCode}", stockName, domesticMarket: market, stockName: stockName, stockCode: stockCode); } public static LegacyComparisonTarget CreateNxtStock( LegacyComparisonDomesticMarket market, string stockName, string stockCode) { ValidateDomesticStock(stockName, stockCode); var marketToken = market == LegacyComparisonDomesticMarket.Kospi ? "kospi" : "kosdaq"; return new LegacyComparisonTarget( LegacyComparisonTargetKind.NxtStock, $"nxt:{marketToken}:{stockCode}", $"{stockName}(NXT)", domesticMarket: market, stockName: stockName, stockCode: stockCode); } public static LegacyComparisonTarget CreateWorldStock( string inputName, string symbol, string nation) { LegacyComparisonValueGuard.RequireText(inputName, 200, nameof(inputName)); if (inputName.Contains(',', StringComparison.Ordinal)) { throw new ArgumentException("A world stock input name cannot contain a comma.", nameof(inputName)); } LegacyComparisonValueGuard.RequireSymbol(symbol, nameof(symbol)); var normalizedNation = LegacyComparisonValueGuard.RequireNation(nation, nameof(nation)); return new LegacyComparisonTarget( LegacyComparisonTargetKind.WorldStock, $"world:{normalizedNation}:{symbol}", inputName, inputName: inputName, symbol: symbol, nation: normalizedNation); } private static void ValidateDomesticStock(string stockName, string stockCode) { LegacyComparisonValueGuard.RequireText(stockName, 200, nameof(stockName)); if (stockName.Contains(',', StringComparison.Ordinal) || stockName.Contains("(NXT)", StringComparison.Ordinal)) { throw new ArgumentException( "A domestic stock name cannot contain a comma or the NXT display suffix.", nameof(stockName)); } LegacyComparisonValueGuard.RequireStockCode(stockCode, nameof(stockCode)); } } public sealed record LegacyComparisonStoredTargetReference( string Name, string Market); public interface ILegacyComparisonDataService { Task> SearchDomesticAsync( string query, int maximumResults, CancellationToken cancellationToken = default); Task> SearchWorldAsync( string query, int maximumResults, CancellationToken cancellationToken = default); Task ResolveStoredTargetAsync( LegacyComparisonStoredTargetReference target, CancellationToken cancellationToken = default); } /// /// File IO stays behind this boundary. A production adapter owns the fixed path and CP949 /// encoding; no UI request can provide a path or raw serialized row. /// public interface ILegacyComparisonPairStore { Task ReadCp949Async(CancellationToken cancellationToken = default); Task WriteCp949Async( IReadOnlyList rows, CancellationToken cancellationToken = default); } public sealed record LegacyComparisonPairWriteRow( int RowNumber, string FirstName, string FirstMarket, string SecondName, string SecondMarket); public sealed record LegacyComparisonTargetView( string SelectionId, string DisplayName, string Metadata, LegacyComparisonTargetKind Kind); public sealed record LegacyComparisonSearchSnapshot( string Query, LegacyComparisonSearchStatus Status, IReadOnlyList Results, string? SelectedResultId, int TotalRowCount, string ErrorMessage); public sealed record LegacyComparisonSavedPairView( string RowId, int RowNumber, string DisplayLabel, LegacyComparisonTargetView First, LegacyComparisonTargetView Second, bool IsSelected); public sealed record LegacyComparisonActionView( string ActionId, string Label, string Section, bool IsAvailable, string? UnavailableReason); public sealed record LegacyComparisonWorkflowSnapshot( long Revision, bool IsBusy, LegacyComparisonSearchSnapshot DomesticSearch, LegacyComparisonSearchSnapshot WorldSearch, IReadOnlyList FixedTargets, string? SelectedFixedTargetId, LegacyComparisonTargetView? FirstTarget, LegacyComparisonTargetView? SecondTarget, IReadOnlyList SavedPairs, string? SelectedPairId, IReadOnlyList Actions, string StatusMessage, LegacyOperatorStatusKind StatusKind); /// /// Generic, already validated playlist materialization. Only an action id crosses the UI /// command boundary; builder, alias, scene selection and hidden identities are produced here. /// public sealed record LegacyGenericPlaylistDraft( string DraftId, string BuilderKey, string CutCode, IReadOnlyList Aliases, string Title, string Detail, string Category, string Market, LegacySceneSelection Selection, bool IsEnabled, int FadeDuration, string Source, string ActionId, string? EffectiveMode, string? Warning) { public LegacyPlaylistEntry ToLegacyPlaylistEntry() => new(DraftId, CutCode, IsEnabled, FadeDuration, Selection); } internal static class LegacyComparisonValueGuard { public static void RequireText(string value, int maximumLength, string parameterName) { ArgumentNullException.ThrowIfNull(value, parameterName); if (value.Length is <= 0 || value.Length > maximumLength || !string.Equals(value, value.Trim(), StringComparison.Ordinal) || value.Contains('^') || value.Any(static character => { var category = CharUnicodeInfo.GetUnicodeCategory(character); return char.IsControl(character) || char.IsSurrogate(character) || character == '\uFFFD' || category is UnicodeCategory.Format or UnicodeCategory.LineSeparator or UnicodeCategory.ParagraphSeparator; })) { throw new ArgumentException("A canonical comparison text value is required.", parameterName); } } public static void RequireIdentifier(string value, int maximumLength, string parameterName) { ArgumentNullException.ThrowIfNull(value, parameterName); if (value.Length is <= 0 || value.Length > maximumLength || value.Any(static character => !char.IsAsciiLetterOrDigit(character) && character is not '-' and not '_')) { throw new ArgumentException("A safe comparison identifier is required.", parameterName); } } public static void RequireStockCode(string value, string parameterName) { ArgumentNullException.ThrowIfNull(value, parameterName); if (value.Length is <= 0 or > 32 || value.Any(static character => !char.IsAsciiLetterOrDigit(character) && character is not '.' and not '-' and not '_')) { throw new ArgumentException("A safe stock code is required.", parameterName); } } public static void RequireSymbol(string value, string parameterName) { ArgumentNullException.ThrowIfNull(value, parameterName); if (value.Length is <= 0 or > 64 || value.Any(static character => !char.IsAsciiLetterOrDigit(character) && character is not '@' and not '.' and not '_' and not ':' and not '-')) { throw new ArgumentException("A safe world symbol is required.", parameterName); } } public static string RequireNation(string value, string parameterName) { ArgumentNullException.ThrowIfNull(value, parameterName); var normalized = value.ToUpperInvariant(); if (normalized is not ("US" or "TW")) { throw new ArgumentException("Only the original US and TW universe is supported.", parameterName); } return normalized; } }