#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
check_sphinx_api_html.py

Sphinxで生成された *_api.html を走査し、
autodoc/API取り込みに失敗して本文が空っぽに近いHTMLを検出する。

例:
    python check_sphinx_api_html.py
    python check_sphinx_api_html.py --root _build/html
    python check_sphinx_api_html.py --root _build/html --files "*_api.html"
    python check_sphinx_api_html.py --outfile failed_api_files.txt
"""

import argparse
import re
import sys
from pathlib import Path
from html import unescape


def read_text(path: Path) -> str:
    """HTMLファイルを文字コード推定つきで読む。"""
    for enc in ("utf-8", "cp932", "shift_jis", "latin-1"):
        try:
            return path.read_text(encoding=enc)
        except UnicodeDecodeError:
            continue
    return path.read_bytes().decode("utf-8", errors="replace")


def strip_html(html: str) -> str:
    """HTMLタグをざっくり除去して本文テキスト化する。"""
    html = re.sub(r"(?is)<script.*?</script>", " ", html)
    html = re.sub(r"(?is)<style.*?</style>", " ", html)
    html = re.sub(r"(?is)<nav.*?</nav>", " ", html)
    html = re.sub(r"(?is)<footer.*?</footer>", " ", html)
    html = re.sub(r"(?is)<header.*?</header>", " ", html)
    text = re.sub(r"(?is)<[^>]+>", " ", html)
    text = unescape(text)
    text = re.sub(r"\s+", " ", text)
    return text.strip()


def extract_main_html(html: str) -> str:
    """
    Sphinx ReadTheDocs系HTMLから本文領域っぽい部分を抜く。
    取れなければ全体を返す。
    """
    patterns = [
        r'(?is)<div[^>]+role=["\']main["\'][^>]*>(.*?)</div>\s*</div>\s*</section>',
        r'(?is)<div[^>]+class=["\'][^"\']*document[^"\']*["\'][^>]*>(.*?)</div>\s*</section>',
        r'(?is)<section[^>]*>(.*?)</section>',
        r'(?is)<main[^>]*>(.*?)</main>',
        r'(?is)<article[^>]*>(.*?)</article>',
    ]

    for pat in patterns:
        m = re.search(pat, html)
        if m:
            return m.group(1)

    return html


def check_api_html(path: Path, min_text_len: int = 300) -> tuple[bool, list[str]]:
    """
    戻り値:
        failed: Trueなら失敗疑い
        reasons: 理由リスト
    """
    html = read_text(path)
    main_html = extract_main_html(html)
    main_text = strip_html(main_html)

    reasons = []

    # Sphinx autodocの典型的なAPI要素
    api_markers = [
        "py function",
        "py class",
        "py method",
        "py attribute",
        "py data",
        "sig-name",
        "sig-prename",
        "descname",
        "descclassname",
        "dl class=\"py",
        "dl class='py",
    ]

    marker_count = sum(1 for s in api_markers if s in html)

    # Python API名らしい signature 行
    signature_like = re.findall(
        r"<dt[^>]*class=[\"'][^\"']*sig[^\"']*[\"'][^>]*>",
        html,
        flags=re.IGNORECASE,
    )

    # 失敗ページでよくある「タイトルだけ」の判定
    has_program_spec_title = "プログラム仕様" in main_text or "API" in main_text
    text_len = len(main_text)

    if marker_count == 0 and len(signature_like) == 0:
        reasons.append("no autodoc API markers")

    if text_len < min_text_len:
        reasons.append(f"main text too short: {text_len} < {min_text_len}")

    # タイトルだけで終わっている感じの補助判定
    if has_program_spec_title and marker_count == 0 and text_len < 1000:
        reasons.append("looks like title-only API page")

    failed = len(reasons) > 0
    return failed, reasons


def find_files(root: Path, pattern: str) -> list[Path]:
    """root以下からワイルドカードに一致するファイルを再帰検索。"""
    return sorted(p for p in root.rglob(pattern) if p.is_file())


def main() -> int:
    parser = argparse.ArgumentParser(
        description="Check Sphinx *_api.html files and list pages where autodoc output looks empty."
    )
    parser.add_argument(
        "--root",
        type=str,
        default="./build/html",
        help="search root directory. default: .",
    )
    parser.add_argument(
        "--files",
        type=str,
        default="*_api.html",
        help='wildcard pattern for target files. default: "*_api.html"',
    )
    parser.add_argument(
        "--outfile",
        type=str,
        default="failed_api_html.txt",
        help="output file path. default: failed_api_html.txt",
    )
    parser.add_argument(
        "--exclude",
        action="append",
        default=[],
        help="exclude path keyword. Can be used multiple times.",
    )
    parser.add_argument(
        "--min-text-len",
        type=int,
        default=300,
        help="minimum main text length. default: 300",
    )
    parser.add_argument(
        "--show-ok",
        type=int,
        default=0,
        choices=[0, 1],
        help="show OK files too. default: 0",
    )

    args = parser.parse_args()

    root = Path(args.root).resolve()
    outfile = Path(args.outfile)

    if not root.exists():
        print(f"ERROR: root directory not found: {root}")
        return 1

    files = find_files(root, args.files)

    print(f"root    : {root}")
    print(f"pattern : {args.files}")
    print(f"found   : {len(files)} files")
    print()

    failed_files: list[tuple[Path, list[str]]] = []

    for path in files:
        rel_str = str(path.relative_to(root)).replace("\\", "/")

        if any(x in rel_str for x in args.exclude):
            if args.show_ok:
                print(f"[SKIP]   {rel_str}")
            continue

        failed, reasons = check_api_html(path, min_text_len=args.min_text_len)
        rel = path.relative_to(root)

        if failed:
            failed_files.append((path, reasons))
            print(f"[FAILED] {rel}")
            for r in reasons:
                print(f"         - {r}")
        elif args.show_ok:
            print(f"[OK]     {rel}")

    print()
    print(f"failed  : {len(failed_files)} / {len(files)}")

    with outfile.open("w", encoding="utf-8") as f:
        for path, reasons in failed_files:
            try:
                rel = path.relative_to(root)
            except ValueError:
                rel = path
            f.write(str(rel).replace("\\", "/"))
            f.write("\n")

    print(f"output  : {outfile.resolve()}")

    return 0


if __name__ == "__main__":
    sys.exit(main())