                            db: AsyncSession = Depends(get_db),
                            user: User = Depends(get_current_user),
                            _: None = Depends(require_csrf)):
    """Upload an attachment. Kind inferred from extension; stored with random prefix path.
    HTML files are stored as editable HTML attachments (variables substituted per recipient)."""
    content = await file.read()
    if len(content) > MAX_UPLOAD_BYTES:
        raise HTTPException(status_code=413, detail="File exceeds 10MB limit")
    if not content:
        raise HTTPException(status_code=400, detail="Empty file")

    orig = file.filename or "attachment.bin"
    ext = _ext(orig)
    kind = _kind_for(ext)
    if kind is None:
        raise HTTPException(status_code=400,
                            detail=f"Unsupported file type: {ext or 'unknown'}. Allowed: "
                                   f"txt, html, pdf, png, jpg, gif, webp, csv, zip")

    # security: scan buffer for executable magic before persisting
    verdict, reason = att_svc.scan_for_malware(content, orig)
    if not verdict:
        raise HTTPException(status_code=400, detail=f"Rejected by malware scan: {reason}")

    is_html = kind == "html"
    html_body = ""
    if is_html:
        try:
            html_body = content.decode("utf-8")
        except UnicodeDecodeError:
            raise HTTPException(status_code=400, detail="HTML file is not valid UTF-8")
        problems = validate_html(html_body)
        if problems:  # validate_html returns a list of problem strings
            raise HTTPException(status_code=400,
                                detail=f"HTML rejected: {'; '.join(problems)}")
        # HTML attachments are stored inline (used with variable substitution at send time)
        att = Attachment(
            user_id=user.id,
            name=name or re.sub(r"\.[^.]+$", "", orig) or "html-attachment",
            filename=orig,
            stored_path="",
            mime_type="text/html",
            size_bytes=len(content),
            kind="html",
            description=description or "",
            is_html_attachment=True,
            html_attachment_body=html_body,
            file_sha256=hashlib.sha256(content).hexdigest(),
        )
    else:
        stored = att_svc.save_upload(orig, content)
        att = Attachment(
            user_id=user.id,
            name=name or re.sub(r"\.[^.]+$", "", orig) or "attachment",
            filename=orig,
            stored_path=stored["stored_path"],
            mime_type=MIME_BY_EXT.get(ext, "application/octet-stream"),
            size_bytes=len(content),
            kind=kind,
            description=description or "",
            file_sha256=hashlib.sha256(content).hexdigest(),
        )
    db.add(att)
    await db.commit()
    await log_audit(db, user.id, "attachment.upload", "attachment", att.id,
                    {"name": att.name, "kind": kind, "sha256": att.file_sha256,
                     "size": len(content)})
    return {"id": att.id, **_serialize(att)}


@router.post("/convert-pdf")
async def convert_html_to_pdf(template_id: int = Query(..., description="HTML template to render"),
                              db: AsyncSession = Depends(get_db),
                              user: User = Depends(get_current_user),
                              _: None = Depends(require_csrf)):
    """Convert one of the user's saved HTML templates into a real PDF attachment
    using WeasyPrint. The generated PDF is stored and can be attached to campaigns."""
    tpl = await db.get(EmailTemplate, template_id)
    if not tpl or tpl.user_id != user.id:
        raise HTTPException(status_code=404, detail="Template not found")

    problems = validate_html(tpl.body_html)
    if problems:  # list of problem strings
        raise HTTPException(status_code=400,
                            detail=f"Template has errors: {'; '.join(problems)}")

    pdf_bytes = await asyncio.get_event_loop().run_in_executor(None, html_to_pdf, tpl.body_html)
    if not pdf_bytes:
        raise HTTPException(status_code=500, detail="PDF rendering failed")

    verdict, reason = att_svc.scan_for_malware(pdf_bytes, "converted.pdf")
    if not verdict:
        raise HTTPException(status_code=500, detail=f"PDF failed safety scan: {reason}")

    base = re.sub(r"\.[^.]+$", "", tpl.name or "attachment") or "attachment"
    fname = f"{base}.pdf"
    stored = att_svc.save_upload(fname, pdf_bytes)
    att = Attachment(
        user_id=user.id,
        name=f"{tpl.name} (PDF)"[:300],
        filename=fname,
        stored_path=stored["stored_path"],
        mime_type="application/pdf",
        size_bytes=len(pdf_bytes),
        kind="pdf",
        description=f"Generated from template '{tpl.name}' via WeasyPrint",
        html_source_template_id=tpl.id,
        file_sha256=hashlib.sha256(pdf_bytes).hexdigest(),
    )
    db.add(att)
    await db.commit()
    await log_audit(db, user.id, "attachment.convert_pdf", "html_template", tpl.id,
                    {"pdf_attachment_id": att.id, "size": len(pdf_bytes)})
    return {"id": att.id, **_serialize(att)}


@router.get("/{att_id}")
async def get_attachment(att_id: int, user: User = Depends(get_current_user),
                         db: AsyncSession = Depends(get_db)):
    att = await _get_own(db, user, att_id)
    out = _serialize(att)
