# openosint/pdf_report.py
"""
PDF report generator for OpenOSINT investigation results.

Converts a Markdown investigation report to a PDF alongside it.
Uses reportlab — falls back silently if not installed.
"""

from __future__ import annotations

import asyncio
import logging
from datetime import datetime
from pathlib import Path
from typing import Any

logger = logging.getLogger(__name__)

_LOGO_TEXT = "OPENOSINT"
_FOOTER_TEXT = "Generated by OpenOSINT — For authorized use only"


def _build_pdf_styles(base_styles: Any) -> dict[str, Any]:
    """Build the custom paragraph styles for an OSINT PDF report."""
    from reportlab.lib import colors  # type: ignore
    from reportlab.lib.enums import TA_CENTER  # type: ignore
    from reportlab.lib.styles import ParagraphStyle  # type: ignore

    return {
        "title": ParagraphStyle(
            "OsiTitle",
            parent=base_styles["Heading1"],
            fontSize=18,
            textColor=colors.HexColor("#00cc66"),
            alignment=TA_CENTER,
            spaceAfter=4,
        ),
        "date": ParagraphStyle(
            "OsiDate",
            parent=base_styles["Normal"],
            fontSize=9,
            textColor=colors.grey,
            alignment=TA_CENTER,
            spaceAfter=12,
        ),
        "h1": ParagraphStyle(
            "OsiH1",
            parent=base_styles["Heading1"],
            fontSize=14,
            textColor=colors.HexColor("#1a1a2e"),
            spaceBefore=12,
            spaceAfter=4,
        ),
        "h2": ParagraphStyle(
            "OsiH2",
            parent=base_styles["Heading2"],
            fontSize=11,
            textColor=colors.HexColor("#1a1a2e"),
            spaceBefore=10,
            spaceAfter=4,
        ),
        "body": ParagraphStyle(
            "OsiBody",
            parent=base_styles["Normal"],
            fontSize=9,
            leading=14,
            spaceAfter=3,
        ),
        "mono": ParagraphStyle(
            "OsiMono",
            parent=base_styles["Code"],
            fontSize=8,
            leading=12,
            fontName="Courier",
            spaceAfter=2,
            leftIndent=10,
        ),
    }


def _build_pdf_story(content: str, styles: dict[str, Any], date_str: str) -> list[Any]:
    """Convert Markdown content to a list of reportlab flowables."""
    from reportlab.lib import colors  # type: ignore
    from reportlab.lib.units import mm  # type: ignore
    from reportlab.platypus import HRFlowable, Paragraph, Spacer  # type: ignore

    story: list[Any] = []
    story.append(Paragraph(_LOGO_TEXT, styles["title"]))
    story.append(Paragraph(f"Investigation Report — {date_str}", styles["date"]))
    story.append(HRFlowable(width="100%", thickness=1, color=colors.HexColor("#e2e8f0")))
    story.append(Spacer(1, 6 * mm))

    is_in_code_block = False
    for line in content.splitlines():
        stripped = line.strip()
        if stripped.startswith("```"):
            is_in_code_block = not is_in_code_block
            continue
        if is_in_code_block or stripped.startswith("[+]") or stripped.startswith("[*]"):
            safe = stripped.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
            story.append(Paragraph(safe, styles["mono"]))
        elif stripped.startswith("# ") and not stripped.startswith("## "):
            story.append(Paragraph(stripped[2:], styles["h1"]))
        elif stripped.startswith("## "):
            story.append(Paragraph(stripped[3:], styles["h2"]))
        elif stripped.startswith("**") or stripped:
            safe = stripped.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
            story.append(Paragraph(safe, styles["body"]))
        else:
            story.append(Spacer(1, 3 * mm))

    return story


def _create_footer_callback(page_width: float) -> Any:
    """Return a reportlab canvas callback that draws the page footer."""
    from reportlab.lib import colors  # type: ignore
    from reportlab.lib.units import mm  # type: ignore

    def _footer(canvas: Any, doc: Any) -> None:
        canvas.saveState()
        canvas.setFont("Helvetica", 7)
        canvas.setFillColor(colors.grey)
        canvas.drawCentredString(
            page_width / 2,
            12 * mm,
            f"{_FOOTER_TEXT}   —   Page {doc.page}",
        )
        canvas.restoreState()

    return _footer


def _generate_pdf_sync(md_path: Path, pdf_path: Path, date_str: str) -> None:
    """Blocking PDF generation — intended to run in a thread executor."""
    try:
        from reportlab.lib.pagesizes import A4  # type: ignore
        from reportlab.lib.styles import getSampleStyleSheet  # type: ignore
        from reportlab.lib.units import mm  # type: ignore
        from reportlab.platypus import SimpleDocTemplate  # type: ignore
    except ImportError:
        logger.warning(
            "reportlab not installed — PDF generation skipped. "
            "Install with: pip install reportlab"
        )
        return

    page_width, _ = A4
    base_styles = getSampleStyleSheet()
    styles = _build_pdf_styles(base_styles)
    content = md_path.read_text(encoding="utf-8")
    story = _build_pdf_story(content, styles, date_str)
    footer = _create_footer_callback(page_width)

    doc = SimpleDocTemplate(
        str(pdf_path),
        pagesize=A4,
        leftMargin=20 * mm,
        rightMargin=20 * mm,
        topMargin=25 * mm,
        bottomMargin=25 * mm,
    )
    doc.build(story, onFirstPage=footer, onLaterPages=footer)
    logger.info("PDF report written: %s", pdf_path)


async def generate_pdf_report(md_path: Path) -> Path | None:
    """
    Asynchronously generate a PDF alongside md_path.

    Runs reportlab in a thread executor so the asyncio event loop
    is not blocked.

    Returns
    -------
    Path | None
        Path to the generated PDF on success, ``None`` if skipped or failed.
    """
    pdf_path = md_path.with_suffix(".pdf")
    date_str = datetime.now().strftime("%Y-%m-%d")
    loop = asyncio.get_event_loop()
    try:
        await loop.run_in_executor(None, _generate_pdf_sync, md_path, pdf_path, date_str)
        return pdf_path if pdf_path.exists() else None
    except Exception:
        logger.exception("PDF generation failed for %s.", md_path)
        return None
