import uuid from fastapi import HTTPException, status from jinja2 import Environment, TemplateSyntaxError, select_autoescape from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.models.pdf_template import PdfTemplate _jinja_env = Environment(autoescape=select_autoescape(["html"])) def validate_jinja2(html: str) -> tuple[bool, str]: """Validate Jinja2 template syntax. Returns (is_valid, error_message).""" try: _jinja_env.parse(html) return True, "" except TemplateSyntaxError as e: return False, f"Template syntax error at line {e.lineno}: {e.message}" async def list_templates(db: AsyncSession, active_only: bool = True, locale: str | None = None) -> list[PdfTemplate]: stmt = select(PdfTemplate) if active_only: stmt = stmt.where(PdfTemplate.is_active == True) # noqa: E712 if locale: stmt = stmt.where(PdfTemplate.locale == locale) stmt = stmt.order_by(PdfTemplate.is_default.desc(), PdfTemplate.name, PdfTemplate.locale) result = await db.execute(stmt) return list(result.scalars().all()) async def get_template(db: AsyncSession, template_id: uuid.UUID) -> PdfTemplate: result = await db.execute(select(PdfTemplate).where(PdfTemplate.id == template_id)) template = result.scalar_one_or_none() if not template: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Template not found") return template async def get_default_template(db: AsyncSession, locale: str = "en") -> PdfTemplate | None: result = await db.execute( select(PdfTemplate).where( PdfTemplate.is_default == True, # noqa: E712 PdfTemplate.locale == locale, ) ) tmpl = result.scalar_one_or_none() if not tmpl: # Fallback to any default result = await db.execute( select(PdfTemplate).where(PdfTemplate.is_default == True) # noqa: E712 ) tmpl = result.scalars().first() return tmpl async def create_template(db: AsyncSession, **kwargs) -> PdfTemplate: html = kwargs.get("html_content", "") valid, error = validate_jinja2(html) if not valid: raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=error) template = PdfTemplate(**kwargs) db.add(template) await db.flush() return template async def update_template(db: AsyncSession, template_id: uuid.UUID, **kwargs) -> PdfTemplate: template = await get_template(db, template_id) if "html_content" in kwargs and kwargs["html_content"] is not None: valid, error = validate_jinja2(kwargs["html_content"]) if not valid: raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=error) for key, value in kwargs.items(): if value is not None: setattr(template, key, value) await db.flush() return template async def delete_template(db: AsyncSession, template_id: uuid.UUID) -> None: template = await get_template(db, template_id) if template.is_default: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Cannot delete the default template", ) await db.delete(template) async def render_preview(html_content: str) -> str: """Render template with sample data for preview.""" valid, error = validate_jinja2(html_content) if not valid: raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=error) template = _jinja_env.from_string(html_content) return template.render( title="Sample Report", user_name="John Doe", generated_at="2026-03-19 12:00 UTC", memories=[ {"category": "health", "title": "Annual Checkup", "content": "Last checkup was in January 2026. All results normal.", "importance": "medium"}, {"category": "finance", "title": "Tax Deadline", "content": "Filing deadline: April 15, 2026", "importance": "high"}, ], documents=[ {"original_filename": "report_2026.pdf", "doc_type": "report", "excerpt": "This is a sample document excerpt showing how document content appears in the report..."}, ], ai_summary="This is a sample AI-generated summary of the user's data.", )