using System.Security.Cryptography;
using Deal.Modules.Kanban.Application.Abstractions;
using Deal.Modules.Kanban.Application.Extensions;
using Deal.Modules.Kanban.Application.Models;
using Deal.Modules.Kanban.Application.Registrars;
using Deal.Modules.Kanban.Application.Services;
namespace Deal.Infrastructure.Integrations;
// Общая запись обучающего сигнала в очередь MlOutbox (ml_client.push L40–49) для адаптеров IMlClient.
// Поведение 1:1 с прототипом и с LocalMlClient.PushAsync этапа 3: пустые после trim text/label —
// тихий no-op, text обрезается до 6000 символов (без разрыва суррогатной пары), id — mle_ +
// 12 случайных hex (store.uid L48). Обучение идёт ВСЕГДА (выключатель mlEnabled его не трогает) —
// и в Local-, и в gRPC-режиме сигнал сначала пишется в outbox, отправку в ml-service делает фоновый
// MlOutboxFlushScheduler (Ruling 6: PushAsync ВСЕГДА пишет MlOutbox).
internal static class MlOutboxQueue
{
// Максимальная длина текста обучающего примера (ml_client.push L48: text[:6000]).
internal const int MaxLearningTextLength = 6000;
// Случайный хвост id outbox: 6 байт → 12 hex-символов (прототип store.uid — uuid4().hex[:12]).
private const int OutboxIdRandomBytes = 6;
///
/// Пишет строку очереди обучения: trim text/label (пустые — no-op), text[:6000], id mle_+hex.
///
/// Хранилище обучения (таблица MlOutbox схемы тенанта).
/// Текст обучающего примера (source_msg карточки или title).
/// Метка: id доски (b_...), spam либо t:hire|t:order.
/// Вес сигнала (1.0 — учить, −1.0 — снять метку).
/// Токен отмены.
/// Задача завершается после записи строки (отправку делает фоновый флашер).
public static async Task PushAsync(IMlLearningStore learningStore, string text, string label, double delta, CancellationToken ct)
{
string trimmedText = (text ?? string.Empty).Trim();
string trimmedLabel = (label ?? string.Empty).Trim();
if (trimmedText.Length == 0 || trimmedLabel.Length == 0)
{
return;
}
await learningStore.AddOutboxAsync(
NewOutboxId(),
TruncateText(trimmedText),
trimmedLabel,
delta,
ct);
}
// Генерирует id строки outbox: префикс mle_ + 12 случайных hex-символов (прототип store.uid).
// Возвращает: Короткий id записи очереди.
private static string NewOutboxId()
=> KanbanIdPrefixes.MlOutbox + Convert.ToHexString(RandomNumberGenerator.GetBytes(OutboxIdRandomBytes)).ToLowerInvariant();
// Обрезает текст до MaxLearningTextLength символов, не разбивая суррогатную пару на конце.
// text: Текст (уже trim-нут).
// Возвращает: Первые 6000 символов (или весь текст, если короче).
// .NET-срез идёт по UTF-16-единицам и может разбить суррогатную пару; Python-срез прототипа
// (text[:6000]) режет по code points — хвостовой high-surrogate убираем, чтобы в БД не ушла «битая» пара.
private static string TruncateText(string text)
{
if (text.Length <= MaxLearningTextLength)
{
return text;
}
string cut = text[..MaxLearningTextLength];
return char.IsHighSurrogate(cut[^1]) ? cut[..^1] : cut;
}
}