"""
PDF report generation from Markdown.

Converts final_report.md to final_report.pdf with professional styling.
Uses fpdf2 (pure Python, no system dependencies).
"""

import logging
import re
from dataclasses import dataclass, field
from pathlib import Path

from fpdf import FPDF, XPos, YPos

from openscientist.exceptions import PDFGenerationError

logger = logging.getLogger(__name__)


@dataclass
class _MarkdownRenderState:
    """Parser state while walking markdown lines."""

    in_code_block: bool = False
    code_buffer: list[str] = field(default_factory=list)
    in_list: bool = False
    list_counter: int = 0
    in_table: bool = False
    table_rows: list[list[str]] = field(default_factory=list)


class ReportPDF(FPDF):
    """Custom PDF class for OpenScientist reports with professional styling."""

    def __init__(self) -> None:
        super().__init__()
        # Use DejaVu font for Unicode support (system-installed fonts)
        # Debian/Ubuntu installs fonts to /usr/share/fonts/truetype/dejavu/
        dejavu_path = "/usr/share/fonts/truetype/dejavu"
        self.add_font("DejaVu", "", f"{dejavu_path}/DejaVuSans.ttf")
        self.add_font("DejaVu", "B", f"{dejavu_path}/DejaVuSans-Bold.ttf")
        # Note: fonts-dejavu-core doesn't include italic variant, so we use regular for italic
        self.add_font("DejaVu", "I", f"{dejavu_path}/DejaVuSans.ttf")
        self.add_page()
        self.set_auto_page_break(auto=True, margin=15)

    def header(self) -> None:
        """Add header to each page (except first)."""
        if self.page_no() > 1:
            self.set_font("DejaVu", "I", 9)
            self.set_text_color(128, 128, 128)
            self.cell(
                0,
                10,
                "OpenScientist Scientific Report",
                new_x=XPos.LMARGIN,
                new_y=YPos.NEXT,
                align="R",
            )
            self.ln(2)

    def footer(self) -> None:
        """Add footer with page number."""
        self.set_y(-15)
        self.set_font("DejaVu", "I", 8)
        self.set_text_color(128, 128, 128)
        self.cell(0, 10, f"Page {self.page_no()}", align="C")

    def add_title(self, title: str) -> None:
        """Add report title."""
        self.set_font("DejaVu", "B", 24)
        self.set_text_color(44, 62, 80)
        self.multi_cell(0, 10, title, new_x=XPos.LMARGIN, new_y=YPos.NEXT)
        self.ln(5)

    def add_heading_1(self, text: str) -> None:
        """Add H1 heading."""
        self.ln(5)
        self.set_font("DejaVu", "B", 18)
        self.set_text_color(52, 73, 94)
        self.multi_cell(0, 10, text, new_x=XPos.LMARGIN, new_y=YPos.NEXT)
        self.ln(2)

    def add_heading_2(self, text: str) -> None:
        """Add H2 heading."""
        self.ln(4)
        self.set_font("DejaVu", "B", 14)
        self.set_text_color(93, 109, 126)
        self.multi_cell(0, 8, text, new_x=XPos.LMARGIN, new_y=YPos.NEXT)
        self.ln(2)

    def add_heading_3(self, text: str) -> None:
        """Add H3 heading."""
        self.ln(3)
        self.set_font("DejaVu", "B", 12)
        self.set_text_color(93, 109, 126)
        self.multi_cell(0, 7, text, new_x=XPos.LMARGIN, new_y=YPos.NEXT)
        self.ln(1)

    def add_paragraph(self, text: str) -> None:
        """Add body paragraph."""
        self.set_font("DejaVu", "", 11)
        self.set_text_color(51, 51, 51)
        # Process bold and italic markdown
        text = self._process_inline_formatting(text)
        self.multi_cell(0, 6, text, new_x=XPos.LMARGIN, new_y=YPos.NEXT, markdown=True)
        self.ln(2)

    def add_code_block(self, code: str) -> None:
        """Add code block."""
        self.ln(2)
        self.set_fill_color(245, 245, 245)
        self.set_font("DejaVu", "", 9)
        self.set_text_color(51, 51, 51)
        self.multi_cell(0, 5, code, new_x=XPos.LMARGIN, new_y=YPos.NEXT, fill=True, border=1)
        self.ln(2)

    def add_list_item(self, text: str, ordered: bool = False, indent: int = 0) -> None:
        """Add list item."""
        self.set_font("DejaVu", "", 11)
        self.set_text_color(51, 51, 51)
        bullet = "•" if not ordered else f"{indent + 1}."
        x_indent = 10 + (indent * 5)
        self.set_x(x_indent)
        text = self._process_inline_formatting(text)
        self.multi_cell(
            0, 6, f"{bullet} {text}", new_x=XPos.LMARGIN, new_y=YPos.NEXT, markdown=True
        )

    def add_footer(self) -> None:
        """Add OpenScientist attribution footer."""
        self.ln(10)
        self.line(10, self.get_y(), 200, self.get_y())
        self.ln(3)
        self.set_font("DejaVu", "I", 9)
        self.set_text_color(149, 165, 166)
        self.cell(
            0,
            5,
            "Generated by OpenScientist - Scientific Hypothesis Agent for Novel Discovery",
            new_x=XPos.LMARGIN,
            new_y=YPos.NEXT,
            align="C",
        )

    def add_table(self, rows: list[list[str]]) -> None:
        """Add a table from parsed markdown rows."""
        from fpdf.fonts import FontFace

        headings_style = FontFace(emphasis="BOLD", color=(255, 255, 255), fill_color=(52, 73, 94))
        self.set_font("DejaVu", "", 10)
        self.set_text_color(51, 51, 51)
        self.ln(2)
        with self.table(
            first_row_as_headings=True,
            headings_style=headings_style,
            borders_layout="HORIZONTAL_LINES",
            cell_fill_mode="ROWS",
            cell_fill_color=(245, 245, 250),
            line_height=6,
            padding=3,
            markdown=True,
        ) as table:
            for row_data in rows:
                row = table.row()
                for cell_text in row_data:
                    row.cell(cell_text)
        self.ln(3)

    @staticmethod
    def _process_inline_formatting(text: str) -> str:
        """Process bold and italic markdown for fpdf2."""
        # fpdf2 markdown support is limited, but we can try
        # **bold** -> still **bold** (fpdf2 handles this)
        # *italic* -> still *italic* (fpdf2 handles this)
        # `code` -> code (remove backticks as fpdf2 doesn't handle inline code well)
        return re.sub(r"`([^`]+)`", r"\1", text)


def markdown_to_pdf(
    markdown_path: Path, pdf_path: Path | None = None, add_footer: bool = True
) -> Path:
    """
    Convert Markdown report to PDF with professional styling.

    Args:
        markdown_path: Path to input Markdown file
        pdf_path: Path for output PDF (default: same name with .pdf extension)
        add_footer: Whether to add OpenScientist attribution footer

    Returns:
        Path to generated PDF file

    Raises:
        FileNotFoundError: If markdown_path doesn't exist
        ValueError: If conversion fails
    """
    if not markdown_path.exists():
        raise FileNotFoundError(f"Markdown file not found: {markdown_path}")

    if pdf_path is None:
        pdf_path = markdown_path.with_suffix(".pdf")

    logger.info("Converting %s to PDF...", markdown_path)

    try:
        markdown_content = _read_markdown_content(markdown_path)
        pdf = ReportPDF()
        state = _MarkdownRenderState()
        for line in markdown_content.split("\n"):
            if _handle_fenced_code_line(pdf, line, state):
                continue
            if state.in_code_block:
                state.code_buffer.append(line)
                continue
            if _handle_table_line(line, state):
                continue
            _flush_table(pdf, state)
            _render_markdown_line(pdf, line, state)

        _flush_table(pdf, state)
        if add_footer:
            pdf.add_footer()
        pdf.output(str(pdf_path))

        logger.info("PDF generated successfully: %s", pdf_path)
        return pdf_path

    except (OSError, RuntimeError, UnicodeEncodeError) as e:
        logger.error("PDF generation failed: %s", e, exc_info=True)
        raise PDFGenerationError(f"Failed to generate PDF: {e}") from e


def _read_markdown_content(markdown_path: Path) -> str:
    """Read markdown file text."""
    with open(markdown_path, encoding="utf-8") as file_obj:
        return file_obj.read()


def _handle_fenced_code_line(pdf: ReportPDF, line: str, state: _MarkdownRenderState) -> bool:
    """Handle markdown fenced-code markers and flush closed blocks."""
    if not line.strip().startswith("```"):
        return False
    if state.in_code_block:
        pdf.add_code_block("\n".join(state.code_buffer))
        state.code_buffer = []
        state.in_code_block = False
    else:
        state.in_code_block = True
    return True


def _handle_table_line(line: str, state: _MarkdownRenderState) -> bool:
    """Collect markdown table rows until a non-table line is reached."""
    stripped = line.strip()
    if not stripped.startswith("|"):
        return False
    state.in_table = True
    if re.match(r"^[|\s:\-]+$", stripped):
        return True
    cells = [cell.strip() for cell in stripped.split("|")]
    if cells and cells[0] == "":
        cells = cells[1:]
    if cells and cells[-1] == "":
        cells = cells[:-1]
    state.table_rows.append(cells)
    return True


def _flush_table(pdf: ReportPDF, state: _MarkdownRenderState) -> None:
    """Render buffered table rows and reset table parsing state."""
    if not state.in_table:
        return
    if state.table_rows:
        pdf.add_table(state.table_rows)
        state.table_rows = []
    state.in_table = False


def _render_markdown_line(pdf: ReportPDF, line: str, state: _MarkdownRenderState) -> None:
    """Render one non-code, non-table markdown line."""
    stripped = line.strip()
    if line.startswith("# "):
        state.in_list = False
        pdf.add_title(line[2:])
        return
    if line.startswith("## "):
        state.in_list = False
        pdf.add_heading_1(line[3:])
        return
    if line.startswith("### "):
        state.in_list = False
        pdf.add_heading_2(line[4:])
        return
    if line.startswith("#### "):
        state.in_list = False
        pdf.add_heading_3(line[5:])
        return
    if stripped.startswith("- ") or stripped.startswith("* "):
        state.in_list = True
        pdf.add_list_item(stripped[2:], ordered=False)
        return
    if re.match(r"^\d+\.\s", stripped):
        if not state.in_list:
            state.list_counter = 0
        state.in_list = True
        state.list_counter += 1
        pdf.add_list_item(
            re.sub(r"^\d+\.\s", "", stripped), ordered=True, indent=state.list_counter - 1
        )
        return
    if stripped in {"---", "***", "___"}:
        state.in_list = False
        pdf.ln(2)
        pdf.line(10, pdf.get_y(), 200, pdf.get_y())
        pdf.ln(2)
        return
    if not stripped:
        state.in_list = False
        return
    state.in_list = False
    pdf.add_paragraph(line)
