"""JATS XML 解析器：将 PMC 全文 XML 解析为结构化段落、化学物、参考文献、表格、许可信息"""

import logging
import re

from lxml import etree

logger = logging.getLogger(__name__)


def parse_full_text(xml_str: str) -> dict | None:
    """解析 PMC JATS XML，返回结构化 dict。

    返回结构：
    {
        "sections": [{"heading": "Introduction", "text": "..."}, ...],
        "chemicals": [{"name": "Trastuzumab", "registry_number": "..."}],
        "refs": [{"pmid": 12345, "text": "..."}],
        "tables": [{"caption": "Table 1", "is_table1": true,
                     "is_outcome_table": false, "table_type": "baseline",
                     "_note": "自动识别仅供参考，请人工核对数据",
                     "headers": ["Characteristic", "Arm A"], "rows": [[...], ...],
                     "footnotes": ["..."]}],
        "license": {"href": "http://creativecommons.org/...", "type": "CC-BY", "text": "..."}
    }
    """
    if not xml_str or len(xml_str) < 500:
        return None

    try:
        root = etree.fromstring(xml_str.encode("utf-8"))
    except Exception:
        logger.warning("jats_parse_error", exc_info=True)
        return None

    # NCBI PMC efetch 返回 <pmc-articleset><article>...</article></pmc-articleset>
    # 展开一层使内部 XPath 能正确匹配 <body>/<sec> 等元素
    if root.tag in ("pmc-articleset", "pmc-articleset2"):
        article = root.find("article") or root.find("{http://www.w3.org/1999/xhtml}article")
        if article is not None:
            root = article

    ns = _detect_namespace(root)
    result: dict = {}

    sections = _extract_sections(root, ns)
    if sections:
        result["sections"] = sections

    chemicals = _extract_chemicals(root, ns)
    if chemicals:
        result["chemicals"] = chemicals

    refs = _extract_references(root, ns)
    if refs:
        result["refs"] = refs

    tables = _extract_tables(root, ns)
    if tables:
        result["tables"] = tables

    license_info = _extract_license(root, ns)
    if license_info:
        result["license"] = license_info

    return result if result else None


def extract_license_from_xml(xml_str: str) -> dict | None:
    """从原始 JATS XML 字符串中提取许可信息（独立入口）。"""
    if not xml_str or len(xml_str) < 500:
        return None
    try:
        root = etree.fromstring(xml_str.encode("utf-8"))
    except Exception:
        return None
    ns = _detect_namespace(root)
    return _extract_license(root, ns)


# ─── 命名空间 ───

def _detect_namespace(root: etree._Element) -> dict:
    tag = root.tag
    m = re.match(r"\{(.+?)\}", tag)
    if m:
        return {"j": m.group(1)}
    return {}


def _xpath(element: etree._Element, path: str, ns: dict) -> list:
    if ns:
        prefixed = path.replace("/", "/j:").replace("j:j:", "j:")
        if not prefixed.startswith("j:"):
            prefixed = "j:" + prefixed
        results = element.xpath(prefixed, namespaces=ns)
        if results:
            return results
    return element.xpath(path)


def _find_all_descendants(root: etree._Element, local_tag: str) -> list[etree._Element]:
    """查找所有后代元素（namespace-agnostic，按 local tag 匹配）。"""
    results = []
    for el in root.iter():
        if not isinstance(el.tag, str):
            continue
        tag = el.tag.split('}')[-1] if '}' in el.tag else el.tag
        if tag == local_tag:
            results.append(el)
    return results


def _extract_text(el: etree._Element | None) -> str:
    if el is None:
        return ""
    parts = [el.text or ""]
    for child in el:
        if child.tag in (
            etree.Comment,
            "{http://www.w3.org/1999/xhtml}disp-formula", "disp-formula",
            "{http://www.w3.org/1999/xhtml}table-wrap", "table-wrap",
            "{http://www.w3.org/1999/xhtml}fig", "fig",
        ):
            continue
        if child.tag in ("xref", "{http://www.w3.org/1999/xhtml}xref"):
            continue
        parts.append(_extract_text(child))
        parts.append(child.tail or "")
    parts.append(el.tail or "")
    return "".join(parts).strip()


def _extract_text_compact(el: etree._Element | None) -> str:
    """紧凑版：不递归子元素，只取直接文本。"""
    if el is None:
        return ""
    return "".join(el.itertext()).strip()


# ─── 段落 ───

def _extract_sections(root: etree._Element, ns: dict) -> list[dict] | None:
    body = _xpath(root, "body", ns)
    if not body:
        return None

    sections = []
    for sec in _xpath(body[0], "sec", ns):
        heading_el = _xpath(sec, "title", ns)
        heading = _extract_text(heading_el[0]) if heading_el else ""
        text = _extract_text(sec)

        sub_sections = []
        for sub in _xpath(sec, "sec", ns):
            sub_heading_el = _xpath(sub, "title", ns)
            sub_heading = _extract_text(sub_heading_el[0]) if sub_heading_el else ""
            sub_text = _extract_text(sub)
            if sub_text:
                sub_sections.append({"heading": sub_heading, "text": sub_text})

        if sub_sections:
            sections.append({"heading": heading, "subsections": sub_sections})
        elif text:
            sections.append({"heading": heading, "text": text})

    return sections if sections else None


# ─── 化学物 ───

def _extract_chemicals(root: etree._Element, ns: dict) -> list[dict] | None:
    chems = []
    for src in ("chemical", "chem-struct-wrap", "chem-struct"):
        for el in _find_all_descendants(root, src):
            name_el = _xpath(el, "name", ns) or _xpath(el, "named-content", ns)
            name = _extract_text(name_el[0]) if name_el else _extract_text(el)
            rn_el = _xpath(el, "registry-number", ns)
            rn = _extract_text(rn_el[0]) if rn_el else None
            if name and len(name) > 1:
                chems.append({"name": name, "registry_number": rn})

    seen = set()
    unique = []
    for c in chems:
        key = c["name"].lower().strip()
        if key not in seen:
            seen.add(key)
            unique.append(c)
    return unique if unique else None


# ─── 参考文献 ───

def _extract_references(root: etree._Element, ns: dict) -> list[dict] | None:
    ref_lists = _find_all_descendants(root, "ref-list")
    if not ref_lists:
        return None

    refs = []
    for ref in _xpath(ref_lists[0], "ref", ns):
        pmid = None
        for el in _xpath(ref, "pub-id", ns):
            if el.get("pub-id-type") == "pmid":
                try:
                    pmid = int(el.text.strip())
                except (ValueError, AttributeError):
                    pass
                break
        ref_text = _extract_text(ref)[:300]
        if ref_text:
            refs.append({"pmid": pmid, "text": ref_text})
    return refs if refs else None


# ═══════════════════════════════════════════
# 结构化表格解析
# ═══════════════════════════════════════════

def _extract_tables(root: etree._Element, ns: dict) -> list[dict] | None:
    """结构化表格解析：提取 thead/tbody/tr/td/th，处理 colspan/rowspan。"""
    tables = []
    for tw in _find_all_descendants(root, "table-wrap"):
        caption_el = _xpath(tw, "caption", ns)
        caption = _extract_text(caption_el[0]) if caption_el else ""

        # 提取脚注
        footnotes = []
        for fn in _xpath(tw, "table-wrap-foot/fn", ns):
            fn_text = _extract_text(fn)
            if fn_text:
                footnotes.append(fn_text)

        table_el = _xpath(tw, "table", ns)
        if not table_el:
            continue

        parsed = _parse_table_element(table_el[0], ns)
        if not parsed:
            continue

        classification = _classify_table(caption, parsed.get("headers", []), parsed.get("rows", []))

        tables.append({
            "caption": caption,
            "is_table1": classification["is_table1"],
            "is_outcome_table": classification["is_outcome_table"],
            "table_type": classification["table_type"],
            "_note": classification["_note"],
            "headers": parsed.get("headers", []),
            "rows": parsed.get("rows", []),
            "footnotes": footnotes,
        })

    return tables if tables else None


def _parse_table_element(table_el: etree._Element, ns: dict) -> dict | None:
    """解析 JATS <table> 元素（XHTML table 模型）。"""
    headers = []
    rows = []

    # JATS <table> 可能直接包含 <thead>/<tbody> 或 <tr>
    thead = _xpath(table_el, "thead", ns)
    if thead:
        for tr in _xpath(thead[0], "tr", ns):
            cells = _extract_row_cells(tr, ns, tag="th")
            if cells:
                # 多行 thead：拼接（如 "Characteristic" + "Treatment Arm" 各占一行）
                if not headers:
                    headers = cells
                else:
                    # 合并多行表头：上层留空时继承上层值
                    for i, cell in enumerate(cells):
                        if cell and i < len(headers):
                            headers[i] = f"{headers[i]} {cell}".strip()
                        elif cell:
                            headers.append(cell)
            break  # 通常只取第一行作为列名

    # 尝试从第一行 <tr> 提取表头（无 thead 时）
    if not headers:
        first_trs = _xpath(table_el, "tr", ns)
        if first_trs:
            cells = _extract_row_cells(first_trs[0], ns, tag="th")
            if cells:
                headers = cells

    # 提取表体
    tbody = _xpath(table_el, "tbody", ns)
    if tbody:
        for tr in _xpath(tbody[0], "tr", ns):
            cells = _extract_row_cells(tr, ns)
            if cells:
                rows.append(cells)
    else:
        # 无 tbody：直接取 <tr>，跳过已用作表头的第一行
        all_trs = _xpath(table_el, "tr", ns)
        start = 1 if headers else 0
        for tr in all_trs[start:]:
            cells = _extract_row_cells(tr, ns)
            if cells:
                rows.append(cells)

    # 即使无 rows 也返回 headers（空表）
    return {"headers": headers or [], "rows": rows or []}


def _extract_row_cells(tr: etree._Element, ns: dict, tag: str = "") -> list[str]:
    """提取一行中所有单元格文本。tag='th' 时只取 <th>，否则取 <td>。<th>。
    处理 colspan：重复单元格 N 次。
    处理 rowspan：简单取文本，不在本层展开。
    """
    cells = []
    cell_tag = tag if tag else ("th" if tag == "th" else "td|th")
    for cell in _xpath(tr, cell_tag if cell_tag != "th" else "th", ns):
        text = _extract_text_compact(cell).replace("\n", " ").replace("\r", "")
        # colspan 重复
        colspan = int(cell.get("colspan", 1))
        for _ in range(colspan):
            cells.append(text)
    return cells


# ─── Table 1 启发式识别 ───

_TABLE1_PATTERNS = {
    "caption": re.compile(
        r"table\s*(1|I)\b|"
        r"baseline\s*(characteristic|demographic)|"
        r"patient\s*(characteristic|demographic|feature)|"
        r"(characteristic|démographique|démographie|merkmale|kenngrößen)\s*(der\s+)?patient",
        re.IGNORECASE,
    ),
    "header_baseline": re.compile(
        r"characteristic|baseline|demographic|demographique|démographie|"
        r"clinical|patient|variable|parameter|feature|"
        r"merkmal|kenngröße|eigenschaft",
        re.IGNORECASE,
    ),
    "row_demographic": re.compile(
        r"age|sex|gender|race|ethnicity|ecog|stage|performance\s*status|"
        r"bmi|weight|height|smok|alcohol|prior\s+therapy|histolog|"
        r"metastasis|differentiation|node|tumor\s+size|subtype|grade|"
        r"älter|alter|geschlecht|rauchen|stadium",
        re.IGNORECASE,
    ),
}


def _detect_table1(caption: str, headers: list[str], rows: list[list[str]]) -> bool:
    """启发式判断是否为 Table 1（基线特征表）。

    规则：
    1. 标题含 "Table 1" / "Baseline Characteristics" 等
    2. 表头含 "Characteristic" / "Baseline" 等
    3. 首列含 "Age" / "Sex" / "Stage" 等人学/临床关键词
    4. 结构启发式：3-6 列、5-30 行、≥60% 首行为人学关键词
    命中 >= 2 条即判定为 Table 1；结构得分单独也判定。
    """
    score = 0

    # 规则 1：标题匹配
    if _TABLE1_PATTERNS["caption"].search(caption):
        score += 1

    # 规则 2：表头含基线列名
    header_text = " ".join(h.lower() for h in headers if h)
    if _TABLE1_PATTERNS["header_baseline"].search(header_text):
        score += 1

    # 规则 3：首列含人学/临床关键词
    first_col_items = [r[0].strip() if r else "" for r in rows[:20]]
    first_col_text = " ".join(item.lower() for item in first_col_items if item)
    row_demo_hits = sum(
        1 for item in first_col_items
        if _TABLE1_PATTERNS["row_demographic"].search(item.lower())
    )
    if _TABLE1_PATTERNS["row_demographic"].search(first_col_text):
        score += 1

    # 规则 4：结构启发式
    n_cols = len(headers)
    n_rows = len(rows)
    demo_density = row_demo_hits / max(len(first_col_items), 1)
    structural_score = 0
    if 3 <= n_cols <= 6:
        structural_score += 1
    if 5 <= n_rows <= 30:
        structural_score += 1
    if demo_density >= 0.6:
        structural_score += 1
    if structural_score >= 2:
        score += 1  # 结构强信号

    return score >= 2


# ─── Outcome 表（Table 2 / 疗效 / 生存）识别 ───

_OUTCOME_PATTERNS = {
    "caption": re.compile(
        r"table\s*(2|ii|3|iii|4|iv)\b|"              # Table 2/3/4
        r"(primary|secondary|co-primary)\s*endpoint|"  # 主要/次要终点
        r"efficacy|survival|response|"
        r"pfs|os|orr|dcr|ttf|tpp|"
        r"progression-free|overall\s*response|"
        r"hazard\s*ratio|outcome|"
        r"subgroup\s+analys|"
        r"multivariat",
        re.IGNORECASE,
    ),
    "header_outcome": re.compile(
        r"hr|hazard\s*ratio|odds\s*ratio|95%\s*ci|p\s*value|"
        r"response|efficacy|endpoint|event|"
        r"median|survival|progression",
        re.IGNORECASE,
    ),
    "row_stat": re.compile(
        r"pfs|os|orr|dcr|ttf|tpp|"
        r"progression-free|overall\s*survival|response\s*rate|"
        r"hazard\s*ratio|mortality|survival",
        re.IGNORECASE,
    ),
}


def _detect_outcome_table(caption: str, headers: list[str], rows: list[list[str]]) -> bool:
    """启发式判断是否为 Outcome 表（疗效/生存/终点）。

    规则：
    1. 标题含 "Table 2/3/4" / "efficacy"/"survival" / 终点缩写
    2. 表头含 HR / 95% CI / p value 等
    3. 首列含 PFS / OS / ORR 等终点关键词
    命中 >= 2 条即判定。与 Table 1 互斥（Outcome 优先判断）。
    """
    score = 0

    if _OUTCOME_PATTERNS["caption"].search(caption):
        score += 1

    header_text = " ".join(h.lower() for h in headers if h)
    if _OUTCOME_PATTERNS["header_outcome"].search(header_text):
        score += 1

    first_col_items = [r[0].strip() if r else "" for r in rows[:15]]
    first_col_text = " ".join(item.lower() for item in first_col_items if item)
    if _OUTCOME_PATTERNS["row_stat"].search(first_col_text):
        score += 1

    return score >= 2


def _classify_table(caption: str, headers: list[str], rows: list[list[str]]) -> dict:
    """对表格做综合分类，返回 {is_table1, is_outcome_table, table_type}。

    table_type: "baseline" / "outcome" / "safety" / "other"
    _note: 自动识别仅供参考查阅。
    """
    if _detect_outcome_table(caption, headers, rows):
        return {
            "is_table1": False,
            "is_outcome_table": True,
            "table_type": "outcome",
            "_note": "自动识别仅供参考，请人工核对数据",
        }

    is_table1 = _detect_table1(caption, headers, rows)
    return {
        "is_table1": is_table1,
        "is_outcome_table": False,
        "table_type": "baseline" if is_table1 else "other",
        "_note": "自动识别仅供参考，请人工核对数据" if is_table1 else "",
    }


# ═══════════════════════════════════════════
# 许可信息提取
# ═══════════════════════════════════════════

_LICENSE_TYPE_MAP = {
    "creativecommons.org/licenses/by/4.0": "CC-BY-4.0",
    "creativecommons.org/licenses/by-nc/4.0": "CC-BY-NC-4.0",
    "creativecommons.org/licenses/by/3.0": "CC-BY-3.0",
    "creativecommons.org/licenses/by-nc/3.0": "CC-BY-NC-3.0",
    "creativecommons.org/licenses/by/2.0": "CC-BY-2.0",
    "creativecommons.org/licenses/by/2.5": "CC-BY-2.5",
    "creativecommons.org/licenses/by-nc-nd": "CC-BY-NC-ND",
    "creativecommons.org/licenses/by-sa": "CC-BY-SA",
    "creativecommons.org/publicdomain/zero": "CC0",
    "creativecommons.org/publicdomain/mark": "PDM",
    "creativecommons.org/licenses/by/": "CC-BY",
    "creativecommons.org/licenses/by-nc/": "CC-BY-NC",
    "creativecommons.org/licenses/by-nc-nd/": "CC-BY-NC-ND",
    "creativecommons.org/licenses/by-sa/": "CC-BY-SA",
}


def _extract_license(root: etree._Element, ns: dict) -> dict | None:
    """从 JATS <article-meta>/<permissions>/<license> 提取许可信息。"""
    front = _xpath(root, "front", ns)
    if not front:
        return None
    article_meta = _xpath(front[0], "article-meta", ns)
    if not article_meta:
        return None

    permissions = _xpath(article_meta[0], "permissions", ns)
    if not permissions:
        return None

    for lic in _xpath(permissions[0], "license", ns):
        href = lic.get("{http://www.w3.org/1999/xlink}href") or lic.get("xlink:href") or ""
        lic_text = " ".join(
            _extract_text_compact(p) for p in _xpath(lic, "license-p", ns)
        ).strip()

        # 从 href 推断许可类型
        lic_type = None
        if href:
            href_lower = href.lower()
            for pattern, ctype in _LICENSE_TYPE_MAP.items():
                if pattern in href_lower:
                    lic_type = ctype
                    break

        # 从文字推断
        if not lic_type and lic_text:
            text_lower = lic_text.lower()
            if "cc by" in text_lower or "creative commons" in text_lower:
                if "noncommercial" in text_lower or "non-commercial" in text_lower:
                    lic_type = "CC-BY-NC"
                elif "sharealike" in text_lower or "share alike" in text_lower:
                    lic_type = "CC-BY-SA"
                else:
                    lic_type = "CC-BY"

        return {"href": href, "type": lic_type, "text": lic_text[:500] if lic_text else ""}

    return None


# ═══════════════════════════════════════════
# Table 1 预抽取（样本量 + 基线特征）
# ═══════════════════════════════════════════

# Row label keywords for Table 1 field classification.
# Ordered by specificity — put longer/more-specific patterns first so they
# match before broad substrings in the same-or-next row.
_TABLE1_KEYWORDS: dict[str, set[str]] = {
    "sample_size": {"no. of patients", "number of patients", "n =", "n=", "patients (n)"},
    "age": {"age", "年"},
    "female": {"female", "woman", "women", "sex.*f", "女性"},
    "male": {"male", "men", "男性"},
    "ecog": {"ecog", "performance status"},
    "stage": {"stage", "分期"},
    "bmi": {"bmi", "body mass index"},
    "smoking": {"smok", "cigarette"},
    "alcohol": {"alcohol", "drink"},
    "prior_therapy": {"prior therapy", "previous treatment", "prior treatment", "pretreatment"},
    "histology": {"histolog", "differentiation"},
    "subtype": {"subtype", "molecular", "biomarker"},
    "race": {"race", "ethnic"},
    "surgery": {"surgery", "resection"},
    "comorbidity": {"comorbid", "charlson", "cci"},
}

# Labels that indicate this row is NOT a data field (e.g., footnotes within row data)
_TABLE1_SKIP_LABELS: set[str] = {
    "note:", "footnote", "abbreviation:", "abbrev:", "standard deviation",
}


def extract_baseline_data(full_text_sections: dict) -> dict | None:
    """从结构化全文数据中预抽取基线特征（样本量、人学、分期等）。

    优先从 Table 1 结构化表格中提取，次优从段落文本中正则匹配。

    返回：
    {
        "table1_caption": str | None,
        "arms": [str, ...],           # 从表头提取的研究组名
        "extracted_fields": {
            "sample_size": {"ArmA": "100", "ArmB": "98"},
            "age": {"ArmA": "65.2 ± 3.4"},
            ...
        },
        "parsed_values": {...},       # 数值解析后的结构化数据
        "header_sample_sizes": {...}, # 表头中解析的样本量
        "outcome_tables": [           # 识别到的终点/疗效表元数据
            {"caption": "Table 2. Efficacy Outcomes",
             "headers": [...], "table_type": "outcome",
             "_note": "自动识别仅供参考，请人工核对数据"}
        ],
        "_note": "预提取数据仅供参考，请人工核对原始表格"  # 无 Table 1 时
    }
    """
    tables = full_text_sections.get("tables") or []
    sections = full_text_sections.get("sections") or []

    result: dict = {"table1_caption": None, "arms": [], "extracted_fields": {}, "parsed_values": {}}

    # === 优先从 Table 1 结构化表格中提取 ===
    table1 = None
    for tbl in tables:
        if tbl.get("is_table1"):
            table1 = tbl
            break

    if table1:
        result["table1_caption"] = table1.get("caption")
        headers = table1.get("headers", [])
        rows = table1.get("rows", [])

        # 表头中提取研究组名（跳过 "Characteristic" 类列）
        for h in headers:
            if h and not _is_label_col(h):
                result["arms"].append(h)

        # 从表头文本中解析样本量（"Arm A (n=100)" → sample_size）
        header_sample_sizes: dict[str, dict] = {}
        for arm in result["arms"]:
            parsed_n = _parse_sample_size(arm)
            if parsed_n:
                header_sample_sizes[arm] = parsed_n
        if header_sample_sizes:
            result["header_sample_sizes"] = header_sample_sizes

        # 从各行中提取基线字段（关键词匹配行标签）
        row_fields: dict[str, list[str]] = {}
        row_fields_parsed: dict[str, list[dict | None]] = {}  # 解析后数值
        for row in rows:
            if not row:
                continue
            label = row[0].strip().lower() if row[0] else ""
            values = row[1:] if len(row) > 1 else []

            # 跳过脚注行
            if any(s in label for s in _TABLE1_SKIP_LABELS):
                continue

            for field_key, keywords in _TABLE1_KEYWORDS.items():
                if _match_any_keyword(label, keywords):
                    # 防止同一 field_key 被多次覆盖（取第一个匹配行）
                    if field_key not in row_fields:
                        row_fields[field_key] = values
                        row_fields_parsed[field_key] = [
                            _parse_field_for_key(field_key, v) for v in values
                        ]
                    break

        if row_fields:
            result["extracted_fields"] = {
                key: {
                    result["arms"][i] if i < len(result["arms"]) else f"col{i+1}": v
                    for i, v in enumerate(vals)
                } if vals else {}
                for key, vals in row_fields.items()
            }

        if row_fields_parsed:
            result["parsed_values"] = {
                key: {
                    result["arms"][i] if i < len(result["arms"]) else f"col{i+1}": pv
                    for i, pv in enumerate(vals)
                } if vals else {}
                for key, vals in row_fields_parsed.items()
            }

    # === 收集 Outcome 表元数据（Table 2/3/4 的 caption + 列名，不解析数值） ===
    outcome_tables = []
    for tbl in tables:
        if tbl.get("is_outcome_table"):
            outcome_tables.append({
                "caption": tbl.get("caption", ""),
                "headers": tbl.get("headers", []),
                "table_type": "outcome",
                "_note": "自动识别仅供参考，请人工核对数据",
            })
    if outcome_tables:
        result["outcome_tables"] = outcome_tables

    # === 从段落文本中正则补充（无 Table 1 时） ===
    if not table1 and sections:
        all_text = " ".join(
            (s.get("text") or "") for s in sections
        )
        _SECTION_PATTERNS: dict[str, re.Pattern] = {
            "sample_size": re.compile(r"(?:total\s*)?[Nn]\s*[=:≈~]\s*(\d[\d,.]*)"),
            "age": re.compile(r"(?:mean|median)\s*age\s*[=:≈~]?\s*([\d.]+)", re.IGNORECASE),
            "female": re.compile(r"(?:female|women?)\s*[=:≈~]?\s*(\d+[\s.,]*\d*)%?", re.IGNORECASE),
        }
        extracted: dict[str, dict[str, str]] = {}
        for field_key, pat in _SECTION_PATTERNS.items():
            m = pat.search(all_text)
            if m:
                extracted[field_key] = {"text": m.group(0), "value": m.group(1)}
        if extracted:
            result["extracted_fields"] = extracted
            result["_note"] = "段落正则提取，请人工核对原始文献"

    if result["extracted_fields"]:
        return result
    return None


def _match_any_keyword(label: str, keywords: set[str]) -> bool:
    """检查 label 是否匹配任一关键词（支持 regex）。

    优先用 re.search（捕获 "sex.*f" → "sex: female"），降级为 substring 检查定长词。
    """
    for kw in keywords:
        try:
            if re.search(kw, label):
                return True
        except re.error:
            if kw in label:
                return True
    return False


def _is_label_col(header: str) -> bool:
    """判断表头列名是否为行标签列（如 "Characteristic"、"Variable"）。"""
    label_words = {
        "characteristic", "variable", "parameter", "feature", "trait",
        "demographic", "baseline", "item", "measure", "指标", "变量", "特征",
    }
    h_lower = header.strip().lower()
    return any(w in h_lower for w in label_words)


# ═══════════════════════════════════════════
# 基线字段数值解析
# ═══════════════════════════════════════════

# Age 格式：mean±sd / mean(sd) / median (IQR) / median (range) / range
_AGE_PARSERS: list[tuple[re.Pattern, str]] = [
    (re.compile(r"([\d.]+)\s*[±±]\s*([\d.]+)"), "mean_sd"),
    (re.compile(r"([\d.]+)\s*\(([\d.]+)\)"), "mean_sd"),  # 58.3 (12.1) — 优先假设为 mean(SD)
    (re.compile(r"([\d.]+),\s*SD\s*([\d.]+)", re.IGNORECASE), "mean_sd"),
    (re.compile(r"median\s+([\d.]+)\s*\(IQR\s*([\d.-]+)\s*[-–]\s*([\d.]+)\)", re.IGNORECASE), "median_iqr"),
    (re.compile(r"median\s+([\d.]+)\s*\(([\d.-]+)\s*[-–]\s*([\d.]+)\)", re.IGNORECASE), "median_range"),
    (re.compile(r"^([\d.]+)\s*[-–]\s*([\d.]+)$"), "range"),
]

# Sex / 百分比格式
_PCT_PARSERS: list[tuple[re.Pattern, str]] = [
    (re.compile(r"(\d+)\s*/\s*(\d+)\s*\(?\s*([\d.]+)%?"), "frac_pct"),      # 45/80 (56.2%) — 优先匹配
    (re.compile(r"(\d+)\s*\(([\d.]+)%\)"), "n_pct"),                          # 45 (56.2%)
    (re.compile(r"([\d.]+)%"), "only_pct"),                                    # 56.2%
    (re.compile(r"^(\d+)$"), "only_n"),                                        # 45
]


def _parse_age_value(raw: str) -> dict | None:
    """尝试解析 Age 列值为结构化数据。"""
    text = raw.strip()
    for pat, fmt in _AGE_PARSERS:
        m = pat.search(text)
        if m:
            result: dict = {"format": fmt, "raw": text}
            groups = [g for g in m.groups() if g is not None]
            if fmt == "mean_sd" and len(groups) >= 2:
                try:
                    result["mean"] = float(groups[0])
                    result["sd"] = float(groups[1])
                except ValueError:
                    continue
            elif fmt in ("median_iqr", "median_range") and len(groups) >= 3:
                try:
                    result["median"] = float(groups[0])
                    result["q1"] = float(groups[1])
                    result["q3"] = float(groups[2])
                except ValueError:
                    continue
            elif fmt == "range" and len(groups) >= 2:
                try:
                    result["min"] = float(groups[0])
                    result["max"] = float(groups[1])
                except ValueError:
                    continue
            return result
    # fallback: 尝试直接提取一个数字
    num_m = re.search(r"([\d.]+)", text)
    if num_m:
        try:
            return {"format": "unstructured", "value": float(num_m.group(1)), "raw": text}
        except ValueError:
            pass
    return None


def _parse_pct_value(raw: str) -> dict | None:
    """尝试解析性别/分类变量的百分比值。"""
    text = raw.strip()
    for pat, fmt in _PCT_PARSERS:
        m = pat.search(text)
        if m:
            result: dict = {"format": fmt, "raw": text}
            groups = [g for g in m.groups() if g is not None]
            if fmt == "n_pct" and len(groups) >= 2:
                try:
                    result["n"] = int(groups[0])
                    result["pct"] = float(groups[1])
                except ValueError:
                    continue
            elif fmt == "frac_pct" and len(groups) >= 3:
                try:
                    result["n"] = int(groups[0])
                    result["total"] = int(groups[1])
                    result["pct"] = float(groups[2])
                except ValueError:
                    continue
            elif fmt == "only_pct":
                try:
                    result["pct"] = float(groups[0])
                except ValueError:
                    continue
            elif fmt == "only_n":
                try:
                    result["n"] = int(groups[0])
                except ValueError:
                    continue
            return result
    return None


def _parse_sample_size(raw: str) -> dict | None:
    """尝试从列标题行文本解析样本量（如 "Arm A (n=100)"）。"""
    m = re.search(r"[Nn]\s*[=:≈~]\s*(\d[\d,.]*)", raw)
    if m:
        try:
            return {"n": int(m.group(1).replace(",", "")), "raw": raw.strip()}
        except ValueError:
            pass
    return None


# ═══════════════════════════════════════════
# Outcome 表效应量数值解析 (v1.2)
# ═══════════════════════════════════════════

_ES_REGEXES: list[tuple[re.Pattern, str]] = [
    # HR/MD/OR/RR with explicit label + 95%CI + optional p
    (re.compile(
        r"(HR|OR|RR|MD|SMD|RD)\s*[=:≈~]?\s*"
        r"([\d.]+)\s*"
        r"(?:\(?95%\s*C?I?\s*)?"
        r"([\d.]+)\s*[-–]\s*([\d.]+)\)?"
        r"(?:[,;]?\s*(?:[Pp]\s*[=≥≤<>])\s*([\d.]+))?",
        re.IGNORECASE,
    ), "labeled"),
    # HR/MD/OR/RR with CI in parens (no 95% CI text): "HR 1.23 (1.01-1.45)"
    (re.compile(
        r"(HR|OR|RR|MD|SMD|RD)\s*[=:≈~]?\s*"
        r"([\d.]+)\s*\(([\d.]+)\s*[-–]\s*([\d.]+)\)"
        r"(?:[,;]?\s*(?:[Pp]\s*[=≥≤<>])\s*([\d.]+))?",
        re.IGNORECASE,
    ), "labeled_paren"),
    # Bare: "0.82 (95%CI 0.70–0.97)" — in outcome table context, assume HR
    (re.compile(
        r"([\d.]+)\s*"
        r"\(?95%\s*C?I?\s*"
        r"([\d.]+)\s*[-–]\s*([\d.]+)\)?"
        r"(?:[,;]?\s*(?:[Pp]\s*[=≥≤<>])\s*([\d.]+))?",
        re.IGNORECASE,
    ), "bare_ci"),
    # Bare paren: "0.71 (0.59-0.85)" — no CI label
    (re.compile(
        r"([\d.]+)\s*\(([\d.]+)\s*[-–]\s*([\d.]+)\)"
    ), "bare_paren"),
    # P-value alone: "p=0.03" or "P <0.001"
    (re.compile(r"[Pp]\s*([=≥≤<>])\s*([\d.]+)"), "p_only"),
]


def parse_outcome_table_effects(caption: str, headers: list[str], rows: list[list[str]]) -> list[dict]:
    """从 Outcome 表的行数据中提取效应量（HR/OR/MD + 95%CI + p）。

    逐行匹配数值模式，返回结构化数据列表。

    返回：
    [
        {
            "endpoint": "Overall Survival",  # 第一列 = 终点名
            "effect_type": "HR",
            "value": 0.82,
            "ci_lower": 0.70,
            "ci_upper": 0.97,
            "p_value": 0.03,
            "raw": "HR 0.82 (95% CI 0.70-0.97, p=0.03)",
        },
        ...
    ]
    """
    results: list[dict] = []
    for row in rows:
        if not row:
            continue
        endpoint = (row[0] or "").strip()
        if not endpoint:
            continue

        # Skip header-like rows and notes
        if endpoint.lower() in (
            "", "endpoint", "outcome", "subgroup", "analysis",
            "note:", "abbreviation:", "p-value",
        ):
            continue
        if len(endpoint) > 120:
            continue  # unlikely to be an endpoint name

        # 合并后面的值列为一个文本，逐列尝试匹配
        value_text = " ".join(v.strip() for v in row[1:] if v and v.strip())

        if not value_text:
            continue

        best: dict | None = None
        for pat, mode in _ES_REGEXES:
            m = pat.search(value_text)
            if not m:
                continue
            groups = [g for g in m.groups() if g is not None]

            try:
                if mode == "labeled":
                    effect = m.group(1).upper()
                    val = float(groups[1])
                    lo = float(groups[2])
                    hi = float(groups[3])
                    pv = float(groups[4]) if len(groups) >= 5 else None
                    best = {
                        "endpoint": endpoint,
                        "effect_type": effect,
                        "value": val,
                        "ci_lower": lo,
                        "ci_upper": hi,
                        "p_value": pv,
                        "raw": value_text[:200],
                    }
                elif mode == "labeled_paren":
                    effect = m.group(1).upper()
                    val = float(groups[1])
                    lo = float(groups[2])
                    hi = float(groups[3])
                    pv = float(groups[4]) if len(groups) >= 5 else None
                    best = {
                        "endpoint": endpoint,
                        "effect_type": effect,
                        "value": val,
                        "ci_lower": lo,
                        "ci_upper": hi,
                        "p_value": pv,
                        "raw": value_text[:200],
                    }
                elif mode == "bare_ci":
                    val = float(groups[0])
                    lo = float(groups[1])
                    hi = float(groups[2])
                    pv = float(groups[3]) if len(groups) >= 4 else None
                    best = {
                        "endpoint": endpoint,
                        "effect_type": None,  # unknown — in outcome table context likely HR
                        "value": val,
                        "ci_lower": lo,
                        "ci_upper": hi,
                        "p_value": pv,
                        "raw": value_text[:200],
                    }
                elif mode == "bare_paren":
                    val = float(groups[0])
                    lo = float(groups[1])
                    hi = float(groups[2])
                    best = {
                        "endpoint": endpoint,
                        "effect_type": None,
                        "value": val,
                        "ci_lower": lo,
                        "ci_upper": hi,
                        "p_value": None,
                        "raw": value_text[:200],
                    }
                elif mode == "p_only":
                    # Only p-value found, no effect size
                    _sign = m.group(1)
                    pv = float(groups[1])
                    best = {
                        "endpoint": endpoint,
                        "effect_type": None,
                        "value": None,
                        "ci_lower": None,
                        "ci_upper": None,
                        "p_value": pv,
                        "raw": value_text[:200],
                    }
            except (ValueError, IndexError, TypeError):
                continue

            if best:
                break  # first matching pattern wins

        if best:
            results.append(best)

    return results


def parse_all_outcome_effects(full_text_sections: dict) -> list[dict]:
    """从所有 Outcome 表中提取效应量。

    包装函数，遍历 full_text_sections 中的所有 outcome table
    并调用 parse_outcome_table_effects()。
    """
    tables = full_text_sections.get("tables") or []
    all_effects: list[dict] = []
    for tbl in tables:
        if tbl.get("is_outcome_table"):
            effects = parse_outcome_table_effects(
                caption=tbl.get("caption", ""),
                headers=tbl.get("headers", []),
                rows=tbl.get("rows", []),
            )
            all_effects.extend(effects)
    return all_effects


def _parse_field_for_key(field_key: str, raw: str) -> dict | None:
    """根据字段类型选择解析器。"""
    if field_key in ("age",):
        return _parse_age_value(raw)
    if field_key in ("female", "male", "smoking", "alcohol", "race"):
        return _parse_pct_value(raw)
    if field_key == "sample_size":
        return _parse_sample_size(raw) or _parse_pct_value(raw)
    return None
