"""
PDF generation service using WeasyPrint.

Based on deep research findings, WeasyPrint is the optimal choice for
production Flask applications due to:
- Pure Python (no external binaries except Pango)
- Modern CSS3 support
- Active maintenance (v66.0 as of July 2025)
- Good paged media features
"""

import io
import platform
from html import escape
from typing import Optional, Dict, Any
import markdown  # type: ignore[import-untyped]
from loguru import logger

from ...security import validate_url


# WeasyPrint pulls in Pango/Cairo/fontTools — a heavy, multi-second import.
# This module is imported eagerly during blueprint registration
# (research_routes -> pdf_service), so importing WeasyPrint at module load
# blocked web-server cold start by ~20s on CPU-constrained CI runners
# (issue #4431: "cold heavy-import under CI 2-core starvation"). PDF export is
# a rare, on-demand operation, so the import is deferred to first use.
#
# These names stay module-level (filled in by _ensure_weasyprint) so the
# render path — and the tests that patch them — keep working as before; they
# are just populated lazily instead of at import time.
HTML = None  # type: ignore[assignment,misc]
CSS = None  # type: ignore[assignment,misc]
# None until the import is attempted; True/False once determined.
WEASYPRINT_AVAILABLE: Optional[bool] = None


def _ensure_weasyprint() -> None:
    """Import WeasyPrint on first use, populating the module-level globals.

    Idempotent: the import is attempted once and the outcome cached in
    ``WEASYPRINT_AVAILABLE``. Handles the same ``(OSError, ImportError)``
    failure modes (e.g. missing Pango/Cairo system libraries) the original
    module-level guard did.
    """
    global HTML, CSS, WEASYPRINT_AVAILABLE, _URL_FETCHER
    if WEASYPRINT_AVAILABLE is not None:
        return
    try:
        from weasyprint import HTML as _HTML, CSS as _CSS
        from weasyprint.urls import URLFetcher

        HTML, CSS = _HTML, _CSS
        _URL_FETCHER = URLFetcher(allow_redirects=False)
        WEASYPRINT_AVAILABLE = True
    except (OSError, ImportError):
        WEASYPRINT_AVAILABLE = False
        logger.warning("WeasyPrint not available — PDF export will be disabled")


def weasyprint_available() -> bool:
    """Return True when WeasyPrint and its system libraries can be imported.

    Triggers the lazy import on first call.
    """
    _ensure_weasyprint()
    return bool(WEASYPRINT_AVAILABLE)


_WEASYPRINT_DOCS_URL = (
    "https://doc.courtbouillon.org/weasyprint/stable/first_steps.html"
)


class UnsafePDFResourceURLError(ValueError):
    """Subclasses ValueError so WeasyPrint skips the resource instead of aborting the render."""


# Populated by _ensure_weasyprint() on first PDF use. The URLFetcher preserves
# the allow_redirects=False posture that default_url_fetcher hard-coded.
# Redirects disabled keeps the SSRF guard airtight — validate_url only inspects
# the initial URL, so a 30x to a cloud metadata endpoint (see
# ssrf_validator.ALWAYS_BLOCKED_METADATA_IPS) would otherwise slip past.
_URL_FETCHER = None


def _safe_url_fetcher(url):
    """WeasyPrint url_fetcher that blocks SSRF targets (GHSA-fj2m-qvh9-jq4q)."""
    if not validate_url(url):
        logger.warning(f"Blocked unsafe URL in PDF rendering: {url}")
        raise UnsafePDFResourceURLError(
            f"Blocked unsafe URL in PDF rendering: {url}"
        )
    _ensure_weasyprint()
    return _URL_FETCHER.fetch(url)


class MissingPDFDependencyError(RuntimeError):
    """Raised when WeasyPrint system libraries are unavailable.

    Distinct from generic RuntimeError so the web layer can surface this
    message to users without also exposing unrelated RuntimeErrors
    (e.g., pandoc subprocess stderr from ODT export).
    """


def get_weasyprint_install_instructions() -> str:
    """Return platform-specific install instructions for WeasyPrint system deps."""
    system = platform.system()
    if system == "Darwin":
        return (
            "PDF export requires WeasyPrint system libraries (Pango, Cairo, GLib).\n"
            "Install with: brew install weasyprint\n"
            f"See: {_WEASYPRINT_DOCS_URL}#macos"
        )
    if system == "Linux":
        return (
            "PDF export requires WeasyPrint system libraries (Pango, Cairo, GLib).\n"
            f"See: {_WEASYPRINT_DOCS_URL}#linux"
        )
    if system == "Windows":
        return (
            "PDF export requires Pango system libraries.\n"
            f"See: {_WEASYPRINT_DOCS_URL}#windows"
        )
    return (
        "PDF export requires WeasyPrint system libraries (Pango, Cairo, GLib).\n"
        f"See: {_WEASYPRINT_DOCS_URL}"
    )


# Default stylesheet for PDF export. Exposed as a module-level constant so
# tests can assert against the source string (WeasyPrint's CSS object does
# not retain its input).
#
# CJK families are listed as fallbacks so WeasyPrint substitutes a
# glyph-bearing font when the primary stack lacks coverage. Without
# this, Chinese/Japanese/Korean text disappears silently from the
# PDF even though it renders fine in the HTML view (issue #4055).
# Glyphs still require the corresponding system font (e.g.
# fonts-noto-cjk) to actually be installed.
#
# The emoji families at the tail of each stack cover emoji
# codepoints that Arial and the CJK fonts lack entirely — without
# them, emojis in the markdown render as "tofu" boxes in the PDF
# even though they look fine in the HTML view. Color first
# (preferred when WeasyPrint can render the CBDT/CBLC bitmap
# tables — the format Debian's fonts-noto-color-emoji ships),
# then a monochrome fallback for renderers without CBDT support.
# Note: "Noto Color Emoji" is provided by fonts-noto-color-emoji;
# the monochrome "Noto Emoji" is a separate package
# (fonts-noto-emoji / fonts-noto-extra) and is a best-effort
# fallback for hosts that happen to have it installed.
MINIMAL_CSS = """
@page {
    size: A4;
    margin: 1.5cm;
}

body {
    font-family: Arial, "Noto Sans CJK SC", "Noto Sans CJK TC",
        "Noto Sans CJK JP", "Noto Sans CJK KR", "Noto Sans SC",
        "PingFang SC", "PingFang TC", "Hiragino Sans",
        "Hiragino Kaku Gothic ProN", "Apple SD Gothic Neo",
        "Microsoft YaHei", "Microsoft JhengHei",
        "Yu Gothic", "Malgun Gothic", "SimSun",
        "Noto Color Emoji", "Noto Emoji", "Segoe UI Emoji",
        "Apple Color Emoji", sans-serif;
    font-size: 10pt;
    line-height: 1.4;
}

table {
    border-collapse: collapse;
    width: 100%;
    margin: 0.5em 0;
}

th, td {
    border: 1px solid #ccc;
    padding: 6px;
    text-align: left;
}

th {
    background-color: #f0f0f0;
}

h1 { font-size: 16pt; margin: 0.5em 0; }
h2 { font-size: 14pt; margin: 0.5em 0; }
h3 { font-size: 12pt; margin: 0.5em 0; }
h4 { font-size: 11pt; margin: 0.5em 0; font-weight: bold; }
h5 { font-size: 10pt; margin: 0.5em 0; font-weight: bold; }
h6 { font-size: 10pt; margin: 0.5em 0; }

code, pre {
    font-family: monospace, "Noto Sans Mono CJK SC",
        "Noto Sans Mono CJK TC", "Noto Sans Mono CJK JP",
        "Noto Sans Mono CJK KR", "Noto Sans CJK SC",
        "PingFang SC", "Hiragino Sans", "Apple SD Gothic Neo",
        "Microsoft YaHei", "SimSun",
        "Noto Color Emoji", "Noto Emoji", "Segoe UI Emoji",
        "Apple Color Emoji";
    background-color: #f5f5f5;
}

code {
    padding: 1px 3px;
}

pre {
    padding: 8px;
    overflow-x: auto;
}

a {
    color: #0066cc;
    text-decoration: none;
}
"""


class PDFService:
    """Service for converting markdown to PDF using WeasyPrint."""

    def __init__(self):
        """Initialize PDF service with minimal CSS for readability."""
        # Defer-load WeasyPrint (lazy import) before using CSS, then
        # build the stylesheet from the module-level MINIMAL_CSS constant.
        _ensure_weasyprint()
        self.minimal_css = CSS(string=MINIMAL_CSS)

    def markdown_to_pdf(
        self,
        markdown_content: str,
        title: Optional[str] = None,
        metadata: Optional[Dict[str, Any]] = None,
        custom_css: Optional[str] = None,
    ) -> bytes:
        """
        Convert markdown content to PDF.

        Args:
            markdown_content: The markdown text to convert
            title: Optional title for the document
            metadata: Optional metadata dict (author, date, etc.)
            custom_css: Optional CSS string layered on top of the default
                stylesheet. Rules here win on equal specificity via the
                cascade, but the default's CJK/emoji font fallbacks and
                page setup are always applied.

        Returns:
            PDF file as bytes

        Note:
            WeasyPrint memory usage can spike with large documents.
            Production deployments should implement:
            - Memory limits (ulimit)
            - Timeouts (30-60 seconds)
            - Worker recycling after 100 requests
        """
        _ensure_weasyprint()
        try:
            # Convert markdown to HTML
            html_content = self._markdown_to_html(
                markdown_content, title, metadata
            )

            # url_fetcher blocks SSRF targets reachable via body/citation URLs.
            html_doc = HTML(string=html_content, url_fetcher=_safe_url_fetcher)

            # Always apply the default stylesheet first, then layer any
            # caller-provided custom_css on top. WeasyPrint resolves
            # conflicts by cascade order, so later stylesheets win on
            # equal specificity — this preserves the CJK and emoji font
            # fallbacks in MINIMAL_CSS even when a caller supplies their
            # own CSS, while still letting them override any default.
            css_list = [self.minimal_css]
            if custom_css:
                css_list.append(CSS(string=custom_css))

            # Generate PDF
            # Use BytesIO to get bytes instead of writing to file
            pdf_buffer = io.BytesIO()
            html_doc.write_pdf(pdf_buffer, stylesheets=css_list)

            # Get the PDF bytes
            pdf_bytes = pdf_buffer.getvalue()
            pdf_buffer.close()

            logger.info(f"Generated PDF, size: {len(pdf_bytes)} bytes")
            return pdf_bytes

        except Exception:
            logger.exception("Error generating PDF")
            raise

    def _markdown_to_html(
        self,
        markdown_content: str,
        title: Optional[str] = None,
        metadata: Optional[Dict[str, Any]] = None,
    ) -> str:
        """
        Convert markdown to HTML with proper structure.

        Uses Python-Markdown with extensions for:
        - Tables
        - Fenced code blocks
        - Table of contents
        - Footnotes
        """
        # Parse markdown with extensions
        md = markdown.Markdown(
            extensions=[
                "tables",
                "fenced_code",
                "footnotes",
                "toc",
                "nl2br",  # Convert newlines to <br>
                "sane_lists",
                "meta",
            ]
        )

        html_body = md.convert(markdown_content)

        # Build complete HTML document
        html_parts = ["<!DOCTYPE html><html><head>"]
        html_parts.append('<meta charset="utf-8">')

        if title:
            html_parts.append(f"<title>{escape(title)}</title>")

        if metadata:
            for key, value in metadata.items():
                html_parts.append(
                    f'<meta name="{escape(str(key))}" content="{escape(str(value))}">'
                )

        html_parts.append("</head><body>")

        # Add the markdown content directly without any extra title or metadata
        html_parts.append(html_body)

        # Add footer with LDR attribution
        html_parts.append("""
            <div style="margin-top: 2em; padding-top: 1em; border-top: 1px solid #ddd; font-size: 9pt; color: #666; text-align: center;">
                Generated by <a href="https://github.com/LearningCircuit/local-deep-research" style="color: #0066cc;">LDR - Local Deep Research</a> | Open Source AI Research Assistant
            </div>
        """)

        html_parts.append("</body></html>")

        return "".join(html_parts)


# Singleton instance
_pdf_service = None


def get_pdf_service() -> PDFService:
    """Get or create the PDF service singleton.

    Raises:
        MissingPDFDependencyError: If WeasyPrint system libraries are not
            available, with platform-specific installation instructions.
    """
    _ensure_weasyprint()
    if not WEASYPRINT_AVAILABLE:
        raise MissingPDFDependencyError(get_weasyprint_install_instructions())
    global _pdf_service
    if _pdf_service is None:
        _pdf_service = PDFService()
    return _pdf_service
