"""pptx_guard.py: 平台层 .pptx 产物机检——拦「整页贴图」式伪导出。

背景(PROGRESS 0.35.1 及本次复发):模型两次绕开 ppt skill 官方导出器
svg_to_pptx.py,用外部渲染器把每页渲成整页 PNG 贴进幻灯片交付——产物零原生
文本、不可编辑,skill 价值归零。官方管线内的门只在模型用了官方脚本时生效,
绕开管线的产物只能在平台层拦:shell / run_python 每步执行后,对本步新产出的
.pptx 就地机检,命中特征即在 tool 结果里注入 [产物机检 ERROR],逼模型当场
返工回官方管线。

判定刻意保守,只拦"确证的伪产物":整本 >= _RASTER_PAGE_RATIO 的页是
「一张覆盖 >= _PIC_AREA_RATIO 版面的图片 + 本页零文本」,且全 deck 原生文本
字符数为 0,且页数 >= 2。正常 deck(原生导出、带背景大图但有文字、单页海报、
含表格图表)都不会命中。解析失败一律放行——这门不做通用校验。
"""
from __future__ import annotations

from pathlib import Path
from typing import Iterator, List, Optional

try:
    from pptx import Presentation
    from pptx.enum.shapes import MSO_SHAPE_TYPE
except Exception:  # pragma: no cover - 宿主缺 python-pptx 时静默降级为不检
    Presentation = None
    MSO_SHAPE_TYPE = None

# 单页判「贴图页」:存在一张覆盖 >= 85% 版面的图片,且本页无任何非空文本
_PIC_AREA_RATIO = 0.85
# 整本判废:贴图页占比 >= 80%(容忍手搓时混入个别空白/封面页)
_RASTER_PAGE_RATIO = 0.8
# mtime 与 host 时钟的容差(秒)
_MTIME_EPSILON = 2.0


def _iter_shapes(shapes) -> Iterator:
    """展平 group,产出所有叶子 shape。"""
    for sh in shapes:
        try:
            if sh.shape_type == MSO_SHAPE_TYPE.GROUP:
                yield from _iter_shapes(sh.shapes)
                continue
        except Exception:
            pass
        yield sh


def find_new_pptx(root: Path, since: float) -> List[Path]:
    """root 下 mtime >= since - epsilon 的 .pptx;跳过隐藏目录(.build 等)。"""
    found: List[Path] = []
    try:
        for p in root.rglob("*.pptx"):
            try:
                rel_parts = p.relative_to(root).parts[:-1]
            except ValueError:
                rel_parts = ()
            if any(part.startswith(".") for part in rel_parts):
                continue
            try:
                if p.stat().st_mtime >= since - _MTIME_EPSILON:
                    found.append(p)
            except OSError:
                continue
    except OSError:
        pass
    return found


def check_raster_deck(path: Path) -> Optional[str]:
    """判「整页贴图」deck。返回 None = 通过;返回字符串 = 判废原因。

    打不开 / 解析失败一律放行:这门只拦确证的伪产物,不承担通用文件校验。
    """
    if Presentation is None:
        return None
    try:
        prs = Presentation(str(path))
        slides = list(prs.slides)
        if len(slides) < 2:
            return None
        page_area = float(prs.slide_width or 0) * float(prs.slide_height or 0)
        if page_area <= 0:
            return None

        raster_pages = 0
        total_text_chars = 0
        has_native_content = False  # 表格/图表也算原生内容
        for slide in slides:
            big_pic = False
            page_text = 0
            for sh in _iter_shapes(slide.shapes):
                try:
                    if sh.has_text_frame:
                        page_text += len(sh.text_frame.text.strip())
                    st = sh.shape_type
                    if st in (MSO_SHAPE_TYPE.PICTURE, MSO_SHAPE_TYPE.LINKED_PICTURE):
                        w = float(sh.width or 0)
                        h = float(sh.height or 0)
                        if w * h / page_area >= _PIC_AREA_RATIO:
                            big_pic = True
                    elif st in (MSO_SHAPE_TYPE.TABLE, MSO_SHAPE_TYPE.CHART):
                        has_native_content = True
                except Exception:
                    continue
            total_text_chars += page_text
            if big_pic and page_text == 0:
                raster_pages += 1

        if (
            total_text_chars == 0
            and not has_native_content
            and raster_pages / len(slides) >= _RASTER_PAGE_RATIO
        ):
            return (
                f"共 {len(slides)} 页,其中 {raster_pages} 页仅为一张覆盖整版的位图,"
                f"且全文档没有任何原生文本/表格/图表 —— 判定为「SVG 渲 PNG 整页贴图」"
                f"式伪导出(用户拿到的是不可编辑的图片册,不是 PPT)"
            )
        return None
    except Exception:
        return None


def scan_and_report(root: Path, since: float) -> Optional[str]:
    """扫 root 下本步新产出的 .pptx,命中伪导出则返回注入 tool 结果的 ERROR 文本。"""
    bad: List[str] = []
    for p in find_new_pptx(root, since):
        reason = check_raster_deck(p)
        if reason:
            bad.append(f"- `{p.name}`:{reason}")
    if not bad:
        return None
    return (
        "[产物机检 ERROR] 检测到不合格的 .pptx 产物:\n"
        + "\n".join(bad)
        + "\n该文件不可交付:请删除它,回 ppt skill 官方管线用 scripts/svg_to_pptx.py "
        "重新导出(它是纯 Python 原生导出,不需要任何渲染依赖;报错就按报错修,"
        "修不动就把报错原文如实告知用户)。不要另写导出脚本或改用渲染贴图。"
    )
