#!/usr/bin/env python3
"""
把 Markdown + manifest 渲染成：
1. 带内联样式的完整 HTML（便于本地检查）
2. 可直接提交给微信公众号 draft API 的正文 fragment

要求：
- 图片必须先在 manifest 里锁定 WeChat URL
- 固定头图 / 正文图 / 固定尾图都走 Markdown 原始顺序替换
"""

from __future__ import annotations

import argparse
import json
import os
import subprocess
import sys
from pathlib import Path
from typing import Any

from bs4 import BeautifulSoup, NavigableString

ARTICLE_INLINE_STYLE = """
.article {
  max-width: 760px;
  margin: 0 auto;
  padding: 24px 22px 60px;
  color: #222;
  background: #fff;
  font-family: -apple-system,BlinkMacSystemFont,"PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif;
  font-size: 13px;
  line-height: 1.92;
}
.title-wrap { margin: 8px 0 28px; }
.title-wrap h1 {
  margin: 0;
  text-align: center;
  font-family: "Songti SC","STSong","Noto Serif CJK SC",serif;
  font-size: 2em;
  line-height: 1.5;
  font-weight: 700;
  color: #1a1a1a;
}
.chapter-title { margin: 52px 0 26px; }
.chapter-title h2 {
  margin: 0;
  text-align: center;
  font-family: "Songti SC","STSong","Noto Serif CJK SC",serif;
  font-size: 1.46em;
  line-height: 1.7;
  font-weight: 700;
  color: #1f1f1f;
}
.img-wrap { margin: 26px 0; }
.img-wrap img { display: block; max-width: 100%; margin: 0 auto; }
.sep { border: none; border-top: 1px solid #ece7df; margin: 30px 0; }
"""


def fail(message: str) -> None:
    print(f"[render-wechat-inline] ERROR: {message}", file=sys.stderr)
    raise SystemExit(1)


def run_command(cmd: list[str], cwd: Path | None = None) -> str:
    print(f"[render-wechat-inline] 执行命令：{' '.join(cmd)}", file=sys.stderr)
    result = subprocess.run(
        cmd,
        cwd=str(cwd) if cwd else None,
        capture_output=True,
        text=True,
        check=False,
    )
    if result.stderr:
        print(result.stderr, file=sys.stderr, end="")
    if result.returncode != 0:
        fail(f"命令失败：{' '.join(cmd)}")
    return result.stdout


def load_json(path: Path) -> Any:
    try:
        return json.loads(path.read_text())
    except Exception as exc:  # noqa: BLE001
        fail(f"JSON 解析失败：{path} -> {exc}")


def resolve_asset_by_original_path(manifest: dict[str, Any], manifest_path: Path, markdown_path: Path, original_path: str) -> dict[str, Any]:
    markdown_base = markdown_path.parent
    original_abs = (markdown_base / original_path).resolve() if not Path(original_path).is_absolute() else Path(original_path).resolve()
    original_name = Path(original_path).name

    for asset in manifest.get("assets", []):
        local_path = str(asset.get("local_path", "")).strip()
        if not local_path:
            continue
        asset_abs = (manifest_path.parent / local_path).resolve()
        if asset_abs == original_abs or asset_abs.name == original_name:
            return asset

    fail(f"manifest 中找不到图片：{original_path}")


def build_image_section(url: str) -> BeautifulSoup:
    fragment = BeautifulSoup("", "lxml")
    section = fragment.new_tag("section")
    section["style"] = "margin: 20px 0;"
    img = fragment.new_tag("img")
    img["data-src"] = url
    img["style"] = "max-width:100%;display:block;margin:0 auto;"
    section.append(img)
    fragment.append(section)
    return fragment


def replace_placeholder(root: Any, placeholder: str, url: str) -> None:
    target_node: NavigableString | None = None
    for text_node in root.find_all(string=True):
        if placeholder in str(text_node):
            target_node = text_node
            break

    if target_node is None:
        fail(f"没有在 HTML 里找到占位符：{placeholder}")

    fragment = build_image_section(url)
    new_section = fragment.section
    if new_section is None:
        fail(f"生成图片 section 失败：{placeholder}")

    parent = target_node.parent
    text = str(target_node)

    if parent is not None and text.strip() == placeholder and len(parent.contents) == 1:
        parent.replace_with(new_section)
        return

    if placeholder == text:
        target_node.replace_with(new_section)
        return

    parts = text.split(placeholder)
    current = []
    for index, part in enumerate(parts):
        if part:
            current.append(NavigableString(part))
        if index < len(parts) - 1:
            current.append(new_section if index == 0 else build_image_section(url).section)
    target_node.replace_with(*current)


def is_image_only_section(tag: Any) -> bool:
    if not getattr(tag, "name", None) == "section":
        return False

    meaningful = []
    for child in tag.contents:
        if isinstance(child, NavigableString):
            if child.strip():
                meaningful.append(child)
        else:
            meaningful.append(child)

    return len(meaningful) == 1 and getattr(meaningful[0], "name", None) == "img"


def transform_output_to_article(output: Any) -> Any:
    current = output
    while getattr(current, "parent", None) is not None:
        current = current.parent

    soup = current if isinstance(current, BeautifulSoup) else None
    if soup is None:
        fail("无法获取输出 DOM 上下文")

    article = soup.new_tag("section")
    article["class"] = ["article"]

    style = soup.new_tag("style")
    style.string = ARTICLE_INLINE_STYLE
    article.append(style)

    source_root = output
    meaningful_children = []
    for child in output.contents:
        if isinstance(child, NavigableString):
            if child.strip():
                meaningful_children.append(child)
            continue
        meaningful_children.append(child)

    if (
        len(meaningful_children) == 1
        and getattr(meaningful_children[0], "name", None) in {"section", "div"}
        and "container" in list(meaningful_children[0].get("class", []))
    ):
        source_root = meaningful_children[0]

    for child in list(source_root.contents):
        if isinstance(child, NavigableString):
            if child.strip():
                article.append(child.extract())
            else:
                child.extract()
            continue

        if getattr(child, "name", None) == "h1":
            header = soup.new_tag("header")
            header["class"] = ["title-wrap"]
            header.append(child.extract())
            article.append(header)
            continue

        if getattr(child, "name", None) == "h2":
            section = soup.new_tag("section")
            section["class"] = ["chapter-title"]
            section.append(child.extract())
            article.append(section)
            continue

        if getattr(child, "name", None) == "hr":
            classes = list(child.get("class", []))
            if "sep" not in classes:
                classes.append("sep")
            child["class"] = classes
            article.append(child.extract())
            continue

        if is_image_only_section(child):
            figure = soup.new_tag("figure")
            figure["class"] = ["img-wrap"]
            img = child.find("img")
            if img is None:
                fail("图片 section 缺少 img 节点")
            figure.append(img.extract())
            child.extract()
            article.append(figure)
            continue

        article.append(child.extract())

    if source_root is not output:
        source_root.extract()

    output.clear()
    output.append(article)
    return article


def extract_output_fragment(full_html_path: Path, fragment_output_path: Path, manifest_path: Path, markdown_path: Path, parsed: dict[str, Any]) -> None:
    soup = BeautifulSoup(full_html_path.read_text(), "lxml")
    output = soup.select_one("#output")
    if output is None:
        fail("内联 HTML 缺少 #output 容器")

    manifest = load_json(manifest_path)
    for image in parsed.get("contentImages", []):
        original_path = str(image.get("originalPath", "")).strip()
        placeholder = str(image.get("placeholder", "")).strip()
        if not original_path or not placeholder:
            fail(f"图片元数据不完整：{image}")

        asset = resolve_asset_by_original_path(manifest, manifest_path, markdown_path, original_path)
        wechat_url = str(asset.get("wechat_url", "")).strip()
        if not wechat_url:
            fail(f"图片缺少 wechat_url：{asset.get('key')}")
        replace_placeholder(output, placeholder, wechat_url)

    article = transform_output_to_article(output)

    fragment = "".join(str(child) for child in output.contents).strip()
    if "IMAGE_PLACEHOLDER" in fragment:
        fail("仍然存在未替换的图片占位符")
    if '.chapter-title' not in fragment or '.img-wrap' not in fragment:
        fail("渲染结果缺少正文结构样式定义")

    full_html_path.write_text(str(soup))
    fragment_output_path.write_text(str(article))


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="把 Markdown 渲染成微信公众号可更新的内联 HTML")
    parser.add_argument("--markdown", required=True, help="源 Markdown 路径")
    parser.add_argument("--manifest", required=True, help="素材 manifest 路径")
    parser.add_argument("--full-output", required=True, help="完整内联 HTML 输出路径")
    parser.add_argument("--fragment-output", required=True, help="公众号 draft API 正文 fragment 输出路径")
    parser.add_argument("--theme", default="wenyan", help="主题名，默认 wenyan")
    return parser.parse_args()


def main() -> None:
    args = parse_args()
    markdown_path = Path(args.markdown).resolve()
    manifest_path = Path(args.manifest).resolve()
    full_output_path = Path(args.full_output).resolve()
    fragment_output_path = Path(args.fragment_output).resolve()

    if not markdown_path.exists():
        fail(f"Markdown 不存在：{markdown_path}")
    if not manifest_path.exists():
        fail(f"manifest 不存在：{manifest_path}")

    script_dir = Path(__file__).resolve().parent
    candidate_paths = []
    if "WECHAT_MD_TO_WECHAT_PATH" in os.environ:
        candidate_paths.append(Path(os.environ["WECHAT_MD_TO_WECHAT_PATH"]).expanduser().resolve())
    candidate_paths.append(script_dir / "vendor" / "md-to-wechat.ts")
    candidate_paths.append(
        script_dir.parent.parent
        / "baoyu-skills"
        / "skills"
        / "baoyu-post-to-wechat"
        / "scripts"
        / "md-to-wechat.ts"
    )

    md_to_wechat_path = next((item for item in candidate_paths if item.exists()), None)
    if md_to_wechat_path is None:
        fail("md-to-wechat.ts 不存在；请设置 WECHAT_MD_TO_WECHAT_PATH，或把文件放到 scripts/vendor/md-to-wechat.ts")

    render_stdout = run_command(
        ["npx", "-y", "bun", str(md_to_wechat_path), str(markdown_path), "--theme", args.theme],
        cwd=markdown_path.parent,
    )

    try:
        parsed = json.loads(render_stdout)
    except Exception as exc:  # noqa: BLE001
        fail(f"md-to-wechat 输出不是合法 JSON：{exc}\n{render_stdout[:500]}")

    rendered_html_path = Path(str(parsed.get("htmlPath", "")).strip())
    if not rendered_html_path.exists():
        fail(f"md-to-wechat 未产出 HTML：{rendered_html_path}")

    full_output_path.parent.mkdir(parents=True, exist_ok=True)
    fragment_output_path.parent.mkdir(parents=True, exist_ok=True)

    run_command(
        [
            "npx",
            "-y",
            "juice",
            "--apply-style-tags",
            "true",
            "--remove-style-tags",
            "true",
            "--resolve-css-variables",
            "true",
            str(rendered_html_path),
            str(full_output_path),
        ],
        cwd=markdown_path.parent,
    )

    if not full_output_path.exists():
        fail(f"juice 未产出完整 HTML：{full_output_path}")

    extract_output_fragment(full_output_path, fragment_output_path, manifest_path, markdown_path, parsed)

    summary = {
        "markdown": str(markdown_path),
        "manifest": str(manifest_path),
        "theme": args.theme,
        "full_output": str(full_output_path),
        "fragment_output": str(fragment_output_path),
        "rendered_html": str(rendered_html_path),
        "image_count": len(parsed.get("contentImages", [])),
    }
    print(json.dumps(summary, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()
