"""PDF-Exporter für die PBI Documentation by Oliver Gropp.

Konvertiert gerendertes Markdown in ein PDF-Dokument mittels fpdf2.
fpdf2 ist pure Python, hat keine nativen Abhängigkeiten und wird von
PyInstaller automatisch gebündelt.
"""
from __future__ import annotations

import base64
import io
import json
import os
import re

from fpdf import FPDF


class _MarkdownPDF(FPDF):
    """FPDF-Subklasse mit Markdown-Tabellen-Support."""

    def __init__(self):
        super().__init__(orientation="L", format="A4")
        self.set_auto_page_break(auto=True, margin=15)
        # Unicode-Font registrieren (Windows Arial oder Fallback)
        font_path = os.path.join(os.environ.get("WINDIR", r"C:\Windows"), "Fonts", "arial.ttf")
        bold_path = os.path.join(os.environ.get("WINDIR", r"C:\Windows"), "Fonts", "arialbd.ttf")
        italic_path = os.path.join(os.environ.get("WINDIR", r"C:\Windows"), "Fonts", "ariali.ttf")
        if os.path.isfile(font_path):
            self.add_font("UniFont", "", font_path)
            self.add_font("UniFont", "B", bold_path if os.path.isfile(bold_path) else font_path)
            self.add_font("UniFont", "I", italic_path if os.path.isfile(italic_path) else font_path)
            self._font_name = "UniFont"
        else:
            self._font_name = "Helvetica"
        self.add_page()
        self.set_font(self._font_name, size=9)
        self._in_code_block = False

    def _render_line(self, line: str) -> None:
        """Rendert eine einzelne Markdown-Zeile."""
        f = self._font_name
        # Überschriften
        if line.startswith("# "):
            self.set_font(f, "B", 16)
            self.cell(0, 8, line[2:].strip(), new_x="LMARGIN", new_y="NEXT")
            self.set_font(f, size=9)
            return
        if line.startswith("## "):
            self.set_font(f, "B", 13)
            self.cell(0, 7, line[3:].strip(), new_x="LMARGIN", new_y="NEXT")
            self.set_font(f, size=9)
            return
        if line.startswith("### "):
            self.set_font(f, "B", 11)
            self.cell(0, 6, line[4:].strip(), new_x="LMARGIN", new_y="NEXT")
            self.set_font(f, size=9)
            return
        if line.startswith("#### "):
            self.set_font(f, "B", 10)
            self.cell(0, 6, line[5:].strip(), new_x="LMARGIN", new_y="NEXT")
            self.set_font(f, size=9)
            return

        # Code-Block Toggle
        if line.startswith("```"):
            self._in_code_block = not self._in_code_block
            return

        if self._in_code_block:
            self.set_font("Courier", size=8)
            self.cell(0, 4, "  " + line, new_x="LMARGIN", new_y="NEXT")
            self.set_font(f, size=9)
            return

        # Horizontale Linie
        if line.strip() in ("---", "***", "___"):
            y = self.get_y()
            self.line(self.l_margin, y, self.w - self.r_margin, y)
            self.ln(3)
            return

        # Leerzeile
        if not line.strip():
            self.ln(3)
            return

        # Normaler Text (mit einfachem Bold/Italic-Support)
        self._write_formatted(line)
        self.ln(4)

    def _write_formatted(self, text: str) -> None:
        """Schreibt Text mit **bold** und *italic* Unterstützung."""
        f = self._font_name
        parts = re.split(r"(\*\*.*?\*\*|\*.*?\*)", text)
        for part in parts:
            if part.startswith("**") and part.endswith("**"):
                self.set_font(f, "B", 9)
                self.write(4, part[2:-2])
                self.set_font(f, size=9)
            elif part.startswith("*") and part.endswith("*"):
                self.set_font(f, "I", 9)
                self.write(4, part[1:-1])
                self.set_font(f, size=9)
            else:
                self.write(4, part)

    def render_table(self, header: list[str], rows: list[list[str]]) -> None:
        """Rendert eine Markdown-Tabelle mit dynamischer Spaltenbreite."""
        f = self._font_name
        num_cols = len(header)
        available = self.w - self.l_margin - self.r_margin
        col_width = available / num_cols

        # Header
        self.set_font(f, "B", 8)
        for cell_text in header:
            self.cell(col_width, 5, cell_text.strip(), border=1)
        self.ln()

        # Rows
        self.set_font(f, size=8)
        for row in rows:
            # Zeilenhöhe berechnen basierend auf Inhalt
            max_lines = 1
            for i, cell_text in enumerate(row):
                text = cell_text.strip() if i < len(row) else ""
                lines = self.multi_cell(col_width, 4, text, border=0, split_only=True)
                max_lines = max(max_lines, len(lines))

            row_height = max(5, max_lines * 4)
            # Seitenumbruch prüfen
            if self.get_y() + row_height > self.h - self.b_margin:
                self.add_page()

            y_start = self.get_y()
            x_start = self.get_x()
            for i in range(num_cols):
                text = row[i].strip() if i < len(row) else ""
                self.set_xy(x_start + i * col_width, y_start)
                self.multi_cell(col_width, 4, text, border=1)

            self.set_y(y_start + row_height)

    def render_mermaid_image(self, png_bytes: bytes, width: float, height: float) -> None:
        """Bettet ein gerendertes Mermaid-Diagramm (PNG) ein.

        Skaliert auf die verfügbare Seitenbreite unter Wahrung des
        Seitenverhältnisses und begrenzt die Höhe auf eine Seite.
        """
        avail_w = self.w - self.l_margin - self.r_margin
        max_h = self.h - self.t_margin - self.b_margin

        disp_w = avail_w
        disp_h = 0.0
        if width and height and width > 0 and height > 0:
            disp_h = disp_w * float(height) / float(width)
            if disp_h > max_h:
                disp_h = max_h
                disp_w = disp_h * float(width) / float(height)

        # Seitenumbruch, falls das Bild nicht mehr auf die aktuelle Seite passt
        if disp_h and self.get_y() + disp_h > self.h - self.b_margin:
            self.add_page()

        x = (self.w - disp_w) / 2  # horizontal zentrieren
        self.image(io.BytesIO(png_bytes), x=x, w=disp_w, h=disp_h)
        self.ln(4)


def images_from_json(images_json: str) -> list[dict | None]:
    """Dekodiert die vom Frontend gelieferten gerenderten Mermaid-Bilder.

    Erwartet einen JSON-String mit einer Liste von Objekten der Form
    ``{"dataUrl": "data:image/png;base64,...", "width": <px>, "height": <px>}``.
    Einträge, die nicht dekodiert werden können, werden zu ``None``
    (das löst im PDF den Fallback auf Rohtext aus).

    Returns:
        Liste mit ``{"png": bytes, "width": int, "height": int}`` oder ``None``,
        in derselben Reihenfolge wie die ```mermaid-Blöcke im Dokument.
    """
    if not images_json:
        return []
    try:
        raw = json.loads(images_json)
    except (ValueError, TypeError):
        return []
    if not isinstance(raw, list):
        return []

    result: list[dict | None] = []
    for item in raw:
        if not isinstance(item, dict):
            result.append(None)
            continue
        data_url = item.get("dataUrl") or ""
        b64 = data_url.split(",", 1)[1] if "," in data_url else data_url
        try:
            png = base64.b64decode(b64) if b64 else b""
        except (ValueError, TypeError):
            png = b""
        if png:
            result.append({
                "png": png,
                "width": item.get("width") or 0,
                "height": item.get("height") or 0,
            })
        else:
            result.append(None)
    return result


def export_pdf(
    markdown_content: str,
    output_path: str,
    mermaid_images: list[dict | None] | None = None,
) -> None:
    """Konvertiert Markdown-Inhalt in ein PDF und speichert es.

    Args:
        markdown_content: Gerendertes Markdown (ohne Platzhalter).
        output_path: Ziel-Pfad für die PDF-Datei.
        mermaid_images: Optionale, in Dokumentreihenfolge sortierte Liste der
            gerenderten ```mermaid-Diagramme (``{"png", "width", "height"}``
            oder ``None``). Fehlt ein Bild, wird der Mermaid-Quelltext als
            Codeblock ausgegeben (Fallback wie bisher).

    Raises:
        OSError: Wenn die Datei nicht geschrieben werden kann.
    """
    images = mermaid_images or []
    os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)

    pdf = _MarkdownPDF()
    lines = markdown_content.split("\n")
    i = 0
    mermaid_idx = 0

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

        # Mermaid-Codeblock erkennen und als Bild einbetten (mit Fallback)
        if line.strip() == "```mermaid":
            j = i + 1
            block_lines: list[str] = []
            while j < len(lines) and lines[j].strip() != "```":
                block_lines.append(lines[j])
                j += 1

            image = images[mermaid_idx] if mermaid_idx < len(images) else None
            mermaid_idx += 1

            if image and image.get("png"):
                pdf.render_mermaid_image(
                    image["png"], image.get("width", 0), image.get("height", 0)
                )
            else:
                # Fallback: Mermaid-Quelltext als Codeblock ausgeben
                pdf._render_line("```mermaid")
                for block_line in block_lines:
                    pdf._render_line(block_line)
                pdf._render_line("```")

            i = j + 1  # hinter die schließende ```-Zeile springen
            continue

        # Tabelle erkennen (Zeile mit | am Anfang und Ende)
        if "|" in line and line.strip().startswith("|"):
            header_cells = [c for c in line.split("|")[1:-1]]
            # Separator-Zeile überspringen
            if i + 1 < len(lines) and re.match(r"^\|[\s\-:|]+\|$", lines[i + 1].strip()):
                i += 2
                rows = []
                while i < len(lines) and "|" in lines[i] and lines[i].strip().startswith("|"):
                    row_cells = [c for c in lines[i].split("|")[1:-1]]
                    rows.append(row_cells)
                    i += 1
                pdf.render_table(header_cells, rows)
                pdf.ln(3)
                continue

        pdf._render_line(line)
        i += 1

    pdf.output(output_path)
