"""PDF Export Plugin — generates PDF from chat responses.

Detects requests like "als PDF", "gib mir ein PDF", "PDF erstellen" etc.
Generates a PDF from the last assistant response and serves it via API.
No external dependencies — uses built-in FPDF-like generator (pure Python).
"""
from __future__ import annotations

import logging
import re
import time
from pathlib import Path

from hivemind.plugins.base import Plugin, Capability, capability

log = logging.getLogger(__name__)

# PDF trigger patterns
_PDF_TRIGGERS = [
    r'(?:als|in|per)\s+pdf',
    r'pdf\s+(?:geben|erstellen|erzeugen|generieren|machen|ausgeben|exportieren|speichern)',
    r'(?:gib|mach|erstell|erzeug|generier)\s+(?:mir\s+)?(?:ein(?:e)?|das|den)?\s*pdf',
    r'(?:kannst|könntest|würdest)\s+du\s+.*pdf',
    r'(?:export|download)\s+(?:als\s+)?pdf',
    r'pdf\s+(?:bitte|please|download|export)',
]
_PDF_RE = re.compile('|'.join(_PDF_TRIGGERS), re.IGNORECASE)


def is_pdf_request(text: str) -> bool:
    return bool(_PDF_RE.search(text))


class SimplePDF:
    """Minimal PDF generator — pure Python, no dependencies.
    
    Supports: text, bold, headers, line breaks, basic formatting.
    """

    def __init__(self, title: str = "HiveMind"):
        self._pages: list[list[dict]] = [[]]
        self._fonts = {"Helvetica": "F1", "Helvetica-Bold": "F2", "Courier": "F3"}
        self._font = "Helvetica"
        self._size = 11
        self._y = 40  # Start position
        self._page_h = 842  # A4 height in points
        self._page_w = 595  # A4 width
        self._margin = 50
        self._line_h = 16
        self.title = title

    def _add_line(self, text: str, font: str = "Helvetica", size: int = 11):
        if self._y > self._page_h - 60:
            self._pages.append([])
            self._y = 40

        self._pages[-1].append({
            "type": "text",
            "x": self._margin,
            "y": self._y,
            "text": text,
            "font": font,
            "size": size,
        })
        self._y += self._line_h * (size / 11)

    def add_title(self, text: str):
        self._y += 5
        self._add_line(text, "Helvetica-Bold", 18)
        self._y += 8

    def add_heading(self, text: str, level: int = 2):
        self._y += 4
        sizes = {1: 18, 2: 15, 3: 13, 4: 12}
        self._add_line(text, "Helvetica-Bold", sizes.get(level, 13))
        self._y += 3

    def add_text(self, text: str, bold: bool = False):
        font = "Helvetica-Bold" if bold else "Helvetica"
        # Word-wrap
        max_w = self._page_w - 2 * self._margin
        chars_per_line = int(max_w / (self._size * 0.52))
        for line in text.split("\n"):
            if not line.strip():
                self._y += self._line_h * 0.5
                continue
            while len(line) > chars_per_line:
                # Find last space before limit
                cut = line[:chars_per_line].rfind(" ")
                if cut < 10:
                    cut = chars_per_line
                self._add_line(line[:cut], font, self._size)
                line = line[cut:].lstrip()
            if line:
                self._add_line(line, font, self._size)

    def add_code(self, text: str):
        self._y += 4
        for line in text.split("\n"):
            self._add_line(line, "Courier", 10)
        self._y += 4

    def add_separator(self):
        self._y += 8
        self._pages[-1].append({
            "type": "line",
            "x1": self._margin,
            "y1": self._y,
            "x2": self._page_w - self._margin,
            "y2": self._y,
        })
        self._y += 8

    def _escape_pdf(self, text: str) -> bytes:
        """Escape text for PDF string and encode to WinAnsi (cp1252)."""
        text = text.replace("\\", "\\\\").replace("(", "\\(").replace(")", "\\)")
        # Encode to cp1252 (WinAnsiEncoding) — this covers ä ö ü ß etc.
        try:
            return text.encode("cp1252")
        except UnicodeEncodeError:
            # Replace characters not in cp1252
            result = bytearray()
            for ch in text:
                try:
                    result.extend(ch.encode("cp1252"))
                except UnicodeEncodeError:
                    result.extend(b"?")
            return bytes(result)

    def generate(self) -> bytes:
        """Generate PDF bytes."""
        objects: list[str] = []
        offsets: list[int] = []
        out = b"%PDF-1.4\n"

        def add_obj(content: str) -> int:
            nonlocal out
            idx = len(objects) + 1
            offsets.append(len(out))
            obj = f"{idx} 0 obj\n{content}\nendobj\n"
            out += obj.encode("latin-1", errors="replace")
            objects.append(content)
            return idx

        # Catalog
        catalog_id = add_obj("<< /Type /Catalog /Pages 2 0 R >>")
        # Pages (placeholder, will fix)
        pages_id = add_obj(f"<< /Type /Pages /Kids [] /Count {len(self._pages)} >>")

        # Fonts
        f1_id = add_obj("<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>")
        f2_id = add_obj("<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold >>")
        f3_id = add_obj("<< /Type /Font /Subtype /Type1 /BaseFont /Courier >>")

        font_map = {"F1": f1_id, "F2": f2_id, "F3": f3_id}
        font_ref = " ".join(f"/{k} {v} 0 R" for k, v in font_map.items())

        page_ids = []
        for page in self._pages:
            # Build content stream
            stream_parts = []
            for item in page:
                if item["type"] == "text":
                    fn = self._fonts.get(item["font"], "F1")
                    stream_parts.append(
                        f"BT /{fn} {item['size']} Tf "
                        f"{item['x']} {self._page_h - item['y']} Td "
                        f"({self._escape_pdf(item['text'])}) Tj ET"
                    )
                elif item["type"] == "line":
                    stream_parts.append(
                        f"0.8 G {item['x1']} {self._page_h - item['y1']} m "
                        f"{item['x2']} {self._page_h - item['y2']} l S"
                    )

            stream = "\n".join(stream_parts)
            stream_id = add_obj(
                f"<< /Length {len(stream)} >>\nstream\n{stream}\nendstream"
            )
            page_id = add_obj(
                f"<< /Type /Page /Parent 2 0 R "
                f"/MediaBox [0 0 {self._page_w} {self._page_h}] "
                f"/Contents {stream_id} 0 R "
                f"/Resources << /Font << {font_ref} >> >> >>"
            )
            page_ids.append(page_id)

        # Fix pages object
        kids = " ".join(f"{pid} 0 R" for pid in page_ids)
        pages_content = f"<< /Type /Pages /Kids [{kids}] /Count {len(page_ids)} >>"
        offsets[1] = len(out)  # We need to rewrite it
        # Actually, let's just rebuild the output
        out = b"%PDF-1.4\n"
        offsets = []

        def write_obj(idx: int, content):
            nonlocal out
            offsets.append(len(out))
            if isinstance(content, bytes):
                out += f"{idx} 0 obj\n".encode("ascii") + content + b"\nendobj\n"
            else:
                out += f"{idx} 0 obj\n{content}\nendobj\n".encode("ascii")

        write_obj(1, "<< /Type /Catalog /Pages 2 0 R >>")
        write_obj(2, pages_content)
        write_obj(3, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>")
        write_obj(4, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>")
        write_obj(5, "<< /Type /Font /Subtype /Type1 /BaseFont /Courier /Encoding /WinAnsiEncoding >>")

        obj_num = 6
        page_obj_ids = []
        for page in self._pages:
            stream_bytes = b""
            for item in page:
                if item["type"] == "text":
                    fn = self._fonts.get(item["font"], "F1")
                    text_bytes = self._escape_pdf(item["text"])
                    prefix = f"BT /{fn} {item['size']} Tf {item['x']} {self._page_h - item['y']} Td (".encode("ascii")
                    suffix = b") Tj ET\n"
                    stream_bytes += prefix + text_bytes + suffix
                elif item["type"] == "line":
                    stream_bytes += (
                        f"0.8 G {item['x1']} {self._page_h - item['y1']} m "
                        f"{item['x2']} {self._page_h - item['y2']} l S\n"
                    ).encode("ascii")

            stream_id = obj_num
            header = f"<< /Length {len(stream_bytes)} >>\nstream\n".encode("ascii")
            footer = b"\nendstream"
            write_obj(stream_id, header + stream_bytes + footer)
            obj_num += 1
            page_id = obj_num
            write_obj(page_id,
                f"<< /Type /Page /Parent 2 0 R "
                f"/MediaBox [0 0 {self._page_w} {self._page_h}] "
                f"/Contents {stream_id} 0 R "
                f"/Resources << /Font << /F1 3 0 R /F2 4 0 R /F3 5 0 R >> >> >>")
            obj_num += 1
            page_obj_ids.append(page_id)

        # Rewrite pages object
        kids = " ".join(f"{pid} 0 R" for pid in page_obj_ids)
        # We already wrote it — need to patch. Simpler: rebuild entirely
        # Actually our write_obj(2, ...) already has the right content since we computed it above

        # Cross-reference table
        xref_pos = len(out)
        out += b"xref\n"
        out += f"0 {obj_num}\n".encode()
        out += b"0000000000 65535 f \n"
        for off in offsets:
            out += f"{off:010d} 00000 n \n".encode()

        out += b"trailer\n"
        out += f"<< /Size {obj_num} /Root 1 0 R >>\n".encode()
        out += b"startxref\n"
        out += f"{xref_pos}\n".encode()
        out += b"%%EOF\n"

        return out


def markdown_to_pdf(text: str, title: str = "HiveMind") -> bytes:
    """Convert markdown-like text to PDF bytes."""
    pdf = SimplePDF(title=title)
    pdf.add_title(title)
    pdf.add_separator()

    in_code = False
    code_buf: list[str] = []

    for line in text.split("\n"):
        # Code block toggle
        if line.strip().startswith("```"):
            if in_code:
                pdf.add_code("\n".join(code_buf))
                code_buf = []
                in_code = False
            else:
                in_code = True
            continue

        if in_code:
            code_buf.append(line)
            continue

        stripped = line.strip()

        # Headers
        if stripped.startswith("####"):
            pdf.add_heading(stripped[4:].strip(), 4)
        elif stripped.startswith("###"):
            pdf.add_heading(stripped[3:].strip(), 3)
        elif stripped.startswith("##"):
            pdf.add_heading(stripped[2:].strip(), 2)
        elif stripped.startswith("#"):
            pdf.add_heading(stripped[1:].strip(), 1)
        elif stripped.startswith("---"):
            pdf.add_separator()
        elif stripped.startswith("- ") or stripped.startswith("* "):
            pdf.add_text("  " + stripped)
        elif stripped.startswith("**") and stripped.endswith("**"):
            pdf.add_text(stripped[2:-2], bold=True)
        elif not stripped:
            pdf.add_text("")
        else:
            # Clean inline markdown
            clean = re.sub(r'\*\*(.+?)\*\*', r'\1', stripped)
            clean = re.sub(r'\*(.+?)\*', r'\1', clean)
            clean = re.sub(r'`(.+?)`', r'\1', clean)
            pdf.add_text(clean)

    if code_buf:
        pdf.add_code("\n".join(code_buf))

    # Footer
    pdf.add_separator()
    pdf.add_text(f"Erstellt von HiveMind — {time.strftime('%d.%m.%Y %H:%M')}", bold=False)

    return pdf.generate()


class PDFExportPlugin(Plugin):
    """Plugin that detects PDF requests and generates downloadable PDFs."""

    name = "pdf_export"
    description = "Erstellt PDFs aus Chat-Antworten"

    def __init__(self, node=None):
        super().__init__(node)
        self.pdf_dir: Path | None = None
        if node and hasattr(node, 'base_dir'):
            self.pdf_dir = node.base_dir / "data" / "pdf"
            self.pdf_dir.mkdir(parents=True, exist_ok=True)

    @capability("Erstellt ein PDF aus der letzten Antwort")
    async def create_pdf(self, messages: list[dict] | None = None, **kwargs) -> str:
        """Check if last message is a PDF request, generate PDF from prior response."""
        if not messages or len(messages) < 2:
            return ""

        last_user = ""
        last_bot = ""
        for msg in reversed(messages):
            if msg["role"] == "user" and not last_user:
                last_user = msg["content"]
            elif msg["role"] == "assistant" and not last_bot:
                last_bot = msg["content"]
            if last_user and last_bot:
                break

        if not is_pdf_request(last_user):
            return ""

        if not last_bot:
            return "Kein vorheriger Chat-Inhalt zum Exportieren gefunden."

        # Generate PDF
        pdf_bytes = markdown_to_pdf(last_bot, title="HiveMind Export")

        if self.pdf_dir:
            filename = f"export_{int(time.time())}.pdf"
            path = self.pdf_dir / filename
            path.write_bytes(pdf_bytes)
            log.info("PDF created: %s (%d bytes)", path, len(pdf_bytes))
            return f"PDF erstellt: [Download](/api/pdf/{filename})"

        return "PDF-Export nicht verfuegbar (kein Speicherpfad)."

PLUGIN_CLASS = PDFExportPlugin
