        problems.append(f"Unsubstituted variables remain: {sorted(set(leftovers))[:10]}")
    return problems


def html_to_pdf(html: str) -> bytes:
    """Real HTML → PDF conversion via WeasyPrint."""
    try:
        return WeasyHTML(string=html).write_pdf()
    except Exception as e:
        raise HtmlValidationError(f"PDF conversion failed: {e}") from e


def sanitize_preview(html: str) -> str:
    """Sanitize HTML for safe rendering in the in-app editor/preview (server-side)."""
    import bleach
    allowed_tags = bleach.ALLOWED_TAGS + ["table", "thead", "tbody", "tr", "td", "th",
                                          "img", "p", "span", "div", "h1", "h2", "h3",
                                          "strong", "em", "a", "ul", "ol", "li", "br", "hr", "font", "center"]
    allowed_attrs = {"a": ["href", "title"], "img": ["src", "alt", "width", "height"],
                     "*": ["style", "class", "align", "bgcolor", "width", "border", "cellpadding", "cellspacing"]}
    protocols = ["http", "https", "mailto"]
    return bleach.clean(html or "", tags=allowed_tags, attributes=allowed_attrs, protocols=protocols, strip=False)


def sanitize_html_for_email(html: str) -> str:
    """Remove scripts/forms from outgoing email HTML (keep email-safe markup)."""
    return sanitize_preview(html)


def generate_attachment_filename(kind: str, invoice_number: str) -> str:
    """Unique, deterministic filenames like Invoice_INV-XXXX.pdf per campaign rules."""
