"""Report generation: JSON preview and DOCX export from surveyor notes.""" from __future__ import annotations from typing import Literal from fastapi import APIRouter, Depends, HTTPException, Response, status from pydantic import BaseModel, Field, model_validator from backend.api.deps import get_current_tenant from backend.core import pii_scrubber, report_assembler, section_mapper, template_discoverer from backend.core.interference import resolve_interference_level from backend.core.photo_store import draft_section_photo_paths from backend.core.rag_store import TIER_REFERENCE, get_rag_store from backend.models.schema import TemplateSchema router = APIRouter(prefix="/api/report", tags=["report"]) _DOCX_MIME = "application/vnd.openxmlformats-officedocument.wordprocessingml.document" class GenerateRequest(BaseModel): raw_notes: str = Field( default="", description="Surveyor field notes (any format). May be empty when using photos only.", ) property_type: str = Field( min_length=1, description="Generic property descriptor, e.g. semi-detached house.", ) tenure: str = Field(min_length=1, description="e.g. freehold or leasehold.") title: str = Field(default="Survey Report", max_length=200) include_footer: bool = Field(default=False) report_draft_id: str | None = Field( default=None, description=( "Draft id from POST /api/report/drafts. When set, selected section photos " "(max 2 per section) are analyzed and mapped into the relevant sections." ), ) survey_level: int = Field( default=3, ge=1, le=3, description=( "RICS survey product tier (1/2/3). When interference_level is omitted, " "Level 1→minimum, Level 2→medium, Level 3→maximum composition." ), ) interference_level: Literal["minimum", "medium", "maximum"] | None = Field( default=None, description=( "Report composition depth. When omitted, derived from survey_level. " "minimum: baseline preservation with in-place fact updates. " "medium: technical in-place edit on past report baseline. " "maximum: full long-form narrative edit retaining baseline scaffolding." ), ) @model_validator(mode="after") def notes_or_draft(self) -> GenerateRequest: if not self.raw_notes.strip() and not self.report_draft_id: raise ValueError("raw_notes or report_draft_id is required") return self def _require_generation_ready( tenant_id: str, interference_level: Literal["minimum", "medium", "maximum"], ) -> TemplateSchema: schema = template_discoverer.ensure_canonical_schema(tenant_id) store = get_rag_store() if store.count(tenant_id, TIER_REFERENCE) == 0: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=( "No past reports have been ingested. Upload reference reports " f"for {interference_level}-AI mapping." ), ) return schema @router.post("/preview") def preview( req: GenerateRequest, tenant_id: str = Depends(get_current_tenant), ) -> dict: il = resolve_interference_level(req.interference_level, req.survey_level) schema = _require_generation_ready(tenant_id, il) result = section_mapper.generate_report( tenant_id, req.raw_notes, property_type=req.property_type, tenure=req.tenure, interference_level=il, survey_level=req.survey_level, report_draft_id=req.report_draft_id, ) return report_assembler.to_preview(result, schema) @router.post("/generate") def generate( req: GenerateRequest, tenant_id: str = Depends(get_current_tenant), ) -> Response: il = resolve_interference_level(req.interference_level, req.survey_level) schema = _require_generation_ready(tenant_id, il) result = section_mapper.generate_report( tenant_id, req.raw_notes, property_type=req.property_type, tenure=req.tenure, interference_level=il, survey_level=req.survey_level, report_draft_id=req.report_draft_id, ) section_photos = draft_section_photo_paths( tenant_id, req.report_draft_id, schema.section_ids(), ) try: data = report_assembler.to_docx( result, schema, title=req.title, include_footer=req.include_footer, section_photo_paths=section_photos, ) except pii_scrubber.PiiDetectedError: for s in result.sections: s.text = pii_scrubber.scrub(s.text).text result.unassigned_text = pii_scrubber.scrub(result.unassigned_text).text data = report_assembler.to_docx( result, schema, title=req.title, include_footer=req.include_footer, section_photo_paths=section_photos, ) return Response( content=data, media_type=_DOCX_MIME, headers={"Content-Disposition": 'attachment; filename="survey_report_draft.docx"'}, )