from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy import select, func 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.snapshot import ConfigSnapshot from app.models.snapshot_policy import SnapshotPolicy from app.models.user import User from app.schemas.snapshot_policy import ( ObjectSnapshotSummary, PolicySnapshotStatus, SnapshotPolicyCreate, SnapshotPolicyRead, SnapshotPolicyUpdate, ) from app.services import task as task_service router = APIRouter(prefix="/api/v1", tags=["snapshot-policies"]) def _policy_snapshot_label(policy: SnapshotPolicy) -> str: if policy.action_type == "mikrotik_get_config": return "routeros-export" return policy.params.get("path", "") # ─── Dashboard ─────────────────────────────────────────────────────────────── @router.get("/snapshots/dashboard", response_model=list[ObjectSnapshotSummary]) async def snapshot_dashboard( db: AsyncSession = Depends(get_db), _: User = Depends(get_current_user), ): # 1. Load all active policies with their objects policies_q = ( select(SnapshotPolicy, Object.name, Object.city, Object.client) .join(Object, SnapshotPolicy.object_id == Object.id) .where(SnapshotPolicy.is_active == True, Object.is_active == True) .order_by(Object.city, Object.name, SnapshotPolicy.label) ) rows = await db.execute(policies_q) policy_rows = rows.all() if not policy_rows: return [] # 2. Count eligible devices per (object_id, category) eligible_q = ( select( Device.object_id, Device.category, func.count(Device.id).label("total"), ) .where(Device.is_active == True) .group_by(Device.object_id, Device.category) ) eligible_result = await db.execute(eligible_q) eligible_map: dict[tuple[int, str], int] = { (r.object_id, r.category): r.total for r in eligible_result.all() } # 3. Snapshot stats: distinct devices with snapshots + last collected time stats_q = ( select( Device.object_id, Device.category, ConfigSnapshot.label, func.count(func.distinct(ConfigSnapshot.device_id)).label("success_count"), func.max(ConfigSnapshot.created_at).label("last_collected_at"), ) .join(Device, ConfigSnapshot.device_id == Device.id) .where(Device.is_active == True) .group_by(Device.object_id, Device.category, ConfigSnapshot.label) ) stats_result = await db.execute(stats_q) stats_map: dict[tuple[int, str, str], tuple[int, object]] = { (r.object_id, r.category, r.label): (r.success_count, r.last_collected_at) for r in stats_result.all() } # 4. Assemble response grouped by object objects_map: dict[int, ObjectSnapshotSummary] = {} for policy, obj_name, obj_city, obj_client in policy_rows: snap_label = _policy_snapshot_label(policy) stats = stats_map.get((policy.object_id, policy.category, snap_label)) success_count = stats[0] if stats else 0 last_collected_at = stats[1] if stats else None total_devices = eligible_map.get((policy.object_id, policy.category), 0) policy_status = PolicySnapshotStatus( policy_id=policy.id, policy_label=policy.label, category=policy.category, action_type=policy.action_type, params=policy.params, last_collected_at=last_collected_at, success_count=success_count, total_devices=total_devices, ) if policy.object_id not in objects_map: objects_map[policy.object_id] = ObjectSnapshotSummary( object_id=policy.object_id, object_name=obj_name, city=obj_city, client=obj_client, policies=[], ) objects_map[policy.object_id].policies.append(policy_status) return list(objects_map.values()) # ─── CRUD ──────────────────────────────────────────────────────────────────── @router.get("/objects/{obj_id}/snapshot-policies", response_model=list[SnapshotPolicyRead]) async def list_policies( obj_id: int, db: AsyncSession = Depends(get_db), _: User = Depends(get_current_user), ): result = await db.execute( select(SnapshotPolicy) .where(SnapshotPolicy.object_id == obj_id) .order_by(SnapshotPolicy.label) ) return result.scalars().all() @router.post("/objects/{obj_id}/snapshot-policies", response_model=SnapshotPolicyRead, status_code=201) async def create_policy( obj_id: int, body: SnapshotPolicyCreate, db: AsyncSession = Depends(get_db), _: User = Depends(get_current_user), ): obj = await db.get(Object, obj_id) if obj is None: raise HTTPException(status_code=404, detail="Object not found") policy = SnapshotPolicy( object_id=obj_id, category=body.category, action_type=body.action_type, params=body.params, label=body.label, is_active=body.is_active, ) db.add(policy) await db.flush() await db.refresh(policy) return policy @router.put("/objects/{obj_id}/snapshot-policies/{policy_id}", response_model=SnapshotPolicyRead) async def update_policy( obj_id: int, policy_id: int, body: SnapshotPolicyUpdate, db: AsyncSession = Depends(get_db), _: User = Depends(get_current_user), ): policy = await db.get(SnapshotPolicy, policy_id) if policy is None or policy.object_id != obj_id: raise HTTPException(status_code=404, detail="Policy not found") for field, value in body.model_dump(exclude_unset=True).items(): setattr(policy, field, value) await db.flush() await db.refresh(policy) return policy @router.delete("/objects/{obj_id}/snapshot-policies/{policy_id}", status_code=204) async def delete_policy( obj_id: int, policy_id: int, db: AsyncSession = Depends(get_db), _: User = Depends(get_current_user), ): policy = await db.get(SnapshotPolicy, policy_id) if policy is None or policy.object_id != obj_id: raise HTTPException(status_code=404, detail="Policy not found") await db.delete(policy) await db.flush() # ─── Collect ───────────────────────────────────────────────────────────────── @router.post("/objects/{obj_id}/snapshot-policies/{policy_id}/collect", status_code=202) async def collect_policy( obj_id: int, policy_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): policy = await db.get(SnapshotPolicy, policy_id) if policy is None or policy.object_id != obj_id: raise HTTPException(status_code=404, detail="Policy not found") if not policy.is_active: raise HTTPException(status_code=400, detail="Policy is disabled") task = await task_service.create_task( action_type=policy.action_type, target_scope={ "type": "category", "object_id": obj_id, "category": policy.category, }, params=policy.params, user_id=current_user.id, ) return {"task_id": task.id}