using Deal.Modules.Kanban.Application.Models;
namespace Deal.Modules.Kanban.Application;
///
/// Единый порт хранилища карточек и контейнеров (таблицы Cards/Containers/LeadComments/CardMoves тенанта;
/// жёсткое удаление карточки дополнительно чистит строки DedupEntries — Ruling 3), Ruling 1.
///
///
/// Объявлен в модуле Kanban — владельце единой сущности карточки (этап 9); реализация — EF-адаптер
/// KanbanStore в Deal.Infrastructure (регистрация в AddDealPersistence). Порт покрывает оба
/// пространства одной таблицы Cards: дашборд (служебные зоны/доски) и «Выбранные» (контейнеры-стадии),
/// поэтому отдельный порт карточек «Выбранных» упразднён. Порт оперирует DTO модуля; маппинг
/// DTO ↔ строки (включая JSON-поля и human-метки времени, Ruling 10) выполняет адаптер вручную
/// (эталон SettingsStore.cs). Набор методов — ровно тот, что нужен сервису карточек (YAGNI):
/// контейнеры, карточки и переносы, комментарии, журнал CardMoves, кандидаты правил хранения (Ruling 8),
/// пересчёт конверсий (Ruling 7) и выборка «Неразобранного» для эвристики (Ruling 3).
/// Id записей генерирует модуль (Ruling 12: короткие префиксные id через KanbanIdPrefixes) и передаёт
/// готовыми — хранилище id не создаёт. Чтения — AsNoTracking; сортировки (received_at DESC и т.п.) —
/// обязанность адаптера. Удаление карточки навсегда (DeleteForeverAsync/ClearColAsync/PurgeAsync)
/// снимает и «мягкие» dedup-ссылки (Ruling 3): строки таблицы DedupEntries чужого модуля адаптер удаляет
/// напрямую тем же TenantDbContext, цикла Kanban → Pipeline не возникает (Kanban о Pipeline не знает).
///
public interface ICardStore
{
// ── Контейнеры (колонки/стадии/зоны) ─────────────────────────────────
///
/// Контейнеры пространства в порядке показа: ORDER BY space, position (этап 9, T4).
///
/// Пространство (dashboard/selected) либо null — все контейнеры.
/// Токен отмены.
/// Контейнеры (DTO, счётчики заполняет сервис); пусто — контейнеров нет.
public Task> ListContainersAsync(string? space, CancellationToken ct);
///
/// Один контейнер по id (PATCH 404-семантика, переносы и валидация колонок).
///
/// Id контейнера (inbox/archive/trash/стадия/b_...).
/// Токен отмены.
/// Контейнер или null, если строки нет.
public Task GetContainerAsync(string containerId, CancellationToken ct);
///
/// Создаёт контейнер (id/позицию/цвет/дефолты вычисляет ContainersService).
///
/// Полный контейнер для вставки, включая .
/// Токен отмены.
public Task CreateContainerAsync(ContainerDto container, CancellationToken ct);
///
/// Обновляет контейнер целиком (сервис читает Get + применяет ContainerPatchDto).
///
/// Контейнер с изменёнными полями (идентифицируется по Id).
/// Токен отмены.
public Task UpdateContainerAsync(ContainerDto container, CancellationToken ct);
///
/// Удаляет контейнер, предварительно перенося его карточки в «Неразобранное».
///
/// Id удаляемого контейнера.
/// Токен отмены.
/// Сколько карточек перенесено в inbox (0 — контейнера/карточек не было; ответ {ok, movedToInbox}).
public Task DeleteContainerAsync(string containerId, CancellationToken ct);
///
/// Переставляет контейнеры пространства: позиции 0..N-1 в порядке списка.
///
/// Пространство переставляемых контейнеров.
/// Id контейнеров в новом порядке.
/// Токен отмены.
public Task ReorderContainersAsync(string space, IReadOnlyList containerIds, CancellationToken ct);
// ── Карточки ──────────────────────────────────────────────────────────
///
/// Карточки колонки или всех колонок дашборда, ORDER BY received_at DESC (list_leads L151–156).
///
/// Фильтр: Col — конкретная колонка либо null (все колонки дашборда).
/// Токен отмены.
/// Полные карточки (DTO): JSON-поля разобраны, комментарии приложены, time посчитан.
public Task> ListCardsAsync(CardsQuery query, CancellationToken ct);
///
/// Карточки пространства «Выбранные», ORDER BY updated_at DESC (list_cards projects.py L58–63).
///
/// Фильтр по контейнеру-стадии (id каталога ); null — все стадии.
/// Токен отмены.
/// Полные карточки (DTO): JSON-поля разобраны, комментарии приложены, time посчитан.
public Task> ListSelectedCardsAsync(string? containerId, CancellationToken ct);
///
/// Полнотекстовый поиск карточек (FTS + LIKE-дополнение; leads.py search L509–551, Ruling 6/Task 12).
///
/// Поисковый запрос (уже Trim+lowercase, как в отсев-поиске; короче 2 символов сервис не пропускает).
/// Ограничение результата (эндпоинт шлёт 12, Ruling 6).
/// Токен отмены.
/// Полные карточки (комментарии приложены, time посчитан): FTS-кандидаты по
/// убыванию ts_rank (SearchTsv @@ plainto_tsquery), затем LIKE-дополнение, внутри — ReceivedAt DESC.
public Task> SearchCardsAsync(string q, int limit, CancellationToken ct);
///
/// Одна карточка по id (GET /api/cards/{id}, а также перечитывание после переноса).
///
/// Id карточки (c_...).
/// Токен отмены.
/// Карточка или null, если строки нет.
public Task GetCardAsync(string cardId, CancellationToken ct);
///
/// Карточка по исходному сообщению (диалог + id сообщения) — ручная разметка ML (§8).
///
/// Если сообщение давало несколько карточек (повторная разметка/пересоздание), берётся самая
/// свежая по ReceivedAt. Пустой dialogId — null (нечем искать).
/// Id диалога-источника сообщения.
/// Id исходного сообщения в Telegram.
/// Токен отмены.
/// Карточка или null, если сообщение не становилось карточкой.
public Task GetCardBySourceAsync(string dialogId, long msgId, CancellationToken ct);
///
/// Создаёт карточку из готового снимка (пайплайн этапа 4, ручное создание; CreatedAt — UTC-now).
///
/// Полное состояние новой карточки (id сгенерирован модулем, см. CardSnapshot).
/// Токен отмены.
public Task AddCardAsync(CardSnapshot snapshot, CancellationToken ct);
///
/// Точечная правка полей по присутствующим в патче + bump UpdatedAt (patch_card projects.py L159–187).
///
/// Id карточки (c_...).
/// Изменяемые поля (null — поле не меняется; JSON-поля — полная замена).
/// Токен отмены.
/// True — строка обновлена; false — карточки нет (404-семантика сервиса).
public Task PatchCardAsync(string cardId, CardPatch patch, CancellationToken ct);
///
/// Атомарно дописывает ссылку в JSON-массив links ОДНИМ UPDATE (jsonb-append) + bump UpdatedAt.
///
/// Id карточки (c_...).
/// Готовая ссылка {id,name,url} (id сгенерирован модулем).
/// Токен отмены.
/// True — строка обновлена; false — карточки нет (404-семантика сервиса).
public Task AddLinkAsync(string cardId, CardLinkDto link, CancellationToken ct);
///
/// Атомарно убирает из JSON-массива links элемент с указанным id ОДНИМ UPDATE (jsonb-фильтрация) + bump UpdatedAt.
///
/// Id карточки (c_...).
/// Id удаляемой ссылки (pl_...).
/// Токен отмены.
/// True — строка обновлена; false — карточки нет (404-семантика сервиса).
public Task RemoveLinkAsync(string cardId, string linkId, CancellationToken ct);
///
/// Атомарно дописывает метаданные файла в JSON-массив files ОДНИМ UPDATE (jsonb-append) + bump UpdatedAt.
///
/// Id карточки (c_...).
/// Готовые метаданные {id,name,size,kind,label,objectKey} (id сгенерирован модулем).
/// Токен отмены.
/// True — строка обновлена; false — карточки нет (404-семантика сервиса).
public Task AddFileAsync(string cardId, CardFileDto file, CancellationToken ct);
///
/// Атомарно убирает из JSON-массива files элемент с указанным id ОДНИМ UPDATE (jsonb-фильтрация) + bump UpdatedAt.
///
/// Id карточки (c_...).
/// Id удаляемой записи файла (pf_...).
/// Токен отмены.
/// True — строка обновлена; false — карточки нет (404-семантика сервиса).
public Task RemoveFileAsync(string cardId, string fileId, CancellationToken ct);
///
/// Смена контейнера-стадии: один UPDATE (col, reminder_at=NULL, reminder_fired=false, updated_at=atMs)
/// + перезапись history-массива с добавленной записью (move_stage projects.py L202–216; Ruling 7).
///
/// Id карточки (c_...).
/// Новый контейнер-стадия (валидирует сервис каталогом ).
/// Готовая запись истории {id,at,stage} (id сгенерирован модулем).
/// Время переноса, epoch-ms (пишется в updated_at и в запись истории).
/// Токен отмены.
/// True — строка обновлена; false — карточки нет (404-семантика сервиса).
public Task MoveCardStageAsync(string cardId, string containerId, CardHistoryDto historyEntry, long atMs, CancellationToken ct);
///
/// Устанавливает напоминание: reminder_at, reminder_fired=false + bump UpdatedAt (set_reminder projects.py L236–243).
///
/// Id карточки (c_...).
/// Время напоминания, epoch-ms.
/// Токен отмены.
public Task SetReminderAsync(string cardId, long atMs, CancellationToken ct);
///
/// Снимает напоминание: reminder_at=NULL, reminder_fired=false (clear_reminder projects.py L246–247).
///
/// Id карточки (c_...).
/// Токен отмены.
public Task ClearReminderAsync(string cardId, CancellationToken ct);
///
/// Полная ручная очистка контейнера-стадии: DELETE строк (clear_stage projects.py L223–231).
///
/// Очищаемая стадия (допустимость — только rejected — валидирует сервис).
/// Токен отмены.
/// Сколько карточек удалено (0 — стадия пуста).
public Task ClearStageAsync(string containerId, CancellationToken ct);
///
/// Наступившие напоминания стадии hold: ReminderAt ≤ now, ReminderFired=false, ORDER BY ReminderAt (check_reminders projects.py L270–275).
///
/// Текущий момент (UTC) для сравнения с ReminderAt.
/// Токен отмены.
/// Due-строки {id,title,containerId} в порядке наступления; пусто — сработавших нет.
public Task> ListDueRemindersAsync(DateTimeOffset now, CancellationToken ct);
///
/// Помечает due-карточки сработавшими: ReminderFired=true по списку id (check_reminders projects.py L277–278).
///
/// Id карточек, чьи напоминания «выстрелили».
/// Токен отмены.
public Task MarkRemindersFiredAsync(IReadOnlyList cardIds, CancellationToken ct);
///
/// Очищает протухшие напоминания при выключенной настройке: ReminderAt=NULL WHERE ReminderAt ≤ now (check_reminders L266–269).
///
/// Текущий момент (UTC).
/// Токен отмены.
/// Сколько строк очищено.
public Task ClearExpiredRemindersAsync(DateTimeOffset now, CancellationToken ct);
///
/// Меняет колонку/состояние карточки (move/trash/restore/автоархив) — см. CardColumnUpdateDto.
///
/// Новое состояние колонки карточки (matchHits пересчитан в модуле, Ruling 2).
/// Токен отмены.
public Task UpdateColumnAsync(CardColumnUpdateDto update, CancellationToken ct);
///
/// Применяет результат ручной переклассификации к существующей карточке ОДНИМ обновлением
/// (leads.py reclassify_lead L346–367): тип/заголовок/суть/стек/бюджет/конверсия/контакты/matchHits + колонка.
///
/// Поля классификации (полная замена; см. CardReclassificationDto).
/// Токен отмены.
/// True — строка обновлена; false — карточки нет (404-семантика сервиса).
public Task ApplyReclassificationAsync(CardReclassificationDto update, CancellationToken ct);
///
/// Снимает флаг «новое»: с одной карточки (cardId), с колонки (col) или со всех (оба null) — leads.py mark_seen L250–256.
///
/// Id карточки либо null.
/// Колонка либо null.
/// Токен отмены.
public Task UpdateSeenAsync(string? cardId, string? col, CancellationToken ct);
///
/// Удаляет карточку навсегда: Cards + комментарии (FK cascade) + строки дедупа карточки
/// (DedupEntries WHERE LeadId=?, Ruling 3); журнал/outbox не трогает (leads.py _hard_delete L225–234).
///
/// Id карточки.
/// Токен отмены.
public Task DeleteForeverAsync(string cardId, CancellationToken ct);
///
/// Полная очистка служебной колонки (только trash|archive — валидирует сервис), leads.py clear_col L237–247;
/// строки дедупа удаляемых карточек чистятся вместе с ними (Ruling 3).
///
/// Очищаемая колонка (trash/archive).
/// Токен отмены.
/// Сколько карточек удалено (0 — колонка пуста).
public Task ClearColAsync(string col, CancellationToken ct);
///
/// Счётчики карточек по колонкам (count + new) для GET /api/cards/counts (leads.py counts L268–279).
///
/// Токен отмены.
/// Словарь col → {count, new}; ключи — только колонки с карточками.
public Task> CountCardsByColAsync(CancellationToken ct);
// ── Комментарии (LeadComments) ────────────────────────────────────────
///
/// Комментарии карточки (для ответа add-comment и карточки; сортировка по времени добавления).
///
/// Id карточки.
/// Токен отмены.
/// Комментарии (time — human-метка от CreatedAt); пусто — комментариев нет.
public Task> ListCommentsAsync(string cardId, CancellationToken ct);
///
/// Добавляет комментарий (leads.py add_comment L259–265; CreatedAt — UTC-now).
///
/// Готовый id (cm_..., генерирует модуль).
/// Id карточки.
/// Автор («Вы» — свои комментарии).
/// Текст (непустой — валидирует сервис, 400 «Пустой комментарий»).
/// Токен отмены.
public Task AddCommentAsync(string commentId, string cardId, string by, string text, CancellationToken ct);
// ── Журнал действий (CardMoves = learning_log) ────────────────────────
///
/// Пишет строку журнала действия пользователя (move/trash/restore/comment) — leads.py _log_learning L40–44.
///
/// Запись журнала (id lm_... сгенерирован модулем).
/// Токен отмены.
public Task AddMoveAsync(CardMoveDto move, CancellationToken ct);
///
/// Число записей журнала = счётчик learning (Ruling 4, Task 5 StatusAsync).
///
/// Токен отмены.
/// Количество строк CardMoves.
public Task CountMovesAsync(CancellationToken ct);
///
/// Свежие примеры разметки пользователя для few-shot ИИ-классификации (pipeline.py _learning_examples L201–215).
///
///
/// Join журнала CardMoves с карточками (Cards.SourceMsg): действия move/restore, цель не служебная
/// (trash/archive), исходный текст непустой; ORDER BY created_at DESC, ≤ limit записей. Используется
/// контекст-билдером классификации (план Task 15, Ruling 5) — текст примера дополнительно режется
/// вызывающим до 500 кодовых точек (python L214).
///
/// Максимум примеров (python L201: 8).
/// Токен отмены.
/// Примеры «текст → колонка», свежие первыми; пусто — истории разметки нет.
public Task> GetAiMarkupExamplesAsync(int limit, CancellationToken ct);
// ── Правила хранения (тик, Ruling 8) ──────────────────────────────────
///
/// Кандидаты на автоархив: карточки досок и «Неразобранного» со ReceivedAt старше срока (tick_storage L462–467).
///
/// Граница: received_at < now − archiveAfterDays.
/// Токен отмены.
/// Id карточек-кандидатов (колонка при архивации пишется archive через UpdateColumnAsync).
public Task> ListArchiveCandidatesAsync(DateTimeOffset receivedBeforeUtc, CancellationToken ct);
///
/// Архивирует пачку карточек ОДНИМ UPDATE: col=archive, is_new=false, archived_at=archivedAt,
/// matchHits — пустой массив (тик tick_storage L462–473; batch-замена по-карточных UpdateColumnAsync,
/// PrevCol при архивации не трогается — Ruling 8).
///
/// Id карточек-кандидатов (список из ListArchiveCandidatesAsync).
/// Момент архивации (один «now» тика).
/// Токен отмены.
/// Сколько карточек реально архивировано (0 — кандидатов не было/уже не в рабочих колонках).
public Task ArchiveAsync(IReadOnlyList cardIds, DateTimeOffset archivedAt, CancellationToken ct);
///
/// Кандидаты на очистку архива: col='archive' и ArchivedAt старше срока (tick_storage L475–478).
///
/// Граница: archived_at < now − archiveClearDays.
/// Токен отмены.
/// Id карточек для жёсткого удаления.
public Task> ListExpiredArchiveCandidatesAsync(DateTimeOffset archivedBeforeUtc, CancellationToken ct);
///
/// Кандидаты на очистку корзины: col='trash' и ReceivedAt старше срока (tick_storage L480–483).
///
/// Граница: received_at < now − trashClearDays.
/// Токен отмены.
/// Id карточек для жёсткого удаления.
public Task> ListTrashCandidatesAsync(DateTimeOffset receivedBeforeUtc, CancellationToken ct);
///
/// Жёстко удаляет пачку карточек: Cards + комментарии (FK cascade) + строки дедупа карточек
/// (DedupEntries WHERE LeadId IN …, Ruling 3) — для очисток тика и clear-col (Ruling 8).
///
/// Id карточек на удаление.
/// Токен отмены.
/// Сколько карточек удалено.
public Task PurgeAsync(IReadOnlyList cardIds, CancellationToken ct);
// ── Пересчёт конверсий (Ruling 7, Task 12) ─────────────────────────────
///
/// Карточки для пересчёта ConvFrom/ConvTo/ConvCur: budgetCur непуст и col NOT IN (archive, trash) — recompute_conversions L106–130.
///
/// Токен отмены.
/// Карточки с бюджетом (используются Budget/бюджетные поля; служебные колонки исключены).
public Task> ListCardsForConversionAsync(CancellationToken ct);
///
/// Записывает пересчитанную конверсию бюджета карточки (обновляет только conv-поля).
///
/// Id карточки.
/// Сконвертированная нижняя граница, либо null.
/// Сконвертированная верхняя граница, либо null.
/// Валюта конверсии (целевая валюта тенанта); пусто — конверсия снята.
/// Токен отмены.
public Task UpdateConversionAsync(string cardId, double? convFrom, double? convTo, string convCur, CancellationToken ct);
// ── Эвристика ИИ-предложений (Ruling 3, Task 14) ───────────────────────
///
/// Карточки «Неразобранного» с исходным текстом — вход эвристики suggest (suggest.py, Ruling 3).
///
/// Токен отмены.
/// Карточки col='inbox' с непустым SourceMsg (частотные темы считаются по source_msg).
public Task> ListInboxWithSourceAsync(CancellationToken ct);
}