using Deal.Api.Events;
using Deal.Api.Extensions;
using Deal.Api.Models;
using Deal.Api.Services;
using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Api.Dtos;
namespace Deal.Api.Endpoints;
///
/// Эндпоинты ИИ-предложений: POST /api/ai/suggest-columns и POST /api/ai/suggest-keywords
/// (план Task 14 L476–479; прототип dashboard_routes.py L395–409).
///
///
/// Контракт 1:1 с прототипом и api-map §3.2 L120–121: suggest-columns → {ok:true, created:N}
/// либо {ok:false, reason, cooldown?}; suggest-keywords → {ok:true, keywords:[…]} либо
/// {ok:false, reason}. Причины — мягкие ошибки (HTTP 200 с ok:false + reason), статусы 4xx/5xx
/// не мапятся. Оба требуют сессию: 401 {detail} без куки (как остальные эндпоинты контейнеров); порт
/// IColumnSuggester резолвится из RequestServices ПОСЛЕ проверки сессии (scoped на tenant-контекст
/// запроса). При успехе suggest-columns публикуется SSE-toast «ИИ предложил колонок: N — откройте и
/// решите» (sparkles, 1:1 с suggest.py L162); boards_changed НЕ шлём (Ruling 5: фронт перечитывает
/// доски сам после ok). Публикации — из эндпоинта (Ruling 5): без подписчиков — no-op.
///
public static class AiSuggestEndpoints
{
// Префикс группы AI-эндпоинтов этапа (роутер dashboard, prefix="/api"; пути L395/L406).
private const string AiGroupPrefix = "/api/ai";
// Путь предложения колонок (dashboard_routes.py L395).
private const string SuggestColumnsPath = "/suggest-columns";
// Путь предложения ключевых слов (dashboard_routes.py L404).
private const string SuggestKeywordsPath = "/suggest-keywords";
// OpenAPI-тег группы (в прототипе роутер dashboard — dashboard_routes.py).
private const string OpenApiTag = "dashboard";
// SSE-тип события тоста (Ruling 5; api.js слушает 'toast').
private const string ToastEventType = "toast";
// Текст тоста после успешных предложений колонок (suggest.py L162, 1:1).
private const string SuggestToastTextFormat = "ИИ предложил колонок: {0} — откройте и решите";
// Иконка тоста предложений колонок (sparkles, 1:1 с прототипом).
private const string SparklesIcon = "sparkles";
///
/// Регистрирует POST /api/ai/suggest-columns и POST /api/ai/suggest-keywords.
///
/// Построитель маршрутов приложения.
/// Построитель маршрутов для цепочки вызовов.
public static IEndpointRouteBuilder MapAiSuggestEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup(AiGroupPrefix).WithTags(OpenApiTag);
group.MapPost(SuggestColumnsPath, SuggestColumnsAsync);
group.MapPost(SuggestKeywordsPath, SuggestKeywordsAsync);
return app;
}
// POST /api/ai/suggest-columns: анализ «Неразобранного» и создание колонок-предложений.
// Ответ — результат порта 1:1: {ok:true, created:N} — доски suggested=true созданы (эндпоинт шлёт
// SSE-toast), {ok:false, reason} (+ cooldown) — мягкая причина (HTTP 200). Кулдаун/«мало карточек»/
// «похожие колонки уже есть» — за адаптером LocalColumnSuggester (Ruling 3).
private static async Task SuggestColumnsAsync(HttpContext context, CancellationToken ct)
{
if (!context.HasUser())
{
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
}
IColumnSuggester suggester = context.RequestServices.GetRequiredService();
SuggestColumnsResultDto result = await suggester.SuggestColumnsAsync(ct);
if (result.Ok)
{
SseBroker broker = context.RequestServices.GetRequiredService();
broker.Publish(
context.GetCurrentUser()!.TenantId,
ToastEventType,
new { text = string.Format(SuggestToastTextFormat, result.Created), icon = SparklesIcon });
}
return Results.Ok(result);
}
// POST /api/ai/suggest-keywords: слова-маркеры сферы по карточкам (настройки «Сфера и ключи»).
// Ответ — результат порта 1:1: {ok:true, keywords:[…]} (≤60) либо {ok:false, reason} (HTTP 200).
private static async Task SuggestKeywordsAsync(HttpContext context, CancellationToken ct)
{
if (!context.HasUser())
{
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
}
IColumnSuggester suggester = context.RequestServices.GetRequiredService();
SuggestKeywordsResultDto result = await suggester.SuggestKeywordsAsync(ct);
return Results.Ok(result);
}
}