"""PDF renderer — Markdown → reportlab.

Line-based Markdown parser mapping to reportlab flowables. Supports:
- Headings (# H1 .. ### H3)
- Bullet and numbered lists
- GFM tables
- Bold (**text**) and italic (*text*) via reportlab's inline HTML markup
- Chinese text rendering via CJK font auto-registration

Chinese font handling: tries common CJK font paths (macOS PingFang, Linux
Noto CJK, etc.). If none found, falls back to Helvetica — Chinese chars
will not render but the PDF is still valid. The fallback is logged.
"""

from __future__ import annotations

import logging
import re
from pathlib import Path
from typing import Any

from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import ParagraphStyle
from reportlab.lib.units import cm
from reportlab.platypus import (
    ListFlowable,
    ListItem,
    Paragraph,
    SimpleDocTemplate,
    Table,
    TableStyle,
)
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont

logger = logging.getLogger(__name__)

# Candidate CJK font paths (macOS, Linux, Windows)
_CJK_FONT_CANDIDATES = [
    "/System/Library/Fonts/PingFang.ttc",
    "/System/Library/Fonts/STHeiti Light.ttc",
    "/Library/Fonts/Arial Unicode.ttf",
    "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
    "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
    "/usr/share/fonts/wqy-zenhei/wqy-zenhei.ttc",
    "/usr/share/fonts/wqy-microhei/wqy-microhei.ttc",
    "C:/Windows/Fonts/msyh.ttc",
    "C:/Windows/Fonts/simsun.ttc",
]

_CJK_FONT_REGISTERED = False
_CJK_FONT_NAME = "Helvetica"  # fallback


def _register_cjk_font() -> None:
    """Try to register a CJK font for Chinese rendering. Falls back to Helvetica."""
    global _CJK_FONT_REGISTERED, _CJK_FONT_NAME
    if _CJK_FONT_REGISTERED:
        return
    for path in _CJK_FONT_CANDIDATES:
        if Path(path).exists():
            try:
                pdfmetrics.registerFont(TTFont("CJK", path))
                _CJK_FONT_NAME = "CJK"
                logger.info(f"Registered CJK font from {path}")
                break
            except Exception as exc:
                logger.debug(f"Failed to register {path}: {exc}")
    if _CJK_FONT_NAME == "Helvetica":
        logger.warning(
            "No CJK font found — Chinese characters may not render in PDF. "
            "Install NotoSansCJK or PingFang for Chinese support."
        )
    _CJK_FONT_REGISTERED = True


def _md_inline_to_reportlab(text: str) -> str:
    """Convert Markdown inline formatting to reportlab Paragraph markup.

    reportlab Paragraph supports a subset of HTML: <b>, <i>.
    **bold** → <b>bold</b>, *italic* → <i>italic</i>
    """
    # Escape XML special chars first (reportlab Paragraph parses XML)
    text = text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
    # Bold: **text** → <b>text</b>
    text = re.sub(r"\*\*(.+?)\*\*", r"<b>\1</b>", text)
    # Italic: *text* → <i>text</i> (but not inside <b> tags)
    text = re.sub(r"\*(.+?)\*", r"<i>\1</i>", text)
    return text


class PDFRenderer:
    """Render Markdown content into a PDF file via reportlab."""

    def render(self, markdown_content: str, output_path: Path) -> Path:
        """Render Markdown to PDF at output_path.

        Args:
            markdown_content: Markdown-formatted text.
            output_path: Destination .pdf path.

        Returns:
            The output_path.
        """
        _register_cjk_font()

        output_path.parent.mkdir(parents=True, exist_ok=True)
        doc = SimpleDocTemplate(
            str(output_path),
            pagesize=A4,
            topMargin=2 * cm,
            bottomMargin=2 * cm,
            leftMargin=2 * cm,
            rightMargin=2 * cm,
        )

        styles = self._build_styles()
        flowables: list[Any] = []
        lines = markdown_content.splitlines()
        i = 0

        while i < len(lines):
            line = lines[i]
            i += 1

            if not line.strip():
                continue

            # Heading
            heading_match = re.match(r"^(#{1,3})\s+(.+)$", line)
            if heading_match:
                level = len(heading_match.group(1))
                text = _md_inline_to_reportlab(heading_match.group(2).strip())
                style = styles[f"Heading{level}"]
                flowables.append(Paragraph(text, style))
                continue

            # GFM table
            if (
                line.lstrip().startswith("|")
                and i < len(lines)
                and re.match(r"^\s*\|[\s:|-]+\|\s*$", lines[i])
            ):
                table_lines = [line, lines[i]]
                i += 1
                while i < len(lines) and lines[i].lstrip().startswith("|"):
                    table_lines.append(lines[i])
                    i += 1
                flowables.append(self._build_table(table_lines, styles))
                continue

            # Bullet list
            bullet_match = re.match(r"^(\s*)[-*+]\s+(.+)$", line)
            if bullet_match:
                items = [bullet_match.group(2)]
                while i < len(lines):
                    m = re.match(r"^(\s*)[-*+]\s+(.+)$", lines[i])
                    if not m:
                        break
                    items.append(m.group(2))
                    i += 1
                list_items = [
                    ListItem(Paragraph(_md_inline_to_reportlab(item), styles["Normal"]))
                    for item in items
                ]
                flowables.append(ListFlowable(list_items, bulletType="bullet"))
                continue

            # Numbered list
            num_match = re.match(r"^(\s*)\d+\.\s+(.+)$", line)
            if num_match:
                items = [num_match.group(2)]
                while i < len(lines):
                    m = re.match(r"^(\s*)\d+\.\s+(.+)$", lines[i])
                    if not m:
                        break
                    items.append(m.group(2))
                    i += 1
                list_items = [
                    ListItem(Paragraph(_md_inline_to_reportlab(item), styles["Normal"]))
                    for item in items
                ]
                flowables.append(ListFlowable(list_items, bulletType="1"))
                continue

            # Plain paragraph
            text = _md_inline_to_reportlab(line)
            flowables.append(Paragraph(text, styles["Normal"]))

        doc.build(flowables)
        return output_path

    def _build_styles(self) -> dict[str, ParagraphStyle]:
        """Build paragraph styles using the registered CJK font."""
        font = _CJK_FONT_NAME
        return {
            "Normal": ParagraphStyle(
                "Normal", fontName=font, fontSize=11, leading=16, spaceAfter=6
            ),
            "Heading1": ParagraphStyle(
                "Heading1", fontName=font, fontSize=20, leading=26, spaceAfter=12, spaceBefore=12
            ),
            "Heading2": ParagraphStyle(
                "Heading2", fontName=font, fontSize=16, leading=22, spaceAfter=8, spaceBefore=10
            ),
            "Heading3": ParagraphStyle(
                "Heading3", fontName=font, fontSize=13, leading=18, spaceAfter=6, spaceBefore=8
            ),
        }

    def _build_table(self, table_lines: list[str], styles: dict[str, ParagraphStyle]) -> Table:
        """Parse GFM table lines into a reportlab Table flowable."""
        rows: list[list[str]] = []
        for idx, line in enumerate(table_lines):
            if idx == 1:  # skip separator
                continue
            cells = [c.strip() for c in line.strip().strip("|").split("|")]
            rows.append(cells)

        # Wrap each cell in a Paragraph for inline formatting + CJK support
        data = [
            [Paragraph(_md_inline_to_reportlab(cell), styles["Normal"]) for cell in row]
            for row in rows
        ]

        table = Table(data)
        table.setStyle(
            TableStyle(
                [
                    ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#E0E0E0")),
                    ("GRID", (0, 0), (-1, -1), 0.5, colors.grey),
                    ("VALIGN", (0, 0), (-1, -1), "TOP"),
                    ("LEFTPADDING", (0, 0), (-1, -1), 6),
                    ("RIGHTPADDING", (0, 0), (-1, -1), 6),
                    ("TOPPADDING", (0, 0), (-1, -1), 4),
                    ("BOTTOMPADDING", (0, 0), (-1, -1), 4),
                ]
            )
        )
        return table
