"""
技术暗标 HTML 格式检查（由 清标工具.js 迁移，不依赖浏览器/jsdom）。
仅解析内联 style 与文档内 <style> 中的 @page 简单规则；无内联样式时部分项可能判为不符合。
"""
from __future__ import annotations

import re
from typing import Any

from bs4 import BeautifulSoup, Tag

# 1pt ≈ 96/72 px (CSS 标准)
_PT_PX = 96.0 / 72.0

# 三号 16pt / 四号 14pt / 五号 10.5pt / 行距 26pt
_TARGET_H = 16 * _PT_PX  # 21.333...
_TARGET_BODY = 14 * _PT_PX
_TARGET_LH = 26 * _PT_PX
_TARGET_FIG = 10.5 * _PT_PX


def _parse_style_attr(style: str | None) -> dict[str, str]:
    if not style or not style.strip():
        return {}
    out: dict[str, str] = {}
    for part in style.split(";"):
        part = part.strip()
        if ":" not in part:
            continue
        k, v = part.split(":", 1)
        k, v = k.strip().lower(), v.strip()
        if k:
            out[k] = v
    return out


def _num(s: str) -> float:
    try:
        return float(re.sub(r"[^\d.\-]", "", s) or "nan")
    except ValueError:
        return float("nan")


def _length_to_px(val: str, font_size_px: float | None = None) -> float:
    """将 font-size / line-height 等长度转为近似 px 浮点，用于与 JS 中 getComputedStyle(px) 对齐。"""
    val = (val or "").strip().lower()
    if not val or val in ("normal", "inherit", "initial"):
        return float("nan")
    if val.isdigit():
        return float(val)
    m = re.match(r"^([\d.]+)\s*(pt|px|em|rem)?\s*$", val)
    if not m:
        m2 = re.match(r"^([\d.]+)", val)
        return float(m2.group(1)) if m2 else float("nan")
    n, unit = float(m.group(1)), (m.group(2) or "px")
    if unit == "pt":
        return n * _PT_PX
    if unit == "px":
        return n
    if unit in ("em", "rem") and font_size_px and font_size_px == font_size_px:
        return n * font_size_px
    if unit in ("em", "rem"):
        return n  # 无字号时仅返回 em 数，供 text-indent 等判断
    return n


def _indent_value(style: dict[str, str], font_size_px: float) -> float:
    """与 JS 中 parseFloat(textIndent) 对齐：'2em' -> 2.0；'2ch' 等取首数字段。"""
    raw = (style.get("text-indent") or "").strip()
    if not raw:
        return float("nan")
    if "em" in raw.lower():
        m = re.search(r"([\d.]+)\s*em", raw, re.I)
        return float(m.group(1)) if m else _num(raw)
    # px 转 em 近似
    px = _length_to_px(raw, font_size_px)
    if px == px and font_size_px > 0:
        return px / font_size_px
    return _num(raw)


def _color_normalized(style: dict[str, str]) -> str:
    c = (style.get("color") or "").strip().lower()
    if not c:
        return ""
    c = c.replace(" ", "")
    if c in ("#000", "#000000", "black", "rgb(0,0,0)"):
        return "rgb(0, 0, 0)"
    m = re.match(r"rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)", c)
    if m:
        r, g, b = int(m.group(1)), int(m.group(2)), int(m.group(3))
        if r == 0 and g == 0 and b == 0:
            return "rgb(0, 0, 0)"
        return c
    return c


def _el_style_dict(tag: Tag) -> dict[str, str]:
    s = tag.get("style")
    if isinstance(s, str):
        return _parse_style_attr(s)
    if isinstance(s, list):
        return _parse_style_attr(";".join(s))
    return {}


def _get_inline_property(tag: Tag, prop: str) -> str:
    d = _el_style_dict(tag)
    return d.get(prop.lower(), "")


def _outer_html_sample(tag: Tag, limit: int = 200) -> str:
    s = str(tag)
    return s[:limit] if len(s) > limit else s


def _is_under(node: Tag | None, ancestor: Tag | None) -> bool:
    if node is None or ancestor is None:
        return False
    p: Tag | None = node
    while p is not None:
        if p is ancestor:
            return True
        p = p.parent
    return False


def _body_text(soup: BeautifulSoup) -> str:
    body = soup.body
    if not body:
        return soup.get_text("\n", strip=True)
    return body.get_text("\n", strip=True)


def _parse_page_margins_from_html(raw_html: str) -> dict[str, str] | None:
    """从 <style> 中粗提取 @page 块内 margin 与 size。"""
    for m in re.finditer(
        r"@page\s*\{([^}]+)\}",
        raw_html,
        re.I | re.DOTALL,
    ):
        block = m.group(1)
        msh = re.search(r"margin\s*:\s*([^;]+);", block, re.I)
        if msh:
            return {"shorthand": msh.group(1).strip()}
        margins: dict[str, str] = {}
        for name, key in (
            (r"margin-top\s*:\s*([^;]+)", "top"),
            (r"margin-bottom\s*:\s*([^;]+)", "bottom"),
            (r"margin-left\s*:\s*([^;]+)", "left"),
            (r"margin-right\s*:\s*([^;]+)", "right"),
            (r"size\s*:\s*([^;]+)", "size"),
        ):
            mm = re.search(name, block, re.I)
            if mm:
                margins[key] = mm.group(1).strip()
        if margins:
            return margins
    return None


def check_technical_bid(html_content: str) -> dict[str, Any]:
    """
    对技术暗标 HTML 执行格式检查。
    返回结构与清标数据.json 一致：overall, details, violations。
    """
    results: dict[str, Any] = {
        "overall": True,
        "details": [],
        "violations": [],
    }

    def add_result(
        rule_name: str,
        passed: bool,
        message: str,
        elements: list[Tag] | None = None,
    ) -> None:
        results["details"].append(
            {"rule": rule_name, "passed": passed, "message": message}
        )
        if not passed:
            results["overall"] = False
            el_snips: list[str] = []
            for el in elements or []:
                if isinstance(el, Tag):
                    el_snips.append(_outer_html_sample(el))
            results["violations"].append(
                {"rule": rule_name, "message": message, "elements": el_snips}
            )

    if not (html_content or "").strip():
        add_result("身份信息隐藏", False, "HTML 内容为空", [])
        return results

    raw_html = html_content
    soup = BeautifulSoup(html_content, "lxml")
    if not soup.body:
        soup = BeautifulSoup(f"<html><body>{html_content}</body></html>", "lxml")

    body = soup.body
    if not body:
        add_result("身份信息隐藏", False, "无法解析 body", [])
        return results

    # ---- 1. 身份 ----
    body_text = _body_text(soup)
    company_pattern = re.compile(
        r"(?:我公司|本公司|[（(]?[A-Za-z\u4e00-\u9fa5]+(?:集团|股份|有限|责任|公司)[）)]?)"
    )
    addr_pattern = re.compile(
        r"(?:省|市|区|县|镇|路|街|大道|号|大厦|楼|层)[\u4e00-\u9fa50-9]+"
    )
    name_pattern = re.compile(
        r"(?:总监理工程师|专业监理工程师|技术负责人|项目经理)[：:]\s*"
        r"[^甲乙丙丁戊己庚辛壬癸\s]{2,4}(?=[，。；\s]|$)"
    )
    found_company = bool(company_pattern.search(body_text))
    found_addr = bool(addr_pattern.search(body_text))
    found_name = bool(name_pattern.search(body_text))
    has_logo = False
    for img in soup.find_all("img"):
        if not isinstance(img, Tag):
            continue
        alt = (img.get("alt") or "") + ""
        src = (img.get("src") or "") + ""
        if re.search(r"logo|商标|微标|公司|品牌", alt, re.I) or re.search(
            r"logo", src, re.I
        ):
            has_logo = True
            break
    passed_id = not (
        found_company or found_addr or found_name or has_logo
    )
    add_result(
        "身份信息隐藏",
        passed_id,
        "未发现投标人身份信息"
        if passed_id
        else "发现投标人身份信息（公司名/地址/真实姓名/商标）",
    )

    def heading_style_ok(tag: Tag) -> bool:
        st = _el_style_dict(tag)
        fs_raw = st.get("font-size", "")
        fs_px = _length_to_px(fs_raw)
        if "em" in (fs_raw or "").lower() and "rem" not in (fs_raw or "").lower():
            fs_px = _num(fs_raw) * 16.0
        size_ok = abs(fs_px - _TARGET_H) <= 3
        fam = (st.get("font-family") or "").lower()
        font_ok = "黑体" in fam or "simhei" in fam or "microsoft yahei" in fam
        font_style = (st.get("font-style") or "").lower()
        style_ok = font_style != "italic"
        text_dec = (st.get("text-decoration") or "").lower()
        decor_ok = "underline" not in text_dec
        cr = (st.get("color") or "").strip().lower()
        if not cr or cr in ("inherit", "initial"):
            color_ok = True
        else:
            cn = _color_normalized(st)
            color_ok = cn == "rgb(0, 0, 0)" or cr in (
                "#000",
                "#000000",
                "black",
                "rgb(0,0,0)",
            )
        fw = (st.get("font-weight") or "400").lower()
        weight_ok = fw not in ("400", "normal")
        if not st.get("font-size"):
            size_ok = False
        return (
            size_ok
            and font_ok
            and style_ok
            and decor_ok
            and color_ok
            and weight_ok
        )

    # ---- 2. 标题 ----
    heading_tags: list[Tag] = []
    for sel in ("h1", "h2", "h3", "h4", "h5", "h6"):
        heading_tags.extend(soup.find_all(sel))
    for t in soup.find_all(attrs={"role": "heading"}):
        if isinstance(t, Tag):
            heading_tags.append(t)
    for t in soup.select(".heading, .title"):
        if isinstance(t, Tag) and t not in heading_tags:
            heading_tags.append(t)

    invalid_h: list[Tag] = []
    for h in heading_tags:
        if not isinstance(h, Tag):
            continue
        if not heading_style_ok(h):
            invalid_h.append(h)
    h_ok = len(invalid_h) == 0
    add_result(
        "标题格式",
        h_ok,
        "所有标题符合三号黑体要求"
        if h_ok
        else "部分标题字号/字体/颜色/下划线不符合要求",
        invalid_h,
    )

    def body_el_ok(el: Tag) -> bool:
        st = _el_style_dict(el)
        if el.name in ("h1", "h2", "h3", "h4", "h5", "h6"):
            return True
        cls = " ".join(el.get("class", [])) if el.get("class") else ""
        if any(
            x in cls
            for x in ("header", "footer", "toc", "目录", "table-of-contents")
        ):
            return True
        text = el.get_text(strip=True)
        if not text:
            return True
        fs_raw = st.get("font-size", "")
        font_px = _length_to_px(fs_raw)
        if not fs_raw:
            return False
        size_ok = abs(font_px - _TARGET_BODY) <= 2
        fam = (st.get("font-family") or "").lower()
        font_ok = "宋体" in fam or "simsun" in fam or "serif" in fam
        col = st.get("color", "")
        color_ok = (not col) or _color_normalized(st) == "rgb(0, 0, 0)" or col.lower() in (
            "#000",
            "#000000",
            "black",
            "rgb(0,0,0)",
        )
        ind = _indent_value(st, font_px)
        indent_ok = ind == ind and 1.8 <= ind <= 2.2
        lh_raw = (st.get("line-height") or "").strip()
        if not lh_raw:
            line_ok = False
        else:
            if "pt" in lh_raw or "px" in lh_raw:
                lh_px = _length_to_px(lh_raw, font_px)
            elif re.match(r"^[\d.]+$", lh_raw):
                lh_px = float(lh_raw) * font_px
            else:
                lh_px = _length_to_px(lh_raw, font_px)
            line_ok = abs(lh_px - _TARGET_LH) <= 2
        tdec = (st.get("text-decoration") or "").lower()
        decor_ok = "underline" not in tdec
        fw = (st.get("font-weight") or "400").lower()
        weight_ok = fw in ("400", "normal", "")
        fst = (st.get("font-style") or "").lower()
        style_ok = fst != "italic"
        return (
            size_ok
            and font_ok
            and color_ok
            and indent_ok
            and line_ok
            and decor_ok
            and weight_ok
            and style_ok
        )

    exclude_set = {
        "h1",
        "h2",
        "h3",
        "h4",
        "h5",
        "h6",
    }
    invalid_body: list[Tag] = []
    for el in soup.find_all(["p", "div", "span", "li", "td", "th"]):
        if not isinstance(el, Tag):
            continue
        if el.name in exclude_set:
            continue
        if "header" in " ".join(el.get("class", [])):
            continue
        if "footer" in " ".join(el.get("class", [])):
            continue
        if "toc" in " ".join(el.get("class", [])) or "目录" in " ".join(
            el.get("class", [])
        ):
            continue
        if not el.get_text(strip=True):
            continue
        if not body_el_ok(el):
            invalid_body.append(el)

    b_ok = len(invalid_body) == 0
    add_result(
        "正文格式",
        b_ok,
        "所有正文符合四号宋体/缩进/行距/颜色要求"
        if b_ok
        else "部分正文段落格式不符合要求",
        invalid_body,
    )

    # ---- 4. 目录 ----
    toc_els: list[Tag] = []
    for cls in ("toc", "table-of-contents", "目录"):
        for t in soup.find_all(class_=cls):
            if isinstance(t, Tag) and t not in toc_els:
                toc_els.append(t)
    for t in soup.find_all(attrs={"role": "directory"}):
        if isinstance(t, Tag) and t not in toc_els:
            toc_els.append(t)

    if not toc_els:
        add_result("目录要求", False, "未检测到目录，请确保包含目录且目录无页码无页眉页脚")
    else:
        no_pn = True
        no_hf = True
        for toc in toc_els:
            text = toc.get_text("\n", strip=True)
            lines = [ln.strip() for ln in text.splitlines() if ln.strip()]
            for line in lines:
                if re.search(r"\d+\s*$", line) and re.search(r"\d$", line):
                    if re.search(r"\.{2,}\s*\d+", line) or re.match(
                        r"^.*\d$", line
                    ):
                        if re.search(r"\.{2,}\s*\d+", line):
                            no_pn = False
                if re.search(r"\.{2,}\s*\d+", line):
                    no_pn = False
            if toc.find(class_=re.compile("header|page-header", re.I)):
                no_hf = False
            if toc.find(class_=re.compile("footer|page-footer", re.I)):
                no_hf = False
        t_ok = no_pn and no_hf
        add_result(
            "目录要求",
            t_ok,
            "目录符合无页码、无页眉页脚要求"
            if t_ok
            else "目录中存在页码或页眉页脚",
        )

    # ---- 5. 图表 / 附件（合法选择器）----
    appendix: Tag | None = None
    for sel in (
        "#appendix",
        ".appendix",
        ".attachment",
        '[id*="附件"]',
        '[class*="附件"]',
        '[class*="附表"]',
    ):
        hit = soup.select_one(sel)
        if hit and isinstance(hit, Tag):
            appendix = hit
            break

    illegal: list[Tag] = []
    for tbl in soup.find_all("table"):
        if isinstance(tbl, Tag) and not _is_under(tbl, appendix):
            illegal.append(tbl)
    for im in soup.find_all("img"):
        if isinstance(im, Tag) and not _is_under(im, appendix):
            illegal.append(im)
    for el in soup.find_all("figure"):
        if isinstance(el, Tag) and not _is_under(el, appendix):
            illegal.append(el)
    for el in soup.find_all(class_="chart"):
        if isinstance(el, Tag) and not _is_under(el, appendix) and el not in illegal:
            illegal.append(el)

    chart_text_valid = True
    if appendix:
        for el in appendix.select("table, td, th, figcaption, .chart-text"):
            if not isinstance(el, Tag):
                continue
            st = _el_style_dict(el)
            if not st.get("font-size"):
                continue
            fs = _length_to_px(st.get("font-size", ""))
            size_ok = abs(fs - _TARGET_FIG) <= 1.5
            fam = (st.get("font-family") or "").lower()
            font_ok = "宋体" in fam or "simsun" in fam
            c_raw = (st.get("color") or "").strip()
            if c_raw and c_raw.lower() not in ("inherit", "initial"):
                c_ok = _color_normalized(st) == "rgb(0, 0, 0)" or c_raw.lower() in (
                    "#000",
                    "#000000",
                    "black",
                    "rgb(0,0,0)",
                )
            else:
                c_ok = True
            if not (size_ok and font_ok and c_ok):
                chart_text_valid = False

    c_ok2 = len(illegal) == 0 and chart_text_valid
    add_result(
        "图表规范",
        c_ok2,
        "图表仅出现在附件/附表内，且图表文字符合五号宋体"
        if c_ok2
        else f"正文中发现{len(illegal)}个图表或附件内图表文字格式错误",
        illegal,
    )

    # ---- 6. 颜色与装饰 ----
    color_v: list[Tag] = []
    decor_v: list[Tag] = []
    for el in soup.find_all(True):
        if not isinstance(el, Tag):
            continue
        st = _el_style_dict(el)
        if not st.get("color") and not st.get("text-decoration") and not st.get(
            "border-bottom-style"
        ):
            continue
        col = (st.get("color") or "").strip().lower()
        if col and col not in (
            "inherit",
            "initial",
            "",
            "#000",
            "#000000",
            "black",
            "rgb(0,0,0)",
            "rgb(0, 0, 0)",
        ):
            if _color_normalized(st) and _color_normalized(st) != "rgb(0, 0, 0)":
                if el.get_text(strip=True):
                    color_v.append(el)
        tdec = (st.get("text-decoration") or "").lower()
        if "underline" in tdec and el.get_text(strip=True):
            decor_v.append(el)
        bbs = (st.get("border-bottom-style") or "").lower()
        if bbs in ("solid", "dotted") and el.get_text(strip=True):
            decor_v.append(el)
    col_ok = len(color_v) == 0 and len(decor_v) == 0
    add_result(
        "颜色与装饰",
        col_ok,
        "无彩色文字、无下划线、无着重号"
        if col_ok
        else f"发现{len(color_v)}处彩色文字，{len(decor_v)}处下划线/着重号",
        (color_v + decor_v)[:20],
    )

    # ---- 7. 页面 ----
    page_valid = True
    margin_top = margin_bottom = margin_left = margin_right = None
    page_info = _parse_page_margins_from_html(raw_html)
    # Word 常把 @page 写在 style 里，已在 raw_html 中解析
    if page_info and "shorthand" in page_info:
        # margin: 2.54cm 3.18cm
        parts = page_info["shorthand"].split()
        if len(parts) >= 4:
            margin_top, margin_right, margin_bottom, margin_left = (
                parts[0],
                parts[1],
                parts[2],
                parts[3],
            )
        elif len(parts) == 2:
            margin_top = margin_bottom = parts[0]
            margin_left = margin_right = parts[1]
    elif page_info:
        margin_top = page_info.get("top")
        margin_bottom = page_info.get("bottom")
        margin_left = page_info.get("left")
        margin_right = page_info.get("right")

    bst = _el_style_dict(body) if body else {}
    mraw = bst.get("margin", "")
    if mraw and not margin_top:
        margins = mraw.split()
        if len(margins) >= 1:
            margin_top = margins[0]
        if len(margins) >= 2:
            margin_right = margins[1]
        if len(margins) >= 3:
            margin_bottom = margins[2]
        if len(margins) >= 4:
            margin_left = margins[3]
        else:
            margin_left = margin_right

    if not margin_top and body:
        margin_top = _get_inline_property(body, "margin-top")
        margin_bottom = _get_inline_property(body, "margin-bottom")
        margin_left = _get_inline_property(body, "margin-left")
        margin_right = _get_inline_property(body, "margin-right")

    if not any([margin_top, margin_bottom, margin_left, margin_right]) and not page_info:
        page_valid = False

    def m_ok(
        m: str | None,
        target: float,
    ) -> bool:
        if not m:
            return False
        s = m.strip()
        if "cm" in s:
            return abs(_num(s) - target) < 0.01
        return abs(_num(s) - target) < 0.01

    top_ok = m_ok(margin_top, 2.54) or (
        (margin_top or "") in ("2.54cm", "1in")
    )
    bottom_ok = m_ok(margin_bottom, 2.54) or (
        (margin_bottom or "") in ("2.54cm", "1in")
    )
    left_ok = m_ok(margin_left, 3.18) or (margin_left or "").startswith("3.18")
    right_ok = m_ok(margin_right, 3.18) or (margin_right or "").startswith("3.18")

    html_tag = soup.find("html")
    w = _get_inline_property(html_tag, "width") if isinstance(html_tag, Tag) else ""  # type: ignore[arg-type]
    page_orientation = "横向" if w and w != "auto" and "%" not in w else "纵向"
    page_ok = bool(
        top_ok
        and bottom_ok
        and left_ok
        and right_ok
        and (page_orientation != "横向" or w in ("", "auto"))
    )
    if not margin_top:
        page_ok = False

    add_result(
        "页面设置",
        page_ok,
        "页面设置符合A4纵向/边距要求"
        if page_ok
        else "页面边距或纸张方向不符合要求",
    )

    return results
