"""MIME message construction + PDF generation for attachments and message bodies.

Real PDF engine: WeasyPrint. Real MIME: stdlib email.mime.
"""
import io
import mimetypes
import os
import re
import secrets
from email.mime.application import MIMEApplication
from email.mime.audio import MIMEAudio
from email.mime.base import MIMEBase
from email.mime.image import MIMEImage
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import formatdate, make_msgid
from typing import List, Optional

from weasyprint import HTML as WeasyHTML


class HtmlValidationError(Exception):
    pass


def build_mime_message(
    from_email: str,
    to_email: str,
    subject: str,
    html_body: str = "",
    text_body: str = "",
    attachments: Optional[list] = None,   # list of {filename, content(bytes), mime_type}
    unsubscribe_url: str = "",
    unsubscribe_mailto: str = "",
) -> MIMEMultipart:
    """Build a real MIME multipart message. From header always equals the authenticated account."""
    attachments = attachments or []
    # RFC 2046 structure: when attachments exist, build
    # multipart/mixed[ alternative/plain+html, attachment... ]; attachments are
    # NEVER siblings of the body alternatives (multipart/alternative only holds
    # the same content in different formats).
    body = MIMEMultipart("alternative") if (html_body and text_body) else None
    msg = MIMEMultipart("mixed") if attachments else (
        MIMEMultipart("alternative") if (html_body and text_body) else MIMEMultipart("mixed"))
    msg["From"] = from_email  # MUST be the authenticated Gmail account (never spoofed)
    msg["To"] = to_email
    msg["Subject"] = subject
    msg["Date"] = formatdate(localtime=True)
    msg["Message-ID"] = make_msgid()
    msg["X-Mailer"] = "BulkGmailMailer (permission-based sending)"

    # List-Unsubscribe headers for compliance
    if unsubscribe_url:
        parts = [f"<{unsubscribe_url}>"]
        if unsubscribe_mailto:
            parts.append(f"<mailto:{unsubscribe_mailto}>")
        msg["List-Unsubscribe"] = ", ".join(parts)
        msg["List-Unsubscribe-Post"] = "List-Unsubscribe=One-Click"

    # Text part first, then HTML (preferred last in alternative)
    if text_body:
        (body or msg).attach(MIMEText(text_body, "plain", "utf-8"))
    if html_body:
        (body or msg).attach(MIMEText(html_body, "html", "utf-8"))
    if body is not None:
        msg.attach(body)

    # Attachments
    for att in attachments:
        data = att["content"]
        ctype = att.get("mime_type") or "application/octet-stream"
        maintype, subtype = ctype.split("/", 1)
        if maintype == "text":
            part = MIMEText(data.decode("utf-8", errors="replace"), subtype, "utf-8")
            part.set_param("name", att["filename"], header="Content-Disposition")
        elif maintype == "image":
            part = MIMEImage(data, _subtype=subtype)
        elif maintype == "application" and subtype == "pdf":
            part = MIMEApplication(data, _subtype=subtype)
        else:
            part = MIMEBase(maintype, subtype)
            part.set_payload(data)
        part.add_header("Content-Disposition", "attachment", filename=att["filename"])
        msg.attach(part)
    return msg


def validate_html(html: str) -> List[str]:
    """Basic structural validation of rendered HTML. Returns list of problems."""
    problems = []
    if not html or not html.strip():
        problems.append("Empty HTML body")
        return problems
    from bs4 import BeautifulSoup
    soup = BeautifulSoup(html, "lxml")
    if soup.find(True) is None:
        problems.append("No HTML elements found")
    for tag in soup.find_all(["script", "iframe", "object", "embed", "form"]):
        problems.append(f"Disallowed tag <{tag.name}> present")
    for img in soup.find_all("img"):
        if not (img.get("src") or "").strip():
            problems.append("img without src")
    leftovers = re.findall(r"%([A-Za-z0-9_]+)%", html)
    if leftovers:
        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 html_to_image(html: str, width: int = 800) -> bytes:
    """Real HTML → PNG conversion via wkhtmltoimage (headless)."""
    import subprocess
    import tempfile
    import shutil
    exe = shutil.which("wkhtmltoimage")
    if not exe:
        raise HtmlValidationError("wkhtmltoimage is not installed — cannot convert HTML to image")
    with tempfile.TemporaryDirectory() as td:
        src = os.path.join(td, "in.html")
        dst = os.path.join(td, "out.png")
        with open(src, "w", encoding="utf-8") as f:
            f.write(html)
        try:
            subprocess.run(
                [exe, "--quiet", "--width", str(width), "--disable-smart-width", src, dst],
                check=True, capture_output=True, timeout=90)
            with open(dst, "rb") as f:
                return f.read()
        except subprocess.CalledProcessError as e:
            detail = (e.stderr or b"").decode("utf-8", "replace")[:200]
            raise HtmlValidationError(f"Image conversion failed: {detail}") from e


def _docx_emit(doc, el) -> None:
    """Recursively emit a parsed HTML node into the python-docx document."""
    from bs4 import NavigableString, Tag
    if isinstance(el, NavigableString):
        txt = str(el).strip()
        if txt:
            doc.add_paragraph(txt)
        return
    if not isinstance(el, Tag):
        return
    name = (el.name or "").lower()
    if name in ("style", "script", "head", "meta", "link", "title"):
        return
    if name in ("h1", "h2", "h3", "h4", "h5"):
        try:
            doc.add_heading(el.get_text(" ", strip=True), level=int(name[1]))
        except Exception:
            doc.add_paragraph(el.get_text(" ", strip=True))
    elif name in ("p", "div", "center", "section", "article", "font", "span"):
        text = el.get_text(" ", strip=True)
        if text:
            # avoid double-emitting when children were already captured above
            doc.add_paragraph(text)
        for c in el.find_all(["table", "ul", "ol"], recursive=False):
            _docx_emit(doc, c)
    elif name in ("ul", "ol"):
        style = "List Bullet" if name == "ul" else "List Number"
        for li in el.find_all("li", recursive=False):
            doc.add_paragraph(li.get_text(" ", strip=True) or "•", style=style)
    elif name == "table":
        rows = el.find_all("tr")
        if not rows:
            return
        ncols = max(len(r.find_all(["td", "th"])) for r in rows)
        t = doc.add_table(rows=len(rows), cols=ncols)
        t.style = "Table Grid"
        for ri, r in enumerate(rows):
            for ci, cell in enumerate(r.find_all(["td", "th"])):
                t.cell(ri, ci).text = cell.get_text(" ", strip=True)
    elif name == "img":
        alt = el.get("alt", "")
        if alt:
            doc.add_paragraph(f"[image: {alt}]")
    elif name in ("br", "hr"):
        pass
    else:
        for c in el.children:
            _docx_emit(doc, c)


def html_to_docx(html: str) -> bytes:
    """Real HTML → Word (.docx) conversion via python-docx.

    Preserves headings, paragraphs, bold structure as text, bullet/numbered
    lists and real Word tables (Table Grid style) from the HTML body."""
    try:
        from bs4 import BeautifulSoup
        from docx import Document
        soup = BeautifulSoup(html or "", "lxml")
        doc = Document()
        root = soup.body if soup.body is not None else soup
        for el in root.children:
            _docx_emit(doc, el)
        buf = io.BytesIO()
        doc.save(buf)
        return buf.getvalue()
    except HtmlValidationError:
        raise
    except Exception as e:
        raise HtmlValidationError(f"Word 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 = list(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, ext: str = "pdf") -> str:
    """Unique, deterministic filenames like Invoice_INV-XXXX.pdf per campaign rules."""
    inv = re.sub(r"[^A-Za-z0-9-]", "", invoice_number or "")
    stem = {"invoice": "Invoice", "order": "Order", "receipt": "Receipt"}.get(kind, "Document")
    fallback = {"invoice": "INV", "order": "ORD", "receipt": "RF"}.get(kind, "")
    suffix = inv or (fallback + secrets.token_hex(3) if fallback else secrets.token_hex(4))
    return f"{stem}_{suffix}.{ext}"
