"""Helpers for assembling board-group snapshot view models.""" from __future__ import annotations from collections import defaultdict from uuid import UUID from sqlalchemy import case, func from sqlmodel import col, select from sqlmodel.ext.asyncio.session import AsyncSession from app.models.agents import Agent from app.models.board_groups import BoardGroup from app.models.boards import Board from app.models.tasks import Task from app.schemas.board_groups import BoardGroupRead from app.schemas.boards import BoardRead from app.schemas.view_models import ( BoardGroupBoardSnapshot, BoardGroupSnapshot, BoardGroupTaskSummary, ) _STATUS_ORDER = {"in_progress": 0, "review": 1, "inbox": 2, "done": 3} _PRIORITY_ORDER = {"high": 0, "medium": 1, "low": 2} _RUNTIME_TYPE_REFERENCES = (UUID, AsyncSession) def _status_weight_expr() -> object: """Return a SQL expression that sorts task statuses by configured order.""" whens = [(col(Task.status) == key, weight) for key, weight in _STATUS_ORDER.items()] return case(*whens, else_=99) def _priority_weight_expr() -> object: """Return a SQL expression that sorts task priorities by configured order.""" whens = [ (col(Task.priority) == key, weight) for key, weight in _PRIORITY_ORDER.items() ] return case(*whens, else_=99) async def _boards_for_group( session: AsyncSession, *, group_id: UUID, exclude_board_id: UUID | None = None, ) -> list[Board]: """Return boards belonging to a board group with optional exclusion.""" statement = Board.objects.filter_by(board_group_id=group_id).statement if exclude_board_id is not None: statement = statement.where(col(Board.id) != exclude_board_id) return list( await session.exec( statement.order_by(func.lower(col(Board.name)).asc()), ), ) async def _task_counts_by_board( session: AsyncSession, board_ids: list[UUID], ) -> dict[UUID, dict[str, int]]: """Return per-board task counts keyed by task status.""" task_counts: dict[UUID, dict[str, int]] = defaultdict(lambda: defaultdict(int)) for board_id, status_value, total in list( await session.exec( select(col(Task.board_id), col(Task.status), func.count(col(Task.id))) .where(col(Task.board_id).in_(board_ids)) .group_by(col(Task.board_id), col(Task.status)), ), ): if board_id is None: continue task_counts[board_id][str(status_value)] = int(total or 0) return task_counts async def _ordered_tasks_for_boards( session: AsyncSession, board_ids: list[UUID], *, include_done: bool, ) -> list[Task]: """Return sorted tasks for boards, optionally excluding completed tasks.""" task_statement = select(Task).where(col(Task.board_id).in_(board_ids)) if not include_done: task_statement = task_statement.where(col(Task.status) != "done") task_statement = task_statement.order_by( col(Task.board_id).asc(), _status_weight_expr().asc(), _priority_weight_expr().asc(), col(Task.updated_at).desc(), col(Task.created_at).desc(), ) return list(await session.exec(task_statement)) async def _agent_names( session: AsyncSession, tasks: list[Task], ) -> dict[UUID, str]: """Return agent names keyed by assigned agent ids in the provided tasks.""" assigned_ids = { task.assigned_agent_id for task in tasks if task.assigned_agent_id is not None } if not assigned_ids: return {} return dict( list( await session.exec( select(col(Agent.id), col(Agent.name)).where( col(Agent.id).in_(assigned_ids), ), ), ), ) def _task_summaries_by_board( *, boards_by_id: dict[UUID, Board], tasks: list[Task], agent_name_by_id: dict[UUID, str], per_board_task_limit: int, ) -> dict[UUID, list[BoardGroupTaskSummary]]: """Build limited per-board task summary lists.""" tasks_by_board: dict[UUID, list[BoardGroupTaskSummary]] = defaultdict(list) if per_board_task_limit <= 0: return tasks_by_board for task in tasks: if task.board_id is None: continue current = tasks_by_board[task.board_id] if len(current) >= per_board_task_limit: continue board = boards_by_id.get(task.board_id) if board is None: continue current.append( BoardGroupTaskSummary( id=task.id, board_id=task.board_id, board_name=board.name, title=task.title, status=task.status, priority=task.priority, assigned_agent_id=task.assigned_agent_id, assignee=( agent_name_by_id.get(task.assigned_agent_id) if task.assigned_agent_id is not None else None ), due_at=task.due_at, in_progress_at=task.in_progress_at, created_at=task.created_at, updated_at=task.updated_at, ), ) return tasks_by_board async def build_group_snapshot( session: AsyncSession, *, group: BoardGroup, exclude_board_id: UUID | None = None, include_done: bool = False, per_board_task_limit: int = 5, ) -> BoardGroupSnapshot: """Build a board-group snapshot with board/task summaries.""" boards = await _boards_for_group( session, group_id=group.id, exclude_board_id=exclude_board_id, ) if not boards: return BoardGroupSnapshot( group=BoardGroupRead.model_validate(group, from_attributes=True), ) boards_by_id = {board.id: board for board in boards} board_ids = list(boards_by_id.keys()) task_counts = await _task_counts_by_board(session, board_ids) tasks = await _ordered_tasks_for_boards( session, board_ids, include_done=include_done, ) agent_name_by_id = await _agent_names(session, tasks) tasks_by_board = _task_summaries_by_board( boards_by_id=boards_by_id, tasks=tasks, agent_name_by_id=agent_name_by_id, per_board_task_limit=per_board_task_limit, ) snapshots = [ BoardGroupBoardSnapshot( board=BoardRead.model_validate(board, from_attributes=True), task_counts=dict(task_counts.get(board.id, {})), tasks=tasks_by_board.get(board.id, []), ) for board in boards ] return BoardGroupSnapshot( group=BoardGroupRead.model_validate(group, from_attributes=True), boards=snapshots, ) async def build_board_group_snapshot( session: AsyncSession, *, board: Board, include_self: bool = False, include_done: bool = False, per_board_task_limit: int = 5, ) -> BoardGroupSnapshot: """Build a board-group snapshot anchored to a board context.""" if not board.board_group_id: return BoardGroupSnapshot(group=None, boards=[]) group = await BoardGroup.objects.by_id(board.board_group_id).first(session) if group is None: return BoardGroupSnapshot(group=None, boards=[]) return await build_group_snapshot( session, group=group, exclude_board_id=None if include_self else board.id, include_done=include_done, per_board_task_limit=per_board_task_limit, )