"""Режим «3 варианта» (ТЗ 3.8).
Попадание в доступное время — нестрогое: оценка может превышать доступное
время до +50% (решение пользователя 2026-09-19). Меньшая задача всегда
подходит. Оценка длительности — только LLM/вручную; задачи без оценки
учитываются как фолбэк.
"""
from typing import Any
from sqlalchemy import select
from app.models import Project, Task
# допустимое превышение оценки над доступным временем
FIT_TOLERANCE = 0.5
def _fits(estimated_minutes: int, available_minutes: int) -> bool:
return estimated_minutes <= available_minutes * (1 + FIT_TOLERANCE)
def pick_options(db: Any, available_minutes: int, limit: int = 3) -> list[Task]:
"""«3 варианта» (ТЗ 3.8): задачи, подходящие под доступное время.
Кандидаты: утверждённые, к выполнению/в работе, проект активен (или без
проекта). Сортировка: приоритет, затем свежесть. Разнообразие: максимум
одна задача на проект; сначала — попадающие в окно ±50%, затем задачи
без оценки (её нельзя отсечь по времени).
"""
stmt = (
select(Task)
.where(Task.detail_state == "approved", Task.status.in_(("to_do", "in_progress")))
.order_by(Task.priority.desc().nullslast(), Task.created_at.asc())
)
tasks = list(db.scalars(stmt).all())
active_projects: set[int | None] = {None}
for p in db.scalars(select(Project)).all():
if p.relevance_status == "active":
active_projects.add(p.id)
used_projects: set[int | None] = set()
picked_ids: set[int] = set()
picked: list[Task] = []
# сначала — попадающие в доступное время (допуск +50%)
for t in tasks:
if len(picked) >= limit:
break
if t.project_id not in active_projects or t.project_id in used_projects:
continue
if t.estimated_minutes is not None and not _fits(t.estimated_minutes, available_minutes):
continue
if t.estimated_minutes is None:
continue # без оценки — второй проход
picked.append(t)
picked_ids.add(t.id)
used_projects.add(t.project_id)
# добираем задачи без оценки длительности (её нельзя отсечь по времени)
if len(picked) < limit:
for t in tasks:
if len(picked) >= limit:
break
if t.estimated_minutes is not None:
continue # с оценкой: либо уложились в первый проход, либо не влезли
if t.id in picked_ids or t.project_id not in active_projects:
continue
if t.project_id in used_projects:
continue
picked.append(t)
picked_ids.add(t.id)
used_projects.add(t.project_id)
return picked[:limit]