using Deal.Api.Http;
using Deal.Modules.Tenants.Application.Abstractions;
using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
using Deal.Api.Services;
using Deal.Api.Dtos;
namespace Deal.Api.Endpoints;
///
/// Операторские эндпоинты лимитов ИИ-бюджета: сводка по всем тенантам и просмотр/смена лимита тенанта
/// (план Task 10, Ruling 3/11).
///
///
/// Все ручки — только оператору: без операторской сессии 401 «Требуется вход оператора» (как остальные
/// /api/operator/*). GET /api/operator/limits — сводка {items:[{tenantId, name, budget, period, used, percent,
/// status}]} по реестру тенантов (Ruling 3: строка лимита на путь чтения заводится лениво с дефолт-бюджетом —
/// тенант без расхода виден как «дефолт, 0»). GET/PATCH /api/operator/tenants/{id}/limit — детали/смена лимита:
/// PATCH принимает {budget?, period?} (оба опциональны — меняется только заданное; null-тело/без полей → 400),
/// сбрасывает Warned80/NotifiedExhausted через UpdateBudgetAsync (Ruling 3: смена бюджета открывает пороги
/// тостов заново) и пишет аудит tenant_limit_changed (только при реальном изменении — повторный PATCH с теми же
/// значениями идемпотентен, аудит не дублируется). Отрицательный бюджет/чужой период отсекаются 400 до вызова
/// хранилища; тенант проверяется по реестру (404 «Тенант не найден»). Ответы деталей — единая форма
/// (см. ) — статус тенанта, флаги порогов и процент расхода.
///
public static class OperatorLimitsEndpoints
{
// Текст 400: PATCH без полей (null-тело/пустой объект).
private const string EmptyUpdateDetail = "Укажите новый бюджет или период";
// Текст 400: бюджет отрицательный (порог лимита не позволяет).
private const string NegativeBudgetDetail = "Бюджет должен быть неотрицательным";
// Текст 400: период не month и не day (константы TenantLimitPeriods).
private const string InvalidPeriodDetail = "Период должен быть month или day";
// Текст 404: тенант с таким id не найден в реестре.
private const string TenantNotFoundDetail = "Тенант не найден";
// Верхняя граница процента расхода (диапазон 0..100) — константа расчёта CalculatePercent.
private const int PercentMax = 100;
// Префикс сводки лимитов (Ruling 11: /api/operator/*).
private const string OperatorGroupPrefix = "/api/operator";
// Префикс группы операторских ручек тенантов (общий с Task 7).
private const string TenantsGroupPrefix = "/api/operator/tenants";
// Путь сводки лимитов по всем тенантам.
private const string SummaryPath = "/limits";
// Относительный путь лимита тенанта (просмотр/смена).
private const string TenantLimitPath = "/{id:guid}/limit";
// OpenAPI-тег группы сводки лимитов.
private const string LimitsOpenApiTag = "operator-limits";
// Без состояния, поэтому безопасен как статический экземпляр (период-математика Task 8).
private static readonly TokenBudgetService BudgetService = new();
///
/// Регистрирует ручки лимитов: GET /api/operator/limits (сводка) и GET/PATCH
/// /api/operator/tenants/{id}/limit (детали/смена).
///
/// Построитель маршрутов приложения.
/// Построитель маршрутов для цепочки вызовов.
public static IEndpointRouteBuilder MapOperatorLimitsEndpoints(this IEndpointRouteBuilder app)
{
app.MapGroup(OperatorGroupPrefix).WithTags(LimitsOpenApiTag).MapGet(SummaryPath, ListSummaryAsync);
var tenantsGroup = app.MapGroup(TenantsGroupPrefix).WithTags(LimitsOpenApiTag);
tenantsGroup.MapGet(TenantLimitPath, GetLimitAsync);
tenantsGroup.MapPatch(TenantLimitPath, PatchLimitAsync);
return app;
}
// GET /api/operator/limits: сводка бюджета/расхода по всем тенантам (план Task 10).
private static async Task ListSummaryAsync(
HttpContext context,
ITenantRepository tenantRepository,
ITenantLimitStore limitStore,
CancellationToken ct)
{
var operatorIdentity = context.GetCurrentOperator();
if (operatorIdentity is null)
{
return EndpointResults.Unauthorized(AuthHelpers.OperatorUnauthorizedDetail);
}
IReadOnlyList tenants = await tenantRepository.ListAsync(ct);
var items = new List