"""样式参考：扫描目录/文件，解析 Word 用 YAML/JSON 样式配置。"""

from __future__ import annotations

import json
from pathlib import Path
from typing import Any, Callable

import yaml

TEXT_EXT = {".md", ".txt", ".markdown", ".css", ".yaml", ".yml", ".json", ".html", ".htm"}
IMAGE_EXT = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp"}


def load_docx_style_profile(
    path: Path,
    *,
    policy_check: Callable[[Path], None],
) -> dict[str, Any]:
    """
    读取 YAML 或 JSON，提取 ``docx_defaults`` 与自由文本 ``notes`` / ``freeform_notes``。

    期望结构示例::

        docx_defaults:
          default_east_asia_font: 宋体
          default_latin_font: Times New Roman
          default_color_hex: "000000"
          default_body_font_pt: 12
        notes: |
          一级标题居中，正文首行缩进两字符（在 blocks 里自行控制）
    """
    path = path.expanduser().resolve()
    policy_check(path)
    if not path.is_file():
        return {"ok": False, "error": f"not a file: {path}"}
    suf = path.suffix.lower()
    if suf not in {".yaml", ".yml", ".json"}:
        return {"ok": False, "error": "style profile must be .yaml, .yml, or .json"}
    raw = path.read_text(encoding="utf-8", errors="replace")
    try:
        if suf == ".json":
            data = json.loads(raw)
        else:
            data = yaml.safe_load(raw)
    except (yaml.YAMLError, json.JSONDecodeError) as e:
        return {"ok": False, "error": f"parse error: {e}"}
    if not isinstance(data, dict):
        return {"ok": False, "error": "root must be object"}

    dd = data.get("docx_defaults")
    if dd is not None and not isinstance(dd, dict):
        return {"ok": False, "error": "docx_defaults must be object"}
    dd = dd or {}

    notes_parts: list[str] = []
    for key in ("notes", "freeform_notes", "description"):
        v = data.get(key)
        if isinstance(v, str) and v.strip():
            notes_parts.append(v.strip())
    notes = "\n\n".join(notes_parts) if notes_parts else ""

    out: dict[str, Any] = {
        "ok": True,
        "profile_path": str(path),
        "docx_defaults": {
            "default_east_asia_font": dd.get("default_east_asia_font"),
            "default_latin_font": dd.get("default_latin_font"),
            "default_color_hex": dd.get("default_color_hex"),
            "default_body_font_pt": dd.get("default_body_font_pt"),
        },
        "notes": notes,
    }
    # drop Nones from docx_defaults for cleaner merge
    out["docx_defaults"] = {k: v for k, v in out["docx_defaults"].items() if v is not None}
    return out


def scan_style_reference(
    root: Path,
    *,
    policy_check: Callable[[Path], None],
    max_files: int = 48,
    max_text_snippet: int = 4000,
    max_total_digest: int = 28000,
) -> dict[str, Any]:
    """
    扫描单个文件或目录，汇总文本类文件片段与图片路径，供模型按用户风格生成内容。
    不调用视觉模型；图片仅列路径与尺寸（Pillow 可读时）。
    """
    root = root.expanduser().resolve()
    policy_check(root)
    entries: list[dict[str, Any]] = []
    digest_parts: list[str] = []

    def _add(kind: str, p: Path, extra: dict[str, Any]) -> None:
        if len(entries) >= max_files:
            return
        e = {"kind": kind, "path": str(p), **extra}
        entries.append(e)
        line = f"[{kind}] {p.name}: {extra.get('summary', '')[:500]}"
        digest_parts.append(line)
        if sum(len(x) for x in digest_parts) > max_total_digest:
            digest_parts.pop()

    def _one_file(p: Path) -> None:
        policy_check(p)
        suf = p.suffix.lower()
        if suf in TEXT_EXT:
            text = p.read_text(encoding="utf-8", errors="replace")
            snip = text[:max_text_snippet]
            if len(text) > max_text_snippet:
                snip += "\n…(truncated)"
            _add("text", p, {"summary": snip, "bytes": len(text.encode("utf-8"))})
        elif suf in IMAGE_EXT:
            w = h = None
            try:
                from PIL import Image

                with Image.open(p) as im:
                    w, h = im.size
            except Exception:
                pass
            _add(
                "image",
                p,
                {"summary": f"image file", "width": w, "height": h},
            )
        elif suf in {".yaml", ".yml", ".json"}:
            pr = load_docx_style_profile(p, policy_check=policy_check)
            if pr.get("ok"):
                _add(
                    "style_profile",
                    p,
                    {
                        "summary": json.dumps(
                            pr.get("docx_defaults", {}), ensure_ascii=False
                        )
                        + (f"\nnotes: {pr.get('notes', '')[:800]}" if pr.get("notes") else ""),
                    },
                )
            else:
                _add("other", p, {"summary": pr.get("error", "unreadable profile")})
        else:
            _add("other", p, {"summary": f"binary or unknown ext {suf!r}"})

    if root.is_file():
        _one_file(root)
    elif root.is_dir():
        for p in sorted(root.rglob("*")):
            if len(entries) >= max_files:
                break
            if p.is_dir():
                continue
            if p.name.startswith("."):
                continue
            rel_depth = len(p.relative_to(root).parts)
            if rel_depth > 4:
                continue
            try:
                _one_file(p)
            except PermissionError:
                continue
    else:
        return {"ok": False, "error": f"not a file or directory: {root}"}

    digest = "\n".join(digest_parts)
    if len(digest) > max_total_digest:
        digest = digest[: max_total_digest - 20] + "\n…(digest truncated)"

    return {
        "ok": True,
        "root": str(root),
        "entry_count": len(entries),
        "entries": entries,
        "style_digest": digest,
        "hint": "Use docx_defaults from style_profile entries with docx_compose; for PPTX use pptx_inspect_template on user template then pptx_compose.",
    }
