#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
静态站点自检
============

扫描 `site/` 下所有 HTML 的 href / src 与 CSS 的 url()，验证每一个站内引用
在磁盘上真实存在；同时检查 UTF-8 编码、标题、体积等常见问题。

    python3 site_build/verify_site.py
    python3 site_build/verify_site.py --site site
"""

from __future__ import annotations

import argparse
import re
import sys
from pathlib import Path
from urllib.parse import unquote, urlparse

from html.parser import HTMLParser

REF_RE = re.compile(r'(?:href|src)\s*=\s*"([^"]*)"', re.I)
CSS_URL_RE = re.compile(r'url\(\s*[\'"]?([^\'")]+)[\'"]?\s*\)')
SKIP_SCHEMES = ("http://", "https://", "mailto:", "tel:", "data:", "javascript:", "//")

# 会话存档的正文里，模型输出的 HTML 片段与绝对路径会被原样保留，
# 这些不是站点的导航链接，检查时按「内容痕迹」跳过并单独汇总。
ARTIFACT_HINTS = ("<", ">", "\n", " ", "/Users/", ".reasonix/", "\\")
# 归档正文里对源仓库 .md 的引用：站点已用 HTML 版替代，非站点导航
CONTENT_SUFFIXES = (".md",)


def is_artifact(ref: str) -> bool:
    return any(h in ref for h in ARTIFACT_HINTS)


class _RefParser(HTMLParser):
    """收集真实导航属性，跳过代码块内部。"""

    SKIP = {"pre", "code", "script", "style", "textarea", "samp", "kbd"}
    ATTRS = ("href", "src", "data-lazy-src", "poster")

    def __init__(self):
        super().__init__(convert_charrefs=True)
        self.refs: list[str] = []
        self._depth = 0

    def handle_starttag(self, tag, attrs):
        if tag in self.SKIP:
            self._depth += 1
            return
        if self._depth:
            return
        for k, v in attrs:
            if k in self.ATTRS and v:
                self.refs.append(v)

    def handle_endtag(self, tag):
        if tag in self.SKIP and self._depth:
            self._depth -= 1


def collect_refs(html: str, css: str = ""):
    """提取真正属于站点导航的 href / src / CSS url()。

    会话存档正文里内嵌了大量 HTML 片段（导出的对话文本，不是链接），
    因此用 HTML 解析器跳过 <pre> / <code> 等元素内的属性，避免误报。
    """
    parser = _RefParser()
    try:
        parser.feed(html)
        parser.close()
    except Exception:
        pass
    refs = list(parser.refs)
    for style in re.findall(r"<style[^>]*>(.*?)</style>", html, re.S | re.I):
        refs += CSS_URL_RE.findall(style)
    if css:
        refs += CSS_URL_RE.findall(css)
    return refs


def check(site: Path):
    errors: list[str] = []
    warnings: list[str] = []
    checked = 0
    pages = 0
    artifacts = 0

    # 独立 CSS 里的引用（相对站点根解析）
    shared_css = []
    for css_file in site.rglob("*.css"):
        shared_css.append((css_file, css_file.read_text(encoding="utf-8", errors="replace")))
        for u in CSS_URL_RE.findall(shared_css[-1][1]):
            if u.startswith(SKIP_SCHEMES):
                continue
            target = (css_file.parent / unquote(u.split("#")[0].split("?")[0])).resolve()
            checked += 1
            if not target.exists():
                errors.append(f"{css_file.relative_to(site)} → {u}（不存在）")

    html_files = sorted(site.rglob("*.html"))
    for f in html_files:
        pages += 1
        raw = f.read_bytes()
        if raw[:3] == b"\xef\xbb\xbf":
            warnings.append(f"{f.relative_to(site)}：文件带 BOM")
        try:
            html = raw.decode("utf-8")
        except UnicodeDecodeError as e:
            errors.append(f"{f.relative_to(site)}：不是合法 UTF-8（{e}）")
            continue
        if "<title>" not in html:
            warnings.append(f"{f.relative_to(site)}：缺少 <title>")
        if 'charset="utf-8"' not in html.lower() and "charset=utf-8" not in html.lower():
            warnings.append(f"{f.relative_to(site)}：未声明 charset")

        for ref in collect_refs(html):
            ref = ref.strip()
            if not ref or ref.startswith(SKIP_SCHEMES) or ref.startswith("#"):
                continue
            if is_artifact(ref) or unquote(ref).lower().split("#")[0].endswith(CONTENT_SUFFIXES):
                artifacts += 1
                continue
            parsed = urlparse(ref)
            path = unquote(parsed.path.split("#")[0].split("?")[0])
            if not path:
                continue
            target = (f.parent / path).resolve()
            if path.endswith("/"):
                target = target / "index.html"
            checked += 1
            if not target.exists():
                errors.append(f"{f.relative_to(site)} → {ref}")

    # 搜索索引里的每个 URL
    idx = site / "assets" / "search-index.json"
    if idx.exists():
        import json
        try:
            data = json.loads(idx.read_text(encoding="utf-8"))
            for item in data:
                target = (site / unquote(item["u"])).resolve()
                checked += 1
                if not target.exists():
                    errors.append(f"search-index.json → {item['u']}（{item['t']}）")
        except json.JSONDecodeError as e:
            errors.append(f"search-index.json 解析失败：{e}")

    print(f"检查完成：{pages} 个 HTML 页面，{checked} 个站内引用"
          + (f"（另有 {artifacts} 处会话正文里的内容痕迹已跳过）" if artifacts else ""))
    if warnings:
        print(f"\n警告 {len(warnings)} 条：")
        for w in warnings[:20]:
            print("  ! " + w)
    if errors:
        print(f"\n错误 {len(errors)} 条：")
        for e in errors[:40]:
            print("  ✗ " + e)
        return 1
    print("\n✓ 所有站内引用均可达")
    return 0


def main():
    ap = argparse.ArgumentParser(description="检查静态站点")
    here = Path(__file__).resolve().parent.parent
    ap.add_argument("--site", default=str(here / "site"))
    args = ap.parse_args()
    site = Path(args.site).resolve()
    if not site.is_dir():
        sys.exit(f"站点目录不存在：{site}")
    sys.exit(check(site))


if __name__ == "__main__":
    main()
