"""将 ``.docx`` 转为 ``.pdf``（LibreOffice 优先，可选 docx2pdf + Word）。"""

from __future__ import annotations

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

_CONVERT_TIMEOUT_SEC = 180


def install_suggestions_docx_to_pdf() -> list[dict[str, Any]]:
    """
    缺失转换引擎时返回给 Agent 的结构化安装建议。

    **约定**：由主对话先 **询问用户是否同意**，再使用 **`run_command`** 执行
    ``shell_command``；勿在未确认时静默安装。
    """
    s: list[dict[str, Any]] = []
    if sys.platform == "win32":
        s.append(
            {
                "id": "libreoffice_winget",
                "title": "安装 LibreOffice（推荐，DOCX→PDF 通用）",
                "shell_command": "winget install --id TheDocumentFoundation.LibreOffice -e --accept-package-agreements",
                "note": "安装完成后**新开终端**或重启 Aicd，再调用 docx_export_pdf。也可从 https://www.libreoffice.org/download/download/ 手动安装。",
            }
        )
        s.append(
            {
                "id": "env_soffice",
                "title": "已安装 LibreOffice 但未在 PATH 中",
                "shell_command": "",
                "note": "在系统环境变量中把 LibreOffice\\program 加入 PATH，或设置环境变量 AICD_SOFFICE 指向 soffice.exe 的完整路径。",
            }
        )
        s.append(
            {
                "id": "pip_docx2pdf_win",
                "title": "可选：pip 安装 docx2pdf（需本机已装 Microsoft Word）",
                "shell_command": 'pip install "aicd[pdf]"',
                "note": "仅在有 Word 时有效；无 Word 请优先 LibreOffice 方案。",
            }
        )
    elif sys.platform == "darwin":
        s.append(
            {
                "id": "libreoffice_brew",
                "title": "安装 LibreOffice（推荐）",
                "shell_command": "brew install --cask libreoffice",
                "note": "无 Homebrew 可从 https://www.libreoffice.org/download/download/ 下载安装。",
            }
        )
        s.append(
            {
                "id": "pip_docx2pdf_mac",
                "title": "可选：pip 安装 docx2pdf（需 Microsoft Word for Mac）",
                "shell_command": 'pip install "aicd[pdf]"',
                "note": "依赖 Word；否则请用 LibreOffice。",
            }
        )
    else:
        s.append(
            {
                "id": "libreoffice_apt",
                "title": "安装 LibreOffice（推荐，Debian/Ubuntu 示例）",
                "shell_command": "sudo apt-get update && sudo apt-get install -y libreoffice",
                "note": "其他发行版请用对应包管理器安装 libreoffice / soffice。",
            }
        )
    s.append(
        {
            "id": "use_aicd_tool",
            "title": "勿手写 reportlab/inline_images 等脚本转 PDF",
            "shell_command": "",
            "note": "请使用内置工具 **docx_export_pdf**；python-docx 的 Document 无 inline_images 属性，易报错。",
        }
    )
    return s


def _attach_suggestions(d: dict[str, Any]) -> dict[str, Any]:
    """为失败结果附加 install_suggestions 与 Agent 指引（原地修改并返回）。"""
    if d.get("ok") is not False:
        return d
    d.setdefault("install_suggestions", install_suggestions_docx_to_pdf())
    d.setdefault(
        "next_step_for_agent",
        "先向用户说明缺少 PDF 转换组件，列出 install_suggestions；**询问用户是否同意安装**后再用 run_command 执行其中的 shell_command（跳过 command 为空的项）；不要未经确认就执行安装。",
    )
    return d


def find_soffice_executable() -> Path | None:
    """
    查找 LibreOffice ``soffice`` / ``libreoffice``。

    环境变量 **AICD_SOFFICE** 或 **SOFFICE_PATH** 可指向可执行文件（含 ``soffice.exe``）。
    """
    for key in ("AICD_SOFFICE", "SOFFICE_PATH"):
        env = (os.environ.get(key) or "").strip()
        if env:
            p = Path(env).expanduser()
            if p.is_file():
                return p.resolve()
    for name in ("soffice", "libreoffice"):
        w = shutil.which(name)
        if w:
            return Path(w).resolve()
    if sys.platform == "win32":
        for pf_key in ("ProgramFiles", "ProgramFiles(x86)"):
            pf = os.environ.get(pf_key) or ""
            if not pf:
                continue
            root = Path(pf)
            if not root.is_dir():
                continue
            for lo in sorted(root.glob("LibreOffice*/program/soffice.exe")):
                if lo.is_file():
                    return lo.resolve()
    if sys.platform == "darwin":
        mac = Path("/Applications/LibreOffice.app/Contents/MacOS/soffice")
        if mac.is_file():
            return mac.resolve()
    return None


def _export_via_libreoffice(
    docx_path: Path,
    output_pdf: Path,
) -> dict[str, Any]:
    exe = find_soffice_executable()
    if exe is None:
        return {
            "ok": False,
            "error": "LibreOffice (soffice) not found",
            "hint": "Install LibreOffice and ensure soffice is on PATH, or set AICD_SOFFICE to the executable.",
        }
    out_dir = output_pdf.parent
    out_dir.mkdir(parents=True, exist_ok=True)
    expected = out_dir / f"{docx_path.stem}.pdf"
    # 若目标与 LO 默认输出名不同，先写到 expected 再移动
    cmd = [
        str(exe),
        "--headless",
        "--norestore",
        "--nofirststartwizard",
        "--convert-to",
        "pdf",
        "--outdir",
        str(out_dir),
        str(docx_path),
    ]
    try:
        cp = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=_CONVERT_TIMEOUT_SEC,
            encoding="utf-8",
            errors="replace",
        )
    except subprocess.TimeoutExpired:
        return {
            "ok": False,
            "error": f"LibreOffice timed out after {_CONVERT_TIMEOUT_SEC}s",
            "via": "libreoffice",
        }
    except OSError as e:
        return {"ok": False, "error": f"LibreOffice spawn failed: {e}", "via": "libreoffice"}

    if not expected.is_file() or expected.stat().st_size == 0:
        tail = (cp.stderr or cp.stdout or "")[:2000]
        return {
            "ok": False,
            "error": f"LibreOffice did not produce {expected.name} (exit {cp.returncode})",
            "stderr_tail": tail,
            "via": "libreoffice",
        }

    if expected.resolve() != output_pdf.resolve():
        try:
            if output_pdf.is_file():
                output_pdf.unlink()
            shutil.move(str(expected), str(output_pdf))
        except OSError as e:
            return {
                "ok": False,
                "error": f"could not move PDF to destination: {e}",
                "via": "libreoffice",
            }

    return {
        "ok": True,
        "pdf_path": str(output_pdf),
        "via": "libreoffice",
        "soffice": str(exe),
    }


def _export_via_docx2pdf(docx_path: Path, output_pdf: Path) -> dict[str, Any]:
    if sys.platform not in ("win32", "darwin"):
        return {
            "ok": False,
            "error": "docx2pdf is only supported on Windows and macOS (requires Microsoft Word)",
            "via": "docx2pdf",
        }
    try:
        from docx2pdf import convert
    except ImportError:
        return {
            "ok": False,
            "error": "docx2pdf not installed",
            "hint": 'pip install "aicd[pdf]"  （可选；需本机已安装 Microsoft Word）',
            "via": "docx2pdf",
        }
    output_pdf.parent.mkdir(parents=True, exist_ok=True)
    try:
        convert(str(docx_path), str(output_pdf))
    except Exception as e:  # noqa: BLE001
        return {
            "ok": False,
            "error": repr(e),
            "hint": "Ensure Microsoft Word is installed and not showing a blocking dialog.",
            "via": "docx2pdf",
        }
    if not output_pdf.is_file() or output_pdf.stat().st_size == 0:
        return {
            "ok": False,
            "error": "docx2pdf did not write a non-empty PDF",
            "via": "docx2pdf",
        }
    return {"ok": True, "pdf_path": str(output_pdf), "via": "docx2pdf"}


def export_docx_to_pdf(
    docx_path: Path,
    output_pdf: Path | None,
    *,
    policy_check: Callable[[Path], None],
    prefer: str | None = None,
) -> dict[str, Any]:
    """
    ``prefer``: ``libreoffice`` | ``docx2pdf`` | ``None``（自动：先 LO，再在 Win/Mac 上尝试 docx2pdf）。
    """
    src = docx_path.expanduser().resolve()
    policy_check(src)
    if not src.is_file():
        return {"ok": False, "error": f"docx file not found: {src}"}
    if src.suffix.lower() != ".docx":
        return {"ok": False, "error": "source must be a .docx file"}

    dst = (
        output_pdf.expanduser().resolve()
        if output_pdf is not None
        else src.with_suffix(".pdf")
    )
    if dst.suffix.lower() != ".pdf":
        return {"ok": False, "error": "output path must end with .pdf"}
    policy_check(dst)
    policy_check(dst.parent)

    pref = (prefer or "").strip().lower()
    attempts: list[tuple[str, Callable[[], dict[str, Any]]]] = []

    if pref == "docx2pdf":
        attempts.append(("docx2pdf", lambda: _export_via_docx2pdf(src, dst)))
        attempts.append(("libreoffice", lambda: _export_via_libreoffice(src, dst)))
    else:
        attempts.append(("libreoffice", lambda: _export_via_libreoffice(src, dst)))
        if pref != "libreoffice":
            attempts.append(("docx2pdf", lambda: _export_via_docx2pdf(src, dst)))

    errors: list[str] = []
    hints: list[str] = []
    for name, fn in attempts:
        r = fn()
        if r.get("ok"):
            r["source_docx"] = str(src)
            return r
        msg = str(r.get("error") or "failed")
        errors.append(f"{name}: {msg}")
        h = r.get("hint")
        if isinstance(h, str) and h.strip():
            hints.append(h.strip())

    out: dict[str, Any] = {
        "ok": False,
        "error": " | ".join(errors),
        "source_docx": str(src),
    }
    if hints:
        out["hint"] = " ".join(dict.fromkeys(hints))
    else:
        out["hint"] = (
            "Install LibreOffice (recommended) or on Windows/macOS install Microsoft Word and pip install docx2pdf."
        )
    return _attach_suggestions(out)
