using Deal.Modules.Kanban.Application.Models;
namespace Deal.Modules.Kanban.Application.ColumnRules;
///
/// Матчинг текста сообщения по правилам колонки
///
public static class ColumnMatcher
{
///
/// Соответствует ли текст правилам колонки.
///
/// Правила колонки (null/пустые → False: пустая колонка не матчит).
/// Текст сообщения/карточки.
/// Курсы для конвертации бюджета (см. ); null — если бюджет в другой валюте, суммы не конвертируются и группа бюджета не совпадает.
/// True — текст прошёл правила в режиме all/any.
public static bool MatchText(
ContainerRulesDto? rules,
string? text,
IReadOnlyDictionary? rates = null)
{
if (rules is null)
{
return false;
}
string lower = ContentNormalizer.ContentText(text ?? string.Empty).ToLowerInvariant();
IReadOnlyList keywords = NormalizeTerms(rules.Keywords);
IReadOnlyList stack = NormalizeTerms(rules.Stack);
IReadOnlyList direction = NormalizeTerms(rules.Direction);
IReadOnlyList grade = NormalizeTerms(rules.Grade);
IReadOnlyList gradeTerms = GradeAliases.ExpandTerms(grade);
IReadOnlyList levels = NormalizeTerms(rules.Levels);
IReadOnlyList levelTerms = GradeAliases.ExpandTerms(levels);
IReadOnlyList locations = NormalizeTerms(rules.Locations);
IReadOnlyList types = NormalizeTerms(rules.Types);
IReadOnlyList typeTerms = TypeAliases.ExpandTerms(types);
bool budgetEnabled = rules.Budget.HasBudget();
bool pricesEnabled = rules.Prices.HasBudget();
bool anyEnabled = keywords.Count > 0 || stack.Count > 0 || direction.Count > 0 || grade.Count > 0
|| levels.Count > 0 || locations.Count > 0 || types.Count > 0 || budgetEnabled || pricesEnabled;
if (!anyEnabled)
{
return false;
}
// Значения групп: пустая (выключенная) группа считается True, но в свёртку не входит.
bool keywordsOk = keywords.Count == 0 || ContainsAny(lower, keywords);
bool stackOk = stack.Count == 0 || ContainsAny(lower, stack);
bool directionOk = direction.Count == 0 || ContainsAny(lower, direction);
bool gradeOk = gradeTerms.Count == 0 || ContainsAny(lower, gradeTerms);
bool levelsOk = levelTerms.Count == 0 || ContainsAny(lower, levelTerms);
bool locationsOk = locations.Count == 0 || ContainsAny(lower, locations);
bool typesOk = typeTerms.Count == 0 || ContainsAny(lower, typeTerms);
bool budgetOk = !budgetEnabled || BudgetInRange.IsInRange(AmountParser.Parse(text), rules.Budget!, rates);
bool pricesOk = !pricesEnabled || BudgetInRange.IsInRange(AmountParser.Parse(text), rules.Prices!, rates);
if (string.Equals(rules.Mode, ColumnRuleModes.Any, StringComparison.OrdinalIgnoreCase))
{
// «любое»: совпасть должна хотя бы одна ВКЛЮЧЁННАЯ (непустая) группа.
return (keywords.Count > 0 && keywordsOk)
|| (stack.Count > 0 && stackOk)
|| (direction.Count > 0 && directionOk)
|| (grade.Count > 0 && gradeOk)
|| (levels.Count > 0 && levelsOk)
|| (locations.Count > 0 && locationsOk)
|| (types.Count > 0 && typesOk)
|| (budgetEnabled && budgetOk)
|| (pricesEnabled && pricesOk);
}
// all: каждая включённая группа обязана совпасть.
return (keywords.Count == 0 || keywordsOk)
&& (stack.Count == 0 || stackOk)
&& (direction.Count == 0 || directionOk)
&& (grade.Count == 0 || gradeOk)
&& (levels.Count == 0 || levelsOk)
&& (locations.Count == 0 || locationsOk)
&& (types.Count == 0 || typesOk)
&& (!budgetEnabled || budgetOk)
&& (!pricesEnabled || pricesOk);
}
///
/// Число совпавших ТЕРМОВ правил.
///
/// Правила колонки.
/// Текст сообщения.
/// Суммарный балл (0 — текст пуст или совпадений нет).
public static int ScoreText(ContainerRulesDto? rules, string? text)
{
if (rules is null || string.IsNullOrEmpty(text))
{
return 0;
}
string lower = ContentNormalizer.ContentText(text).ToLowerInvariant();
int score = 0;
score += CountMatchedTerms(lower, rules.Direction);
score += CountMatchedTerms(lower, rules.Keywords);
score += CountMatchedTerms(lower, rules.Stack);
score += CountMatchedTerms(lower, rules.Locations);
foreach (string alias in GradeAliases.ExpandTerms(NormalizeTerms(rules.Grade)))
{
if (lower.Contains(alias, StringComparison.Ordinal))
{
score += 1;
}
}
foreach (string alias in GradeAliases.ExpandTerms(NormalizeTerms(rules.Levels)))
{
if (lower.Contains(alias, StringComparison.Ordinal))
{
score += 1;
}
}
foreach (string alias in TypeAliases.ExpandTerms(NormalizeTerms(rules.Types)))
{
if (lower.Contains(alias, StringComparison.Ordinal))
{
score += 1;
}
}
return score;
}
///
/// Есть ли в правилах хотя бы одна реально работающая группа фильтров.
///
/// Правила колонки (null → False).
/// True — есть непустой терм группы или бюджет с границей.
public static bool HasActiveRules(ContainerRulesDto? rules)
{
if (rules is null)
{
return false;
}
return rules.Direction.HasAnyTerm()
|| rules.Keywords.HasAnyTerm()
|| rules.Stack.HasAnyTerm()
|| rules.Grade.HasAnyTerm()
|| rules.Levels.HasAnyTerm()
|| rules.Locations.HasAnyTerm()
|| rules.Types.HasAnyTerm()
|| (rules.Budget is not null && (rules.Budget.From is not null || rules.Budget.To is not null))
|| (rules.Prices is not null && (rules.Prices.From is not null || rules.Prices.To is not null));
}
// Совпал ли хотя бы один терм (подстрока в тексте).
// lower: Текст в нижнем регистре (очищен от ссылок).
// terms: Термы в нижнем регистре.
// Возвращает: True — есть совпадение подстрокой.
private static bool ContainsAny(string lower, IReadOnlyList terms)
{
foreach (string term in terms)
{
if (lower.Contains(term, StringComparison.Ordinal))
{
return true;
}
}
return false;
}
// Сколько термов списка совпало в тексте (для score).
// lower: Текст в нижнем регистре.
// rawTerms: Термы как сохранены (регистр не важен).
// Возвращает: Число совпавших термов.
private static int CountMatchedTerms(string lower, IReadOnlyList? rawTerms)
{
if (rawTerms is null)
{
return 0;
}
int count = 0;
foreach (string? raw in rawTerms)
{
string term = (raw ?? string.Empty).Trim().ToLowerInvariant();
if (term.Length > 0 && lower.Contains(term, StringComparison.Ordinal))
{
count += 1;
}
}
return count;
}
private static IReadOnlyList NormalizeTerms(IReadOnlyList? terms)
{
var result = new List();
if (terms is null)
{
return result;
}
foreach (string? raw in terms)
{
string term = (raw ?? string.Empty).Trim().ToLowerInvariant();
if (term.Length > 0)
{
result.Add(term);
}
}
return result;
}
}