from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy import select, desc from sqlalchemy.orm import selectinload from sqlalchemy.ext.asyncio import AsyncSession from app.dependencies import get_current_user, get_db from app.models.device import Device from app.models.object import Object from app.models.rack import Rack from app.models.task import Task, TaskResult from app.models.zone import Zone from app.models.user import User from app.plugins.registry import action_registry from app.schemas.task import ActionInfo, TaskCreate, TaskDetail, TaskRead from app.services import task as task_service router = APIRouter(prefix="/api/v1/tasks", tags=["tasks"]) async def _resolve_labels(db: AsyncSession, tasks: list[Task]) -> dict[str, str]: """Batch-resolve target_scope to human-readable labels.""" obj_ids: set[int] = set() dev_ids: set[int] = set() rack_ids: set[int] = set() zone_ids: set[int] = set() for t in tasks: scope = t.target_scope kind = scope.get("type") if kind == "object": obj_ids.add(scope["id"]) elif kind == "device": dev_ids.add(scope["id"]) elif kind == "rack": rack_ids.add(scope["id"]) elif kind == "zone": zone_ids.add(scope["id"]) elif kind in ("category", "role") and (oid := scope.get("object_id")): obj_ids.add(oid) objs: dict[int, str] = {} devs: dict[int, str] = {} rack_names: dict[int, str] = {} zone_names: dict[int, str] = {} if obj_ids: rows = await db.execute(select(Object.id, Object.name).where(Object.id.in_(obj_ids))) objs = {row[0]: row[1] for row in rows.all()} if dev_ids: rows = await db.execute( select(Device.id, Device.hostname, Device.ip).where(Device.id.in_(dev_ids)) ) devs = {row[0]: (row[1] or row[2]) for row in rows.all()} if rack_ids: rows = await db.execute(select(Rack.id, Rack.name).where(Rack.id.in_(rack_ids))) rack_names = {row[0]: row[1] for row in rows.all()} if zone_ids: rows = await db.execute(select(Zone.id, Zone.name).where(Zone.id.in_(zone_ids))) zone_names = {row[0]: row[1] for row in rows.all()} labels: dict[str, str] = {} for t in tasks: scope = t.target_scope kind = scope.get("type", "") if kind == "object": labels[t.id] = objs.get(scope.get("id"), f"Объект #{scope.get('id')}") elif kind == "device": labels[t.id] = devs.get(scope.get("id"), f"Устройство #{scope.get('id')}") elif kind == "rack": _rid = scope.get('id') labels[t.id] = f"Стойка: {rack_names.get(_rid, f'#{_rid}')}" elif kind == "zone": _zid = scope.get('id') labels[t.id] = f"Зона: {zone_names.get(_zid, f'#{_zid}')}" elif kind in ("category", "role"): val = scope.get("value", "?") if oid := scope.get("object_id"): labels[t.id] = f"{val} @ {objs.get(oid, f'#{oid}')}" else: labels[t.id] = f"{val} (все объекты)" else: labels[t.id] = "—" return labels @router.get("/actions", response_model=list[ActionInfo]) async def list_actions(_: User = Depends(get_current_user)): return [ ActionInfo( name=a.name, display_name=a.display_name, compatible_categories=a.compatible_categories, params_schema=a.params_schema, ) for a in action_registry.all() ] @router.post("", response_model=TaskRead, status_code=status.HTTP_202_ACCEPTED) async def create_task( body: TaskCreate, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): try: task = await task_service.create_task( action_type=body.type, target_scope=body.target_scope, params=body.params, user_id=current_user.id, ) except ValueError as e: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) labels = await _resolve_labels(db, [task]) result = TaskRead.model_validate(task) result.target_label = labels.get(task.id) return result @router.get("", response_model=list[TaskRead]) async def list_tasks( limit: int = 50, skip: int = 0, device_id: Optional[int] = Query(None), db: AsyncSession = Depends(get_db), _: User = Depends(get_current_user), ): query = select(Task).order_by(desc(Task.created_at)) if device_id is not None: query = ( query .join(TaskResult, Task.id == TaskResult.task_id) .where(TaskResult.device_id == device_id) .distinct() ) rows = await db.execute(query.offset(skip).limit(limit)) tasks = list(rows.scalars().all()) labels = await _resolve_labels(db, tasks) result = [] for t in tasks: r = TaskRead.model_validate(t) r.target_label = labels.get(t.id) result.append(r) return result @router.get("/{task_id}", response_model=TaskDetail) async def get_task( task_id: str, db: AsyncSession = Depends(get_db), _: User = Depends(get_current_user), ): rows = await db.execute( select(Task) .options(selectinload(Task.events), selectinload(Task.device_results)) .where(Task.id == task_id) ) task = rows.scalar_one_or_none() if task is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Task not found") labels = await _resolve_labels(db, [task]) result = TaskDetail.model_validate(task) result.target_label = labels.get(task.id) return result