"""通过 https://embed.diagrams.net（proto=json）加载 .drawio XML 并导出 PNG。"""

from __future__ import annotations

import base64
import json
import os
import re
import subprocess
from pathlib import Path
from typing import Any, Callable

from aicd.kit import drawio_spec

MAX_DRAWIO_XML_CHARS = 3_000_000

# 环境变量（启动进程前设置）：
# - AICD_ENABLE_DRAWIO_EXPORT_PNG=1 — **开启** `.drawio`→PNG 导出（默认关闭；未开启时工具不会注册给模型）
# - AICD_DISABLE_DRAWIO_EXPORT_PNG=1 — 强制关闭（即使已设 ENABLE）
# - AICD_DRAWIO_OFFLINE_ONLY=1 — 禁止 Playwright + embed.diagrams.net；仅允许 AICD_DRAWIO_CLI 本机导出

IFRAME_SRC = (
    "https://embed.diagrams.net/?embed=1&proto=json&spin=1"
    "&noSaveBtn=1&noExitBtn=1&saveAndExit=0&libraries=0"
)


def _parse_data_uri_png(data_uri: str) -> bytes:
    s = (data_uri or "").strip()
    m = re.match(r"^data:image/png;base64,(.+)$", s, re.I | re.DOTALL)
    if not m:
        raise ValueError("export data is not a PNG data URI")
    return base64.b64decode(m.group(1), validate=True)


def _build_parent_page(diagram_xml: str) -> str:
    xml_lit = json.dumps(diagram_xml, ensure_ascii=False)
    return f"""<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8"/>
  <title>drawio embed export</title>
</head>
<body style="margin:0;padding:8px;background:#f0f0f0;">
<iframe
  id="dio"
  title="drawio"
  src="{IFRAME_SRC}"
  style="width:1600px;height:1000px;border:0;display:block;"
></iframe>
<script>
(function () {{
  const DIAGRAM_XML = {xml_lit};
  window.__DRAWIO_DONE__ = false;
  window.__DRAWIO_ERROR__ = null;
  window.__DRAWIO_PNG_URI__ = null;

  function parseMsg(data) {{
    if (data == null) return null;
    if (typeof data === "string") {{
      try {{ return JSON.parse(data); }} catch (e) {{ return null; }}
    }}
    if (typeof data === "object") return data;
    return null;
  }}

  window.addEventListener("message", function (event) {{
    const iframe = document.getElementById("dio");
    if (!iframe || event.source !== iframe.contentWindow) return;
    const msg = parseMsg(event.data);
    if (!msg || typeof msg !== "object") return;

    if (msg.error) {{
      window.__DRAWIO_ERROR__ = JSON.stringify(msg);
      window.__DRAWIO_DONE__ = true;
      return;
    }}

    if (msg.event === "init") {{
      iframe.contentWindow.postMessage(JSON.stringify({{
        action: "load",
        xml: DIAGRAM_XML
      }}), "*");
      return;
    }}

    if (msg.event === "load") {{
      iframe.contentWindow.postMessage(JSON.stringify({{
        action: "export",
        format: "png",
        scale: __EXPORT_SCALE__,
        transparent: __EXPORT_TRANSPARENT__,
        border: __EXPORT_BORDER__,
        size: "diagram"
      }}), "*");
      return;
    }}

    if (msg.event === "export") {{
      if (msg.format === "png" && msg.data) {{
        window.__DRAWIO_PNG_URI__ = msg.data;
        window.__DRAWIO_DONE__ = true;
        return;
      }}
      window.__DRAWIO_ERROR__ = JSON.stringify(msg);
      window.__DRAWIO_DONE__ = true;
    }}
  }});
}})();
</script>
</body>
</html>
"""


def _env_flag(name: str) -> bool:
    return (os.environ.get(name) or "").strip().lower() in ("1", "true", "yes")


def drawio_export_png_allowed() -> bool:
    """
    是否允许使用 ``diagram_drawio_export_png``（注册工具 + 执行导出）。

    默认 **关闭**；需 ``AICD_ENABLE_DRAWIO_EXPORT_PNG=1``。
    ``AICD_DISABLE_DRAWIO_EXPORT_PNG=1`` 时恒为 False。
    """
    if _env_flag("AICD_DISABLE_DRAWIO_EXPORT_PNG"):
        return False
    return _env_flag("AICD_ENABLE_DRAWIO_EXPORT_PNG")


def _export_via_drawio_desktop_cli(
    src: Path,
    out: Path,
    scale: float,
) -> dict[str, Any] | None:
    """
    可选：若设置环境变量 **AICD_DRAWIO_CLI** 为 diagrams.net / draw.io **桌面版可执行文件**路径，
    则优先用其命令行导出（适合**无外网**或 embed 失败时）。未设置则返回 None。
    """
    cli = (os.environ.get("AICD_DRAWIO_CLI") or "").strip()
    if not cli:
        return None
    exe = Path(cli)
    if not exe.is_file():
        return {
            "ok": False,
            "error": f"AICD_DRAWIO_CLI is not an existing file: {cli}",
            "hint": "指向 draw.io / diagrams.net 安装目录下的可执行文件（Windows 常为 draw.io.exe）。",
        }
    sc = max(0.5, min(8.0, float(scale)))
    attempts: list[list[str]] = [
        [str(exe), "-x", "-f", "png", "--scale", str(sc), "-o", str(out), str(src)],
        [str(exe), "-x", "-f", "png", "-o", str(out), str(src)],
    ]
    last_err = ""
    for cmd in attempts:
        try:
            cp = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                timeout=180,
                check=False,
            )
            if out.is_file() and out.stat().st_size > 0:
                return {
                    "ok": True,
                    "png_path": str(out),
                    "source_drawio": str(src),
                    "scale": sc,
                    "transparent": False,
                    "border": 0,
                    "via": "drawio_desktop_cli",
                }
            last_err = (cp.stderr or cp.stdout or f"exit {cp.returncode}")[:2000]
        except subprocess.TimeoutExpired:
            last_err = "draw.io CLI timed out after 180s"
        except OSError as e:
            last_err = str(e)
    return {
        "ok": False,
        "error": f"draw.io desktop CLI export failed: {last_err}",
        "hint": "确认 AICD_DRAWIO_CLI 与参数兼容；Linux 无桌面时可能需 xvfb-run。",
    }


def _playwright_drawio_hint(exc: BaseException) -> str:
    msg = str(exc)
    parts = [
        "需要可访问 https://embed.diagrams.net 的出站 HTTPS；",
        "以及 Playwright 的 Chromium（执行: python -m playwright install chromium）。",
    ]
    low = msg.lower()
    if "executable doesn't exist" in low or "browserType.launch" in msg:
        parts.insert(
            0,
            "未检测到 Chromium：请先执行 `python -m playwright install chromium`。",
        )
    if "timeout" in low or "navigation" in low:
        parts.append("若网络受限，可安装 draw.io 桌面版并设置环境变量 AICD_DRAWIO_CLI 后重试导出。")
    return " ".join(parts)


def export_drawio_file_to_png(
    drawio_path: Path,
    output_png: Path,
    *,
    policy_check: Callable[[Path], None],
    scale: float = 3.0,
    transparent: bool = False,
    border: int = 2,
    timeout_ms: int = 120_000,
) -> dict[str, Any]:
    """
    导出顺序：

    1. 若设置了 **AICD_DRAWIO_CLI**，先尝试 **draw.io 桌面版**命令行导出（可离线）。
    2. 否则使用 **Playwright 无头 Chromium** 加载本地 HTML，内嵌 iframe 调用 **embed.diagrams.net**
       的 JSON 协议（``load`` → ``export``）生成 PNG（需出站 HTTPS）。

    插入 Word 前请确保本函数返回 **ok** 且 PNG 路径存在，再在 **docx_compose** 的 **image** 块中引用该路径。

    启用 / 禁用 / 离线策略见模块顶部环境变量说明。
    """
    if not drawio_export_png_allowed():
        return {
            "ok": False,
            "error": "diagram_drawio_export_png 未启用（默认关闭）。请设置 AICD_ENABLE_DRAWIO_EXPORT_PNG=1。",
            "hint": "`.drawio` 源文件可用 diagram_drawio_write 离线生成；Word 插图可改用 diagram_mermaid_png，"
            "或在 Draw.io 桌面版手动导出 PNG。若已设 ENABLE 仍失败，检查是否被 AICD_DISABLE_DRAWIO_EXPORT_PNG=1 覆盖。",
        }

    src = drawio_path.expanduser().resolve()
    policy_check(src)
    out = output_png.expanduser().resolve()
    policy_check(out)
    if not src.is_file():
        return {"ok": False, "error": f"drawio file not found: {src}"}
    if out.suffix.lower() != ".png":
        return {"ok": False, "error": "output path must end with .png"}

    raw = src.read_text(encoding="utf-8", errors="replace")
    raw = drawio_spec.strip_bom_and_xml_decl(raw)
    if len(raw) > MAX_DRAWIO_XML_CHARS:
        return {
            "ok": False,
            "error": f"drawio XML exceeds {MAX_DRAWIO_XML_CHARS} chars",
        }
    if "<mxfile" not in raw.lower():
        return {"ok": False, "error": "file does not look like draw.io XML (expected mxfile)"}

    sc = max(0.5, min(8.0, float(scale)))
    bd = max(0, min(64, int(border)))
    tr = bool(transparent)

    offline_only = _env_flag("AICD_DRAWIO_OFFLINE_ONLY")

    cli_first: dict[str, Any] | None = _export_via_drawio_desktop_cli(src, out, sc)
    if cli_first is not None:
        if cli_first.get("ok"):
            return cli_first
        err_t = str(cli_first.get("error") or "")
        if "not an existing file" in err_t.lower():
            return cli_first
        if offline_only:
            hint = str(cli_first.get("hint") or "").strip()
            extra = "已启用 AICD_DRAWIO_OFFLINE_ONLY，不会使用需访问 embed.diagrams.net 的导出方式。"
            return {
                **cli_first,
                "hint": f"{hint} {extra}".strip() if hint else extra,
            }
        # 已安装 CLI 但导出失败时仍尝试 Playwright + embed（便于内网失败时走在线）

    if offline_only:
        return {
            "ok": False,
            "error": "已启用 AICD_DRAWIO_OFFLINE_ONLY，但未通过本机 draw.io 桌面版完成导出。",
            "hint": "将 AICD_DRAWIO_CLI 设为安装目录下的 draw.io 可执行文件（本机路径，非 URL），"
            "或改用 diagram_mermaid_png；不需要该功能时不要设置 AICD_ENABLE_DRAWIO_EXPORT_PNG。",
        }

    try:
        from playwright.sync_api import sync_playwright
    except ImportError as e:
        return {"ok": False, "error": f"playwright not importable: {e}"}

    html = _build_parent_page(raw)
    html = (
        html.replace("__EXPORT_SCALE__", str(sc))
        .replace("__EXPORT_TRANSPARENT__", "true" if tr else "false")
        .replace("__EXPORT_BORDER__", str(bd))
    )

    work = out.parent
    work.mkdir(parents=True, exist_ok=True)
    policy_check(work)
    html_path = work / "_aicd_drawio_export_host.html"
    html_path.write_text(html, encoding="utf-8")
    policy_check(html_path)
    uri = html_path.resolve().as_uri()

    try:
        with sync_playwright() as p:
            browser = p.chromium.launch(headless=True)
            try:
                context = browser.new_context()
                page = context.new_page()
                page.set_default_timeout(timeout_ms)
                page.goto(uri, wait_until="load")
                page.wait_for_function(
                    "() => window.__DRAWIO_DONE__ === true",
                    timeout=timeout_ms,
                )
                err = page.evaluate("() => window.__DRAWIO_ERROR__")
                if err:
                    return {
                        "ok": False,
                        "error": f"draw.io embed/export failed: {err}",
                        "hint": "请检查能否访问 embed.diagrams.net（代理/防火墙）。"
                        "离线可安装 draw.io 桌面版并设置环境变量 AICD_DRAWIO_CLI 指向可执行文件。",
                    }
                data_uri = page.evaluate("() => window.__DRAWIO_PNG_URI__")
                if not data_uri or not isinstance(data_uri, str):
                    return {
                        "ok": False,
                        "error": "no PNG data returned from draw.io export",
                    }
                png_bytes = _parse_data_uri_png(data_uri)
                out.write_bytes(png_bytes)
            finally:
                browser.close()
    except Exception as e:
        out_d: dict[str, Any] = {
            "ok": False,
            "error": str(e),
            "hint": _playwright_drawio_hint(e),
        }
        if (
            cli_first is not None
            and not cli_first.get("ok")
            and (os.environ.get("AICD_DRAWIO_CLI") or "").strip()
        ):
            out_d["cli_attempt_error"] = cli_first.get("error")
        return out_d
    finally:
        try:
            html_path.unlink(missing_ok=True)
        except OSError:
            pass

    if not out.is_file() or out.stat().st_size == 0:
        return {"ok": False, "error": "PNG not written or empty"}

    return {
        "ok": True,
        "png_path": str(out),
        "source_drawio": str(src),
        "scale": sc,
        "transparent": tr,
        "border": bd,
    }
