"""Content reports router — public submission + admin management. Public: POST /reports Submit a content issue report Admin: GET /admin/reports List reports (filterable by status, content_type) GET /admin/reports/{id} Get single report detail PATCH /admin/reports/{id} Update status / add admin notes """ import logging import uuid from datetime import datetime, timezone from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from database import get_session from models import ContentReport, ReportStatus from schemas import ( ContentReportCreate, ContentReportListResponse, ContentReportRead, ContentReportUpdate, ) logger = logging.getLogger("chrysopedia.reports") router = APIRouter(tags=["reports"]) # ── Public ─────────────────────────────────────────────────────────────────── @router.post("/reports", response_model=ContentReportRead, status_code=201) async def submit_report( body: ContentReportCreate, db: AsyncSession = Depends(get_session), ): """Submit a content issue report (public, no auth).""" report = ContentReport( content_type=body.content_type, content_id=body.content_id, content_title=body.content_title, report_type=body.report_type, description=body.description, page_url=body.page_url, ) db.add(report) await db.commit() await db.refresh(report) logger.info( "New content report: id=%s type=%s content=%s/%s", report.id, report.report_type, report.content_type, report.content_id, ) return report # ── Admin ──────────────────────────────────────────────────────────────────── @router.get("/admin/reports", response_model=ContentReportListResponse) async def list_reports( status: Annotated[str | None, Query(description="Filter by status")] = None, content_type: Annotated[str | None, Query(description="Filter by content type")] = None, offset: Annotated[int, Query(ge=0)] = 0, limit: Annotated[int, Query(ge=1, le=100)] = 50, db: AsyncSession = Depends(get_session), ): """List content reports with optional filters.""" stmt = select(ContentReport) if status: stmt = stmt.where(ContentReport.status == status) if content_type: stmt = stmt.where(ContentReport.content_type == content_type) # Count count_stmt = select(func.count()).select_from(stmt.subquery()) total = (await db.execute(count_stmt)).scalar() or 0 # Fetch page stmt = stmt.order_by(ContentReport.created_at.desc()) stmt = stmt.offset(offset).limit(limit) result = await db.execute(stmt) items = result.scalars().all() return {"items": items, "total": total, "offset": offset, "limit": limit} @router.get("/admin/reports/{report_id}", response_model=ContentReportRead) async def get_report( report_id: uuid.UUID, db: AsyncSession = Depends(get_session), ): """Get a single content report by ID.""" result = await db.execute( select(ContentReport).where(ContentReport.id == report_id) ) report = result.scalar_one_or_none() if not report: raise HTTPException(status_code=404, detail="Report not found") return report @router.patch("/admin/reports/{report_id}", response_model=ContentReportRead) async def update_report( report_id: uuid.UUID, body: ContentReportUpdate, db: AsyncSession = Depends(get_session), ): """Update report status and/or admin notes.""" result = await db.execute( select(ContentReport).where(ContentReport.id == report_id) ) report = result.scalar_one_or_none() if not report: raise HTTPException(status_code=404, detail="Report not found") if body.status is not None: # Validate status value try: ReportStatus(body.status) except ValueError: raise HTTPException( status_code=422, detail=f"Invalid status: {body.status}. Must be one of: open, acknowledged, resolved, dismissed", ) report.status = body.status if body.status in ("resolved", "dismissed"): report.resolved_at = datetime.now(timezone.utc).replace(tzinfo=None) elif body.status == "open": report.resolved_at = None if body.admin_notes is not None: report.admin_notes = body.admin_notes await db.commit() await db.refresh(report) logger.info( "Report updated: id=%s status=%s", report.id, report.status, ) return report