"""从 Ctrl+S 保存的 HTML / MHTML 提取布局真源，配合截图做更精确还原（行业无关）。"""

from __future__ import annotations

import email
import re
from email import policy
from html.parser import HTMLParser
from typing import Any


_SKIP_TAGS = {"script", "style", "noscript", "svg", "path"}
_MAX_TEXT = 48
_NAV_NOISE = {
    "欢迎您",
    "退出",
    "帮助",
    "登录",
    "注册",
    "返回",
    "首页",
    "客服",
    "电话",
    "反馈",
    "消息",
    "系统消息",
}
# 泛化业务词：用于区分「姓名」与「页签」
_BUSINESS_HINTS = (
    "列表",
    "新增",
    "创建",
    "看板",
    "概览",
    "统计",
    "报表",
    "管理",
    "设置",
    "详情",
    "汇总",
    "筛选",
    "查询",
    "导入",
    "导出",
    "设备",
    "人员",
    "信息",
    "数据",
    "记录",
    "测点",
    "断面",
    "工点",
    "观测",
    "订单",
    "商品",
    "库存",
    "客户",
)


def _is_nav_noise(text: str) -> bool:
    t = (text or "").strip()
    if not t or t in _NAV_NOISE:
        return True
    if len(t) <= 3 and re.fullmatch(r"[\u4e00-\u9fff]{2,3}", t):
        if not any(b in t for b in _BUSINESS_HINTS):
            return True
    return False


class _LayoutHTMLParser(HTMLParser):
    def __init__(self) -> None:
        super().__init__(convert_charrefs=True)
        self.title = ""
        self._in_title = False
        self._skip = 0
        self.headings: list[str] = []
        self.nav_items: list[str] = []
        self.buttons: list[str] = []
        self.labels: list[str] = []
        self.table_headers: list[list[str]] = []
        self.radio_options: list[str] = []
        self._cur_th: list[str] = []
        self._in_th = False
        self._in_nav = 0
        self._in_button = False
        self._in_label = False
        self._pending_radio = False
        self._buf = ""
        self.meta_desc = ""

    def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
        t = tag.lower()
        ad = {k.lower(): (v or "") for k, v in attrs}
        if t in _SKIP_TAGS:
            self._skip += 1
            return
        if self._skip:
            return
        if t == "title":
            self._in_title = True
            self._buf = ""
        elif t == "meta" and ad.get("name", "").lower() == "description":
            self.meta_desc = (ad.get("content") or "")[:200]
        elif t in {"nav", "header"} or "nav" in (ad.get("class") or "").lower() or ad.get("role") == "navigation":
            self._in_nav += 1
        elif t in {"button"} or ad.get("role") == "button" or (
            t == "input" and ad.get("type") in {"button", "submit", "reset"}
        ):
            self._in_button = True
            self._buf = ad.get("value") or ad.get("aria-label") or ad.get("title") or ""
        elif t == "input" and ad.get("type") == "radio":
            # 值若是纯数字/无意义，等后续文本当选项；有中文 value 则直接采用
            self._pending_radio = True
            val = (ad.get("value") or "").strip()
            if val and val not in {"", "on"} and not re.fullmatch(r"\d+", val) and len(val) <= 16:
                self.radio_options.append(val)
        elif t == "a" and self._in_nav:
            self._buf = ""
        elif t in {"h1", "h2", "h3", "h4"}:
            self._buf = ""
        elif t == "label":
            self._in_label = True
            self._buf = ""
        elif t == "th":
            self._in_th = True
            self._buf = ""
        elif t == "tr":
            self._cur_th = []

    def handle_endtag(self, tag: str) -> None:
        t = tag.lower()
        if t in _SKIP_TAGS:
            if self._skip:
                self._skip -= 1
            return
        if self._skip:
            return
        text = re.sub(r"\s+", " ", self._buf or "").strip()
        if t == "title":
            self._in_title = False
            if text:
                self.title = text[:80]
        elif t in {"nav", "header"}:
            if self._in_nav:
                self._in_nav -= 1
        elif t in {"button"} or self._in_button and t in {"input", "a", "span", "div"}:
            if text and len(text) <= _MAX_TEXT:
                self.buttons.append(text)
            self._in_button = False
        elif t == "a" and self._in_nav and text and len(text) <= _MAX_TEXT:
            if not _is_nav_noise(text):
                self.nav_items.append(text)
        elif t in {"h1", "h2", "h3", "h4"} and text and len(text) <= 64:
            if not _is_nav_noise(text):
                self.headings.append(text)
        elif t == "label":
            self._in_label = False
            cleaned = text.rstrip("：:")
            if cleaned and len(cleaned) <= _MAX_TEXT:
                # 单选旁的短文案不要当导航噪声丢掉（如「全部」「类型A」）
                if self._pending_radio and 1 <= len(cleaned) <= 12:
                    self.radio_options.append(cleaned)
                elif not _is_nav_noise(cleaned):
                    self.labels.append(cleaned)
            self._pending_radio = False
        elif t == "th":
            self._in_th = False
            if text:
                self._cur_th.append(text[:32])
        elif t == "tr" and self._cur_th:
            self.table_headers.append(self._cur_th[:12])
            self._cur_th = []
        self._buf = ""

    def handle_data(self, data: str) -> None:
        if self._skip:
            return
        if self._in_title or self._in_button or self._in_nav or self._in_th or self._in_label:
            self._buf += data
            return
        self._buf += data


def _decode_bytes(raw: bytes) -> str:
    for enc in ("utf-8", "gb18030", "gbk", "latin-1"):
        try:
            return raw.decode(enc)
        except UnicodeDecodeError:
            continue
    return raw.decode("utf-8", errors="ignore")


def extract_html_from_mhtml(raw: bytes) -> str:
    msg = email.message_from_bytes(raw, policy=policy.default)
    if msg.is_multipart():
        for part in msg.walk():
            ctype = (part.get_content_type() or "").lower()
            if ctype in {"text/html", "application/xhtml+xml"}:
                try:
                    return part.get_content()
                except Exception:  # noqa: BLE001
                    payload = part.get_payload(decode=True) or b""
                    return _decode_bytes(payload)
    text = _decode_bytes(raw)
    i = text.lower().find("<html")
    if i >= 0:
        return text[i:]
    return text


def parse_layout_file(filename: str, content: bytes) -> dict[str, Any]:
    name = (filename or "").lower()
    if name.endswith((".mhtml", ".mht")):
        html = extract_html_from_mhtml(content)
        source = "mhtml"
    elif name.endswith((".html", ".htm", ".xhtml")):
        html = _decode_bytes(content)
        source = "html"
    else:
        html = _decode_bytes(content)
        source = "text"

    if len(html) > 2_000_000:
        html = html[:2_000_000]

    parser = _LayoutHTMLParser()
    try:
        parser.feed(html)
        parser.close()
    except Exception as e:  # noqa: BLE001
        return {"ok": False, "error": str(e), "filename": filename, "source": source}

    # 兜底：radio 后紧跟的短文案（常见 value=数字 + 中文标签）
    for m in re.finditer(
        r'type=["\']radio["\'][^>]*>\s*(?:<[^>]+>\s*)*([\u4e00-\u9fffA-Za-z][\u4e00-\u9fffA-Za-z0-9]{0,11})',
        html,
        flags=re.I,
    ):
        lab = m.group(1).strip()
        if lab and not re.fullmatch(r"\d+", lab):
            parser.radio_options.append(lab)

    def uniq(items: list[str], limit: int = 40) -> list[str]:
        seen: set[str] = set()
        out: list[str] = []
        for x in items:
            x = x.strip()
            if not x or x in seen:
                continue
            seen.add(x)
            out.append(x)
            if len(out) >= limit:
                break
        return out

    ths = parser.table_headers[0] if parser.table_headers else []
    return {
        "ok": True,
        "filename": filename,
        "source": source,
        "title": parser.title,
        "meta_description": parser.meta_desc,
        "headings": uniq(parser.headings, 20),
        "nav_items": uniq(parser.nav_items, 24),
        "buttons": uniq(parser.buttons, 30),
        "labels": uniq(parser.labels, 30),
        "radio_options": uniq(parser.radio_options, 12),
        "table_headers": uniq(ths, 16),
        "table_header_rows": parser.table_headers[:3],
    }


def layout_summary_text(parsed: dict[str, Any]) -> str:
    if not parsed or not parsed.get("ok"):
        err = (parsed or {}).get("error") or "parse failed"
        return f"[页面 HTML 解析失败] {err}"
    lines = [
        "[页面 HTML 布局真源 · 优先于猜测 · 禁止套用其它行业模板]",
        f"文件: {parsed.get('filename')} ({parsed.get('source')})",
    ]
    if parsed.get("title"):
        lines.append(f"文档标题: {parsed['title']}")
    if parsed.get("headings"):
        lines.append("标题层级: " + " / ".join(parsed["headings"][:12]))
    if parsed.get("nav_items"):
        lines.append("导航页签: " + "、".join(parsed["nav_items"][:16]))
    if parsed.get("buttons"):
        lines.append("操作按键: " + "、".join(parsed["buttons"][:20]))
    if parsed.get("labels"):
        lines.append("分区/筛选项标签: " + "、".join(parsed["labels"][:20]))
    if parsed.get("radio_options"):
        lines.append("单选选项: " + "、".join(parsed["radio_options"][:12]))
    if parsed.get("table_headers"):
        lines.append("表格列: " + "、".join(parsed["table_headers"][:16]))
    lines.append(
        "要求：pages 标题、导航、action_labels、表格列、筛选文案必须优先采用上述 HTML 原文；"
        "meta.ui_preset=screenshot_faithful；文案以 HTML 为准、分区以截图为准；"
        "禁止擅自换成其它业务的固定文案。"
    )
    return "\n".join(lines)


def merge_layout_into_ui_hints(hints: dict[str, Any], parsed: dict[str, Any]) -> dict[str, Any]:
    """把 HTML 解析结果并入 ui_hints。只使用文件里的原文，不写死行业词。"""
    if not parsed or not parsed.get("ok"):
        return hints
    ui = hints.setdefault("ui", {})

    if not hints.get("app_name") and parsed.get("title"):
        title = str(parsed["title"])
        parts = re.split(r"\s*[-_|｜]\s*", title)
        if parts:
            cand = parts[0][:64]
            if cand and not _is_nav_noise(cand):
                hints["app_name"] = cand
            if len(parts) > 1 and "platform_title" not in ui:
                ui["platform_title"] = parts[-1][:64]

    labels = [x for x in (parsed.get("labels") or []) if x and not _is_nav_noise(x)]
    # 按出现顺序把较长分区标签分给 图 / 条 / 表（通用，不认行业）
    region_labels = [x for x in labels if len(x) >= 4 and not x.startswith("(")]
    if region_labels:
        if len(region_labels) >= 1:
            ui.setdefault("chart_side_label", region_labels[0])
            hints.setdefault("chart_title", region_labels[0])
        if len(region_labels) >= 2:
            ui.setdefault("strip_side_label", region_labels[1])
            hints.setdefault("strip_title", region_labels[1])
        if len(region_labels) >= 3:
            ui.setdefault("table_side_label", region_labels[2])
        # 表格横标题：优先含「表」或最长的后续标签
        for lab in reversed(region_labels):
            if "表" in lab or len(lab) >= 8:
                hints.setdefault("table_title", lab)
                ui.setdefault("table_title", lab)
                break

    nav = [x for x in (parsed.get("nav_items") or []) if not _is_nav_noise(x)]
    if nav:
        ui["nav_items"] = nav[:16]
        for n in nav:
            if any(k in n for k in ("看板", "概览", "统计", "dashboard")):
                hints.setdefault("dash_title", n)
            elif any(k in n for k in ("新增", "创建", "新建")):
                hints.setdefault("create_title", n)
            elif any(k in n for k in ("列表", "明细", "记录", "数据", "测点", "订单", "商品")):
                hints.setdefault("list_title", n)

    if parsed.get("buttons"):
        hints["_html_buttons_line"] = "操作：" + "、".join(parsed["buttons"][:20])
    if parsed.get("table_headers"):
        ui["table_headers"] = parsed["table_headers"]
    if labels:
        ui.setdefault("filter_labels", labels[:12])

    radios = [
        x
        for x in (parsed.get("radio_options") or [])
        if x
        and x not in {"全部", "all", "All"}
        and not re.fullmatch(r"\d+", x)
        and len(x) <= 12
    ]
    if len(radios) >= 2:
        ui.setdefault("section_options", radios[:8])
        ui.setdefault("filter_radios", radios[:8])
        ui.setdefault("filter_style", "section_radios")

    return hints
