"""Markdown → PDF：默认 **PyMuPDF Story**（MD→HTML→排版）；可选 **pandoc** 外部引擎。"""

from __future__ import annotations

import shutil
import subprocess
from pathlib import Path
from typing import Any, Callable

PolicyCheck = Callable[[Path], None]

# PyMuPDF Story 的 HTML/CSS 子集较弱：thead/th 上隐式或继承的 background 常被画成**整块黑色**；
# border-collapse:collapse 与粗线也可能变成异常横条。以下用**显式浅色**、separate 边框、细 hr。
DEFAULT_STORY_CSS = """
body { font-family: Helvetica, Arial, "Microsoft YaHei", "PingFang SC", sans-serif; font-size: 11pt; line-height: 1.35; color: #000000; background-color: #ffffff; }
h1 { font-size: 20pt; margin-top: 0.6em; margin-bottom: 0.3em; color: #000000; background-color: transparent; }
h2 { font-size: 16pt; margin-top: 0.6em; margin-bottom: 0.3em; color: #000000; background-color: transparent; }
h3 { font-size: 13pt; margin-top: 0.5em; margin-bottom: 0.25em; color: #000000; background-color: transparent; }
pre { background-color: #f4f4f4; color: #000000; padding: 8px; font-size: 9pt; overflow-x: auto; border: 0.5pt solid #dddddd; }
code { font-family: Consolas, monospace; font-size: 9.5pt; background-color: #f4f4f4; color: #000000; padding: 1px 4px; }
table { border-collapse: separate; border-spacing: 0; width: 100%; margin: 0.5em 0; background-color: #ffffff; }
thead { background-color: #e8e8e8; color: #000000; }
tbody { background-color: #ffffff; }
tr { background-color: #ffffff; }
thead tr { background-color: #e8e8e8; }
th, td {
  border: 0.75pt solid #888888;
  padding: 4px 6px;
  vertical-align: top;
  background-color: #ffffff;
  color: #000000;
}
th {
  background-color: #eeeeee;
  font-weight: bold;
  color: #000000;
}
thead th { background-color: #e8e8e8; color: #000000; }
blockquote { border-left: 3px solid #cccccc; margin-left: 0; padding-left: 12px; color: #444444; background-color: transparent; }
img { max-width: 100%; height: auto; }
hr {
  display: block;
  height: 0;
  border: 0;
  border-top: 0.75pt solid #999999;
  margin: 0.8em 0;
  padding: 0;
  background-color: transparent;
  color: #000000;
}
"""

MAX_MARKDOWN_CHARS = 2_000_000


def _read_markdown(
    markdown_path: Path | None, markdown_text: str | None
) -> tuple[str | None, str | None]:
    if markdown_path is not None and markdown_text is not None:
        return None, "pass only one of markdown_path or markdown_text"
    if markdown_path is None and (markdown_text is None or not str(markdown_text).strip()):
        return None, "markdown_path or markdown_text is required"
    if markdown_path is not None:
        p = markdown_path
        if not p.is_file():
            return None, f"markdown file not found: {p}"
        if p.suffix.lower() not in (".md", ".markdown", ".txt"):
            return None, "markdown_path should be .md / .markdown / .txt"
        raw = p.read_text(encoding="utf-8", errors="replace")
        if len(raw) > MAX_MARKDOWN_CHARS:
            return None, f"markdown exceeds {MAX_MARKDOWN_CHARS} characters"
        return raw, None
    t = str(markdown_text or "")
    if len(t) > MAX_MARKDOWN_CHARS:
        return None, f"markdown exceeds {MAX_MARKDOWN_CHARS} characters"
    return t, None


def _md_to_html_fragment(md: str) -> tuple[str | None, str | None]:
    """延迟导入 ``markdown``，避免未 ``pip install`` 时阻塞整个 Agent（ToolRouter 加载）。"""
    try:
        import markdown as md_mod
    except ImportError:
        return (
            None,
            "缺少 Python 包 markdown（ModuleNotFoundError）。请执行: pip install \"markdown>=3.5\" "
            "或重新 pip install -e . 后重启 Aicd。",
        )
    exts = [
        "markdown.extensions.tables",
        "markdown.extensions.fenced_code",
        "markdown.extensions.nl2br",
        "markdown.extensions.sane_lists",
    ]
    return md_mod.markdown(md, extensions=exts), None


def export_markdown_to_pdf_pymupdf(
    md_source: str,
    output_pdf: Path,
    *,
    policy_check: PolicyCheck,
    archive_dir: Path,
    user_css: str | None = None,
    paper: str = "a4",
    margin_pt: float = 36.0,
) -> dict[str, Any]:
    import fitz

    output_pdf = output_pdf.expanduser().resolve()
    policy_check(output_pdf)
    policy_check(output_pdf.parent)
    ad = archive_dir.expanduser().resolve()
    policy_check(ad)

    fragment, md_err = _md_to_html_fragment(md_source)
    if md_err or fragment is None:
        return {
            "ok": False,
            "error": md_err or "markdown to HTML failed",
            "engine": "pymupdf_story",
            "hint": "pip install \"markdown>=3.5\"",
        }
    html = (
        "<!DOCTYPE html><html><head><meta charset='utf-8'/></head><body>"
        + fragment
        + "</body></html>"
    )
    css = (user_css or "").strip() + "\n" + DEFAULT_STORY_CSS
    story = fitz.Story(html=html, user_css=css, archive=str(ad))
    mediabox = fitz.paper_rect(paper.lower())
    m = max(12.0, float(margin_pt))
    where = mediabox + (m, m, -m, -m)

    output_pdf.parent.mkdir(parents=True, exist_ok=True)
    writer = fitz.DocumentWriter(str(output_pdf))
    try:
        more = 1
        while more:
            dev = writer.begin_page(mediabox)
            more, _ = story.place(where)
            story.draw(dev)
            writer.end_page()
        writer.close()
    except Exception as e:  # noqa: BLE001
        try:
            writer.close()
        except Exception:  # noqa: BLE001
            pass
        if output_pdf.is_file():
            try:
                output_pdf.unlink()
            except OSError:
                pass
        return {"ok": False, "error": repr(e), "engine": "pymupdf_story"}

    if not output_pdf.is_file() or output_pdf.stat().st_size == 0:
        return {"ok": False, "error": "empty PDF output", "engine": "pymupdf_story"}
    return {
        "ok": True,
        "pdf_path": str(output_pdf),
        "engine": "pymupdf_story",
        "paper": paper.lower(),
    }


def export_markdown_to_pdf_pandoc(
    md_path: Path,
    output_pdf: Path,
    *,
    policy_check: PolicyCheck,
    extra_args: list[str] | None = None,
) -> dict[str, Any]:
    exe = shutil.which("pandoc")
    if not exe:
        return {
            "ok": False,
            "error": "pandoc not found on PATH",
            "hint": "Install Pandoc (https://pandoc.org) or use engine=pymupdf (built-in).",
            "engine": "pandoc",
        }
    src = md_path.expanduser().resolve()
    dst = output_pdf.expanduser().resolve()
    policy_check(src)
    policy_check(dst)
    policy_check(dst.parent)
    if not src.is_file():
        return {"ok": False, "error": f"source not found: {src}", "engine": "pandoc"}
    dst.parent.mkdir(parents=True, exist_ok=True)
    args = [exe, str(src), "-o", str(dst)]
    if extra_args:
        args.extend(str(x) for x in extra_args if str(x).strip())
    try:
        cp = subprocess.run(
            args,
            capture_output=True,
            text=True,
            timeout=600,
            encoding="utf-8",
            errors="replace",
            cwd=str(src.parent),
        )
    except subprocess.TimeoutExpired:
        return {"ok": False, "error": "pandoc timed out (600s)", "engine": "pandoc"}
    except OSError as e:
        return {"ok": False, "error": f"pandoc failed to run: {e}", "engine": "pandoc"}
    if cp.returncode != 0:
        tail = (cp.stderr or cp.stdout or "")[:4000]
        return {
            "ok": False,
            "error": f"pandoc exit {cp.returncode}",
            "stderr_tail": tail,
            "engine": "pandoc",
        }
    if not dst.is_file() or dst.stat().st_size == 0:
        return {"ok": False, "error": "pandoc produced no PDF", "engine": "pandoc"}
    return {"ok": True, "pdf_path": str(dst), "engine": "pandoc"}


def _write_source_snapshot(
    *,
    snapshot_path: Path,
    policy_check: PolicyCheck,
    markdown_path: Path | None,
    raw_text: str | None,
) -> str | None:
    """将参与排版的 Markdown 落盘，便于日后只改编排不改内容。返回绝对路径或 None。"""
    sp = snapshot_path.expanduser().resolve()
    policy_check(sp)
    policy_check(sp.parent)
    sp.parent.mkdir(parents=True, exist_ok=True)
    if markdown_path is not None and Path(markdown_path).is_file():
        shutil.copy2(Path(markdown_path).resolve(), sp)
    elif raw_text is not None:
        sp.write_text(raw_text, encoding="utf-8")
    else:
        return None
    return str(sp)


def export_markdown_to_pdf(
    *,
    output_pdf_path: Path,
    policy_check: PolicyCheck,
    markdown_path: Path | None = None,
    markdown_text: str | None = None,
    engine: str = "pymupdf",
    archive_path: Path | None = None,
    user_css: str | None = None,
    paper: str = "a4",
    margin_pt: float = 36.0,
    pandoc_extra_args: list[str] | None = None,
    source_snapshot_path: Path | None = None,
) -> dict[str, Any]:
    """
    ``engine``: ``pymupdf`` | ``pandoc``.

    - **pymupdf**：内置 **Story**；``archive_path`` 为解析 ``![](relative.png)`` 的基准目录（默认：``markdown_path`` 父目录或 workspace 当前策略路径）。
    - **pandoc**：需系统安装 **pandoc**；仅支持 ``markdown_path``（临时文件可在外部写入）。
    - **source_snapshot_path**：可选；将本次用于生成的 **.md** 副本写到该路径（与 PDF 同目录的 ``*_source.md`` 便于改版式不改内容）。
    """
    dst = output_pdf_path.expanduser().resolve()
    if dst.suffix.lower() != ".pdf":
        return {"ok": False, "error": "output_pdf_path must end with .pdf"}

    eng = (engine or "pymupdf").strip().lower()
    raw, err = _read_markdown(
        Path(markdown_path) if markdown_path is not None else None,
        markdown_text,
    )
    if err:
        return {"ok": False, "error": err}

    if eng == "pandoc":
        if markdown_path is None:
            return {
                "ok": False,
                "error": "engine=pandoc requires markdown_path on disk",
                "hint": "Save markdown to a workspace file first, or use engine=pymupdf with markdown_text.",
            }
        out = export_markdown_to_pdf_pandoc(
            Path(markdown_path),
            dst,
            policy_check=policy_check,
            extra_args=pandoc_extra_args,
        )
        if out.get("ok") and source_snapshot_path is not None:
            snap = _write_source_snapshot(
                snapshot_path=source_snapshot_path,
                policy_check=policy_check,
                markdown_path=Path(markdown_path),
                raw_text=None,
            )
            if snap:
                out["source_snapshot_path"] = snap
        return out

    if eng not in ("pymupdf", "pymupdf_story", ""):
        return {
            "ok": False,
            "error": f"unknown engine: {engine!r}; use pymupdf or pandoc",
        }

    base = archive_path
    if base is None:
        base = Path(markdown_path).resolve().parent if markdown_path else dst.parent
    base = Path(base).expanduser().resolve()
    policy_check(base)

    out = export_markdown_to_pdf_pymupdf(
        raw or "",
        dst,
        policy_check=policy_check,
        archive_dir=base,
        user_css=user_css,
        paper=paper,
        margin_pt=margin_pt,
    )
    if out.get("ok") and source_snapshot_path is not None:
        snap = _write_source_snapshot(
            snapshot_path=source_snapshot_path,
            policy_check=policy_check,
            markdown_path=Path(markdown_path) if markdown_path else None,
            raw_text=raw,
        )
        if snap:
            out["source_snapshot_path"] = snap
    return out


def pandoc_available() -> bool:
    return bool(shutil.which("pandoc"))
