File size: 5,714 Bytes
3c31a2a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
732b14f
3c31a2a
b76f199
3c31a2a
eda3d74
732b14f
3c31a2a
 
 
 
 
 
 
 
 
 
 
 
 
 
732b14f
3c31a2a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eda3d74
 
 
 
 
 
 
 
 
3c31a2a
 
 
eda3d74
 
3c31a2a
 
eda3d74
b76f199
eda3d74
 
 
b76f199
 
 
3c31a2a
 
 
 
 
 
 
 
b76f199
 
 
 
 
 
 
 
 
732b14f
 
 
 
 
 
 
 
 
b76f199
3c31a2a
 
 
 
 
 
 
 
 
 
 
 
b76f199
 
3c31a2a
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
"""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}"'},
    )