"""Report export endpoint — download the assembled report as a DOCX file. GET /reports/{report_id}/export?format=docx → streams a professional RICS-formatted ``.docx`` file. The endpoint reuses the exact same section-loading logic as GET /reports/{report_id}/sections so the exported document always matches what the user sees on screen. """ from __future__ import annotations import json import logging from fastapi import APIRouter, Depends, HTTPException, Query, Request from fastapi.responses import Response from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.api.rate_limit import check_read from app.db.database import get_db from app.db.models import Report, ReportSection, ReportSectionPhoto from app.generator.docx_builder import build_docx from app.models.schemas import Provenance, SectionPayload, WritingStyleProfile from app.storage.photo_paths import resolve_report_photo_path logger = logging.getLogger(__name__) router = APIRouter() _DOCX_MIME = "application/vnd.openxmlformats-officedocument.wordprocessingml.document" @router.get("/reports/{report_id}/export") async def export_report( report_id: str, request: Request, fmt: str = Query(default="docx", alias="format", pattern="^docx$"), db: AsyncSession = Depends(get_db), _: None = Depends(check_read), ) -> Response: """Stream the assembled report as a downloadable DOCX file. All sections are fetched from the database and rendered into a professional RICS-styled document. Sections that have not been generated yet are replaced with a placeholder paragraph. Args: report_id: UUID of the report to export. request: Provides ``state.tenant_id``. fmt: Output format — currently only ``docx`` is supported. db: Injected async database session. Returns: A binary ``Response`` with DOCX content and appropriate ``Content-Disposition`` header for browser downloads. Raises: HTTPException 404: Report not found or belongs to a different tenant. HTTPException 400: Unsupported export format (only ``docx`` accepted). """ tenant_id: str = request.state.tenant_id report = await db.get(Report, report_id) if report is None or report.tenant_id != tenant_id: raise HTTPException(status_code=404, detail="Report not found") # Load all sections from the database result = await db.execute( select(ReportSection).where(ReportSection.report_id == report_id) ) sections_orm = result.scalars().all() sections: dict[str, SectionPayload] = {} for s in sections_orm: raw = json.loads(s.provenance or "[]") if isinstance(raw, dict): sources = raw.get("sources", []) meta = raw.get("meta", {}) else: sources = raw meta = {} style_profile: WritingStyleProfile | None = None raw_sp = meta.get("style_profile") if isinstance(raw_sp, dict): try: style_profile = WritingStyleProfile(**raw_sp) except Exception: style_profile = None prov_list: list[Provenance] = [] for p in sources: if not isinstance(p, dict) or "doc_id" not in p or "chunk_id" not in p or "score" not in p: continue try: prov_list.append(Provenance.model_validate(p)) except Exception: # noqa: BLE001 continue sections[s.section_code] = SectionPayload( text=s.text, confidence=s.confidence, provenance=prov_list, cached=bool(s.cached), mode=meta.get("mode", "generate"), style_profile=style_profile, ai_level=meta.get("ai_level"), ai_percent=meta.get("ai_percent"), ai_transparency=meta.get("ai_transparency") if isinstance(meta.get("ai_transparency"), dict) else None, pipeline=meta.get("pipeline"), fallback_used=meta.get("fallback_used"), inspector=meta.get("inspector") if isinstance(meta.get("inspector"), dict) else None, ) if not sections: raise HTTPException( status_code=422, detail="No sections have been generated yet. Generate at least one section before exporting.", ) photos_res = await db.execute( select(ReportSectionPhoto).where( ReportSectionPhoto.tenant_id == tenant_id, ReportSectionPhoto.report_id == report_id, ) ) photo_rows = photos_res.scalars().all() photo_paths_by_section: dict[str, list[str]] = {} for p in photo_rows: resolved = resolve_report_photo_path( p.file_path, tenant_id=tenant_id, report_id=report_id, section_code=p.section_code, photo_id=p.id, ) if resolved: photo_paths_by_section.setdefault(p.section_code, []).append(resolved) logger.info( "Exporting report=%s as %s for tenant=%s (%d sections)", report_id, fmt, tenant_id, len(sections), ) docx_bytes = build_docx( sections=sections, report_id=report_id, tenant_id=tenant_id, survey_level=report.survey_level, section_photo_paths=photo_paths_by_section, ) safe_report_id = report_id.replace("-", "")[:12] filename = f"RICS_Survey_Report_{safe_report_id}.docx" return Response( content=docx_bytes, media_type=_DOCX_MIME, headers={"Content-Disposition": f'attachment; filename="{filename}"'}, )