# -*- coding: utf-8 -*-
"""
需求提取器模块 - LLM增强版
使用阿里云千问大模型智能提取和分类需求
"""

import re
import json
import logging
from difflib import SequenceMatcher
from typing import List, Dict, Optional, Tuple, Any
from .document_parser import Section
from .settings import AppSettings
from .requirement_id_generator import RequirementIDGenerator
from .requirement_splitter import RequirementSplitter

logger = logging.getLogger(__name__)


class Requirement:
    """表示一个需求项"""
    
    def __init__(self, req_id: str, description: str, req_type: str = "functional",
                 section_number: str = "", section_title: str = "",
                 interface_name: str = "", interface_type: str = "",
                 section_uid: str = "",
                 source: str = "", destination: str = "",
                 source_type: str = "text", source_order: int = 0,
                 source_table_index: int = -1, source_row_span: str = ""):
        self.id = req_id
        self.description = description
        self.type = req_type
        self.section_number = section_number
        self.section_title = section_title
        self.section_uid = section_uid
        # 接口需求特有字段
        self.interface_name = interface_name
        self.interface_type = interface_type
        self.source = source
        self.destination = destination
        self.source_type = source_type
        self.source_order = source_order
        self.source_table_index = source_table_index
        self.source_row_span = source_row_span
    
    def to_dict(self) -> Dict:
        result = {
            "需求编号": self.id,
            "需求描述": self.description
        }
        # 接口需求增加额外字段
        if self.type == 'interface':
            result["接口名称"] = self.interface_name
            result["接口类型"] = self.interface_type
            result["数据来源"] = self.source
            result["数据目的地"] = self.destination
        return result
    
    def __repr__(self) -> str:
        return f"Requirement(id='{self.id}', type='{self.type}')"


class RequirementExtractor:
    """需求提取器 - LLM增强版"""
    
    def __init__(self, config: Dict = None, llm=None):
        self.config = config or {}
        self.llm = llm
        self.settings = AppSettings(self.config)
        self.id_generator = RequirementIDGenerator(self.settings.type_prefix)
        self.splitter = None
        if self.settings.splitter_enabled:
            self.splitter = RequirementSplitter(
                max_sentence_len=self.settings.splitter_max_sentence_len,
                min_clause_len=self.settings.splitter_min_clause_len,
            )
        self.requirements: List[Requirement] = []
        self._req_counters: Dict[str, Dict[str, int]] = {}  # {section_number: {type: count}}
        self._global_order = 0
    
    def extract_from_sections(self, sections: List[Section]) -> List[Requirement]:
        """
        从章节列表中提取需求
        
        Args:
            sections: 解析后的章节列表
            
        Returns:
            需求列表
        """
        self.requirements = []
        self._req_counters = {}
        self._global_order = 0
        
        for section in sections:
            self._process_section(section)

        if self.settings.enable_cross_section_dedup:
            self.requirements = self._global_deduplicate_requirements(self.requirements)

        # 去重后统一连续重编号，避免出现跳号。
        if self.settings.renumber_enabled:
            self.requirements = self._renumber_requirements_continuous(self.requirements)
        
        logger.info(f"共提取 {len(self.requirements)} 个需求项")
        return self.requirements
    
    def _process_section(self, section: Section, depth: int = 0) -> None:
        """递归处理章节，提取需求"""
        # 检查是否应该跳过此章节
        if self._should_skip_section(section):
            logger.debug(f"跳过非需求章节: {section.number} {section.title}")
            for child in section.children:
                self._process_section(child, depth + 1)
            return
        
        # 先提取当前章节需求（包含表格）
        reqs = self._extract_requirements_from_section(section)
        self.requirements.extend(reqs)
        
        # 再递归处理子章节
        for child in section.children:
            self._process_section(child, depth + 1)
    
    def _should_skip_section(self, section: Section) -> bool:
        """判断是否应该跳过此章节"""
        if self.settings.is_non_requirement_section(section.title):
            return True

        if self._is_diagram_or_overview_section(section.title):
            return True
        
        # 检查是否是系统描述章节（如3.1.1通常是系统描述）
        if self._is_system_description(section):
            return True
        
        return False

    def _is_diagram_or_overview_section(self, title: str) -> bool:
        t = (title or "").strip()
        if not t:
            return False
        # 示意图/概述章节通常不承载可验证需求。
        if any(k in t for k in ["示意图", "概述"]):
            if "需求" not in t and "要求" not in t:
                return True
            # 即使包含“需求/要求”，若明确是示意图也仍跳过。
            if "示意图" in t:
                return True
        return False
    
    def _is_system_description(self, section: Section) -> bool:
        """判断是否是系统描述章节（应该跳过）"""
        title = section.title or ""

        # 明确的需求语义章节优先提取，避免误判导致漏提。
        if self._is_requirement_semantic_section(title):
            return False

        # 检查标题
        desc_keywords = self.settings.system_description_hints
        for kw in desc_keywords:
            if kw in title:
                return True
        
        if not self.llm:
            raise RuntimeError("LLM实例未初始化，当前版本仅支持LLM提取")

        if section.content:
            try:
                result = self._llm_check_system_description(section)
                return result
            except Exception as e:
                raise RuntimeError(f"LLM系统描述判断失败: section={section.number} {section.title}, error={e}") from e

        return False
    
    def _llm_check_system_description(self, section: Section) -> bool:
        """使用LLM判断是否是系统描述"""
        prompt = f"""请判断以下章节是否是对系统的整体描述（而不是具体的功能需求）。

章节编号：{section.number}
章节标题：{section.title}
章节内容（前500字符）：
{section.content[:500] if section.content else '无'}

请只回答"是"或"否"：
- "是"：这是系统整体描述、功能模块组成介绍、系统架构说明等概述性内容
- "否"：这是具体的功能需求、接口需求、性能要求等可提取的需求内容

回答（只需要回答"是"或"否"）："""
        
        response = self.llm.call(prompt).strip()
        return response.startswith("是")

    def _is_requirement_semantic_section(self, title: str) -> bool:
        """章节标题是否具有明确需求语义。"""
        return (
            self.settings.is_interface_semantic_title(title)
            or self.settings.is_functional_semantic_title(title)
            or self.settings.is_other_semantic_title(title)
            or ("需求" in (title or ""))
            or ("要求" in (title or ""))
        )

    def _should_extract_text_block(self, section: Section, text: str, req_type: str) -> bool:
        """判断文本块是否值得提取需求。"""
        cleaned = (text or "").strip()
        if len(cleaned) < 8:
            return False

        if self._is_requirement_semantic_section(section.title):
            return True

        hard_requirement_hints = ["应", "必须", "需", "shall", "should", "不得", "支持"]
        if any(h in cleaned for h in hard_requirement_hints):
            return True

        prompt = f"""请判断以下文本是否包含具体、可验证的软件需求。

章节标题：{section.title}
需求类型：{req_type}
文本内容：
{cleaned[:700]}

判定规则：
- 回答“是”：包含可执行、可测试、可验收的具体要求。
- 回答“否”：仅为概括、背景、系统组成说明、引言或泛化描述。

回答仅输出“是”或“否”。"""

        response = self.llm.call(prompt).strip()
        return response.startswith("是")
    
    def _extract_requirements_from_section(self, section: Section) -> List[Requirement]:
        """从单个章节按文档顺序提取需求。"""
        requirements: List[Requirement] = []
        if not self.llm:
            raise RuntimeError("LLM实例未初始化，当前版本仅支持LLM提取")

        req_type = self._identify_requirement_type(section.title, section.content)
        is_environment_section = self._is_environment_section(section.title)

        if self._should_suppress_section_requirements(section, req_type):
            return []

        blocks = self._iter_section_blocks(section)
        for block in blocks:
            block_type = block.get("type", "text")

            temp_section = Section(
                level=section.level,
                title=section.title,
                number=section.number,
                content="",
                uid=section.uid,
            )

            if block_type == "text":
                # 环境类章节以表格为准，一表一条，正文不单独拆多条。
                if is_environment_section:
                    continue
                temp_section.content = self._sanitize_block_text(block.get("text", ""), section.number)
                if len(temp_section.content.strip()) < 8:
                    continue
                if not self._should_extract_text_block(temp_section, temp_section.content, req_type):
                    continue
                block_reqs = self._llm_extract_requirements(temp_section, req_type, source_type="text")
                table_index = -1
            else:
                table_data = block.get("table", [])
                temp_section.tables = [table_data] if table_data else []
                table_index = int(block.get("table_index", -1))

                # 安全/保密/适应性章节若误挂接口风格表格，直接忽略。
                if self._should_skip_table_for_section(section.title, table_data, req_type):
                    continue

                if is_environment_section:
                    table_mode = "single"
                else:
                    table_mode = self._classify_table_mode(section.title, table_data, req_type)
                if table_mode == "skip":
                    continue
                if table_mode == "single":
                    block_reqs = self._llm_extract_table_as_single_requirement(temp_section, req_type)
                else:
                    interface_rows = self._extract_interface_rows_from_table(table_data) if table_mode == "interface" else []
                    if self.llm and self.settings.table_llm_semantic_enabled:
                        block_reqs = self._llm_extract_table_requirements(temp_section, req_type, interface_rows=interface_rows)
                    else:
                        block_reqs = self._llm_extract_requirements(temp_section, req_type, source_type="table")

            for req in block_reqs:
                self._global_order += 1
                req.source_type = block_type
                req.source_order = self._global_order
                req.source_table_index = table_index
                req.source_row_span = block.get("row_span", "")
                req.description = self._maybe_light_rewrite(req.description, block_type, req.type)
                req.description = self._clean_description(req.description)
                if self._is_low_quality_requirement(req.description, req.type):
                    continue
                if req.type != 'interface' and block_type == 'text':
                    req.description = self._snap_to_source_sentence(req.description, temp_section.content)
                    if self._is_low_quality_requirement(req.description, req.type):
                        continue
                if req.type == 'interface':
                    if self._is_generic_reference_requirement(req.description):
                        continue
                    req.interface_name = self._normalize_interface_field(req.interface_name)
                    req.interface_type = self._normalize_interface_field(req.interface_type)
                    req.source = self._normalize_interface_field(req.source)
                    req.destination = self._normalize_interface_field(req.destination)
                requirements.append(req)

        requirements = self._semantic_integrity_postprocess(requirements)
        requirements = self._merge_fragment_requirements(requirements)
        requirements = self._deduplicate_requirements(requirements)
        requirements = self._drop_inferior_interface_duplicates(requirements)
        return requirements

    def _is_environment_section(self, title: str) -> bool:
        t = (title or "").strip()
        return any(k in t for k in ["硬件环境", "软件环境", "运行环境", "计算机硬件", "计算机软件"])

    def _should_skip_table_for_section(self, section_title: str, table: List[List[str]], req_type: str) -> bool:
        if not table:
            return False
        title = (section_title or "")
        header_text = " ".join(str(c or "") for c in table[0]).lower() if table else ""

        # 安全/保密/适应性章节中出现接口风格表头，通常是误挂表。
        if any(k in title for k in ["安全性需求", "保密性需求", "适应性需求"]):
            interface_style_hints = ["来源", "目的地", "标识", "数据消息", "接口"]
            if any(h in header_text for h in interface_style_hints):
                return True

        return False

    def _is_low_quality_requirement(self, description: str, req_type: str) -> bool:
        text = (description or "").strip()
        if len(text) < 6:
            return True

        normalized = re.sub(r"\s+", "", text)
        if "无。" in normalized and "否。" in normalized:
            return True
        bad_markers = ["无。", "否。", "是。", "无，", "否，"]
        if any(m in normalized for m in bad_markers):
            action_hints = ["应", "必须", "需", "支持", "通过", "提供", "实现", "具备", "监测", "控制", "接口"]
            if not any(h in normalized for h in action_hints):
                return True

        # 类似“4。xxx。MOD_XXX。否。”的噪声拼接句。
        if re.match(r"^\d+[。．、]", normalized):
            if normalized.count("。") >= 3 and ("否。" in normalized or "无。" in normalized):
                return True

        # 标点占比过高且缺少动作谓词，通常是表格噪声拼接。
        punct_count = len(re.findall(r"[，,。；;：:（）()\[\]{}]", text))
        if punct_count / max(len(text), 1) > 0.22:
            action_hints = ["应", "必须", "需", "支持", "通过", "提供", "实现", "具备", "监测", "控制", "接口"]
            if not any(h in text for h in action_hints):
                return True

        # OCR断裂文本（大量单字空格分隔）通常不是高质量需求。
        if re.search(r"(?:[\u4e00-\u9fa5A-Za-z0-9]\s+){5,}", text):
            return True

        # 短“标题:残句”且无明确需求谓词，通常是截断噪声。
        modal_hints = ["应", "必须", "需", "shall", "should"]
        if "：" in text and len(text) < 35 and not any(h in text for h in modal_hints):
            return True

        # 典型截断尾词。
        if text.endswith(("与。", "上。", "设。")) and len(text) < 50:
            return True

        return False

    def _should_suppress_section_requirements(self, section: Section, req_type: str) -> bool:
        """抑制父级汇总章节提取，优先保留子章节明细。"""
        if not section.children:
            return False

        title = section.title or ""
        child_titles = " ".join((c.title or "") for c in section.children)

        # 接口父章节若存在“接口描述”子章节，则仅保留子章节。
        if self.settings.is_interface_semantic_title(title) and ("接口描述" in child_titles):
            return True

        # 功能/性能父章节常见“汇总表”，若有子章节则抑制父级抽取。
        summary_hints = ["系统功能要求", "功能要求", "性能要求", "汇总", "总表"]
        if req_type == "functional" and section.children and "功能要求" in title:
            return True

        if req_type != "interface" and any(h in title for h in ["功能要求", "性能要求", "需求"]):
            if any(any(h in " ".join(str(c or "") for c in row) for h in summary_hints) for table in section.tables for row in table[:1]):
                return True

        return False

    def _is_generic_reference_requirement(self, text: str) -> bool:
        """过滤仅引用其他章节的泛化接口描述。"""
        t = (text or "").strip()
        if not t:
            return True
        generic_patterns = [
            r"应满足.*章节",
            r"应满足本文件",
            r"应满足.*规定",
            r"适用于测试类",
            r"合格性方法",
        ]
        return any(re.search(p, t) for p in generic_patterns)

    def _merge_fragment_requirements(self, requirements: List[Requirement]) -> List[Requirement]:
        """将短碎片需求并入前一条主需求，避免“指令左舷启停”类条目独立存在。"""
        if not requirements:
            return requirements

        merged: List[Requirement] = [requirements[0]]
        for req in requirements[1:]:
            prev = merged[-1]
            if prev.section_uid == req.section_uid and prev.type == req.type and self._is_fragment_requirement(req.description):
                prev.description = self._clean_description(
                    f"{prev.description.rstrip('；;。')}；{req.description.lstrip('；;。')}"
                )
            else:
                merged.append(req)
        return merged

    def _is_fragment_requirement(self, description: str) -> bool:
        text = (description or "").strip()
        if not text:
            return True
        if len(text) <= 18:
            return True

        strong_modal = ["应", "必须", "需", "shall", "should", "不得"]
        if not any(m in text for m in strong_modal):
            weak_tail = ["启停", "报警", "电流", "电压", "参数", "状态"]
            if len(text) <= 30 and any(text.rstrip("。").endswith(w) for w in weak_tail):
                return True
        return False

    def _sanitize_block_text(self, text: str, current_section_number: str) -> str:
        """清理正文块并裁剪误混入的其他章节内容。"""
        cleaned = self._normalize_ocr_spacing(text or "")
        cleaned = self._trim_text_to_current_section(cleaned, current_section_number)
        return cleaned.strip()

    def _trim_text_to_current_section(self, text: str, current_section_number: str) -> str:
        """遇到后续章节标题时截断，避免跨章节污染。"""
        if not text:
            return ""

        current = (current_section_number or "").strip()
        if not current:
            return text

        current_depth = current.count(".") + 1
        kept: List[str] = []
        for raw_line in text.splitlines():
            line = raw_line.strip()
            if not line:
                kept.append(raw_line)
                continue

            m = re.match(r"^(\d+(?:\.\d+){1,5})\s*[、.)）]?\s*(.+)$", line)
            if m:
                found_no = m.group(1)
                found_depth = found_no.count(".") + 1
                # 命中其他同级/上级或下级章节号时，认为当前章节正文到此结束。
                if found_no != current and (found_depth <= current_depth or found_no.startswith(f"{current}.")):
                    break
            kept.append(raw_line)

        return "\n".join(kept)

    def _iter_section_blocks(self, section: Section) -> List[Dict[str, Any]]:
        """返回章节中的顺序块（文本/表格）。"""
        blocks: List[Dict[str, Any]] = []
        if getattr(section, "blocks", None):
            for idx, block in enumerate(section.blocks, 1):
                block_type = block.get("type")
                if block_type == "text":
                    text = (block.get("text") or "").strip()
                    if text:
                        blocks.append({"type": "text", "text": text, "order": idx})
                elif block_type == "table":
                    table = block.get("table")
                    table_index = int(block.get("table_index", -1))
                    if table_index >= 0 and table_index < len(section.tables):
                        table = section.tables[table_index]
                    if table:
                        blocks.append(
                            {
                                "type": "table",
                                "table": table,
                                "table_index": table_index,
                                "order": idx,
                            }
                        )

        if blocks:
            return blocks

        # 兼容旧解析结果：无顺序块时退化为文本后表格。
        fallback_order = 1
        text = (section.content or "").strip()
        if text:
            blocks.append({"type": "text", "text": text, "order": fallback_order})
            fallback_order += 1
        for table_index, table in enumerate(section.tables):
            blocks.append(
                {
                    "type": "table",
                    "table": table,
                    "table_index": table_index,
                    "order": fallback_order,
                }
            )
            fallback_order += 1
        return blocks

    def _classify_table_mode(self, section_title: str, table: List[List[str]], req_type: str) -> str:
        """
        表格模式分类。
        返回：skip | single | interface | generic
        """
        if not table:
            return "generic"

        header_text = " ".join(str(c or "") for c in table[0])
        combined = f"{section_title} {header_text}".lower()

        has_interface_hint = any(k.lower() in combined for k in self.settings.table_interface_keywords)
        has_skip_hint = any(k.lower() in combined for k in self.settings.table_skip_keywords)

        if has_skip_hint and not has_interface_hint:
            return "skip"

        if any(k.lower() in combined for k in self.settings.table_single_requirement_keywords):
            return "single"

        if req_type == "interface":
            return "interface"

        # 接口表必须满足章节语义优先，避免“计算机通信需求”等章节被关键词误判为接口需求。
        if has_interface_hint and self.settings.is_interface_semantic_title(section_title):
            return "interface"

        return "generic"

    def _extract_interface_rows_from_table(self, table: List[List[str]]) -> List[Dict[str, str]]:
        """从接口表中提取接口名称/类型/来源/目的地字段。"""
        if not table or len(table) < 2:
            return []

        header = [self._clean_description(str(c or "")) for c in table[0]]

        def find_col(candidates: List[str]) -> int:
            for idx, h in enumerate(header):
                if any(k in h for k in candidates):
                    return idx
            return -1

        name_idx = find_col(["接口名称", "接口名", "名称"])
        type_idx = find_col(["接口类型", "类型", "通信类型"])
        source_idx = find_col(["数据来源", "来源", "发送方", "源"])
        dst_idx = find_col(["数据目的地", "目的地", "接收方", "去向"])

        rows: List[Dict[str, str]] = []
        for row in table[1:]:
            if not row or not any(str(c or "").strip() for c in row):
                continue

            def pick(col_idx: int) -> str:
                if col_idx < 0 or col_idx >= len(row):
                    return self.settings.interface_unknown_fallback
                return self._normalize_interface_field(row[col_idx])

            rows.append(
                {
                    "interface_name": pick(name_idx),
                    "interface_type": pick(type_idx),
                    "source": pick(source_idx),
                    "destination": pick(dst_idx),
                }
            )

        return rows

    def _llm_extract_table_as_single_requirement(self, section: Section, req_type: str) -> List[Requirement]:
        """硬件/软件/运行环境类表格按“一表一条”提取。"""
        if not section.tables:
            return []

        table_text = self._format_tables_for_prompt(section.tables)
        prompt = f"""请将下列表格合并为一条完整需求。

章节标题：{section.title}
表格内容：
{table_text}

要求：
1. 仅输出1条需求。
2. 保留关键配置项、数值、阈值、版本信息。
3. 使用原文措辞，尽量不改写。

输出JSON：
{{
  "requirements": [
    {{"req_id": "可为空", "description": "一条完整需求"}}
  ]
}}"""

        response = self.llm.call(prompt)
        data = self._parse_llm_json_response(response)
        if not data or not isinstance(data.get("requirements"), list) or not data["requirements"]:
            return []

        req_data = data["requirements"][0]
        desc = self._clean_description(req_data.get("description", ""))
        if not desc:
            return []

        doc_req_id = self._normalize_req_id(req_data.get("req_id", ""))
        req_id = self._generate_requirement_id(req_type, section.number, 1, doc_req_id, "")
        return [
            Requirement(
                req_id=req_id,
                description=desc,
                req_type=req_type,
                section_number=section.number,
                section_title=section.title,
                section_uid=section.uid,
                source_type="table",
            )
        ]
    
    def _llm_extract_requirements(self, section: Section, req_type: str, source_type: str = "text") -> List[Requirement]:
        """使用LLM提取需求。source_type: text|table"""
        requirements = []
        
        content_text = section.content or ""
        table_text = self._format_tables_for_prompt(section.tables)
        if len(content_text.strip()) < 8 and not table_text:
            return requirements
        
        is_text_source = source_type == "text"

        # 根据需求类型构建不同的提示词
        if req_type == 'interface':
            if is_text_source:
                if self.settings.preserve_source_text_for_text_blocks:
                    rewrite_rule = "需求描述必须尽量保持原文句式，不得润色改写，只允许去除换行、编号前缀和明显OCR噪声。"
                else:
                    rewrite_rule = "可在不改变语义和数值的前提下做轻微整理，使句子完整清晰。"
            else:
                rewrite_rule = "可在不改变语义和数值的前提下做轻微整理，使句子完整清晰。"

            prompt = f"""请从以下SRS文档章节中提取具体的接口需求，并智能识别每个接口的详细信息。

章节编号：{section.number}
章节标题：{section.title}
章节内容：
{content_text}

章节内表格（若有）：
{table_text if table_text else '无'}

提取要求：
1. 只提取具体的、可验证的接口需求
2. 不要提取系统描述、背景说明等非需求内容
3. 去除原文中的换行符、表格格式噪声
4. {rewrite_rule}
5. 每条需求应该是完整的句子，描述清楚接口规范
6. 如果有多条需求，请分别列出
6.1 不要输出“a）/b）/1）”等前缀编号
6.2 不要输出短语碎片（如“左舷艏侧推启停”），应并入主需求句
7. 对于每条接口需求，请智能识别以下信息（若表格中存在对应列，优先按表格填写）：
   - interface_name: 接口名称
   - interface_type: 接口类型 （如：CAN接口、以太网接口、串口等）
   - source: 数据来源/发送方（数据或信号从哪里来）
   - destination: 数据目的地/接收方（数据或信号发送到哪里）
8. 如果某个字段无法从文本中识别，请填写"未知"
9. 若原文给出需求编号，请优先使用原文编号（req_id）

请以JSON格式输出，格式如下：
{{
    "requirements": [
        {{
            "req_id": "需求编号（如有）",
            "description": "接口需求描述",
            "interface_name": "接口名称",
            "interface_type": "接口类型",
            "source": "数据来源",
            "destination": "数据目的地"
        }}
    ]
}}

如果该章节没有可提取的需求，返回空数组：
{{"requirements": []}}

JSON输出："""
        else:
            if is_text_source:
                if self.settings.preserve_source_text_for_text_blocks:
                    source_rule = "需求描述必须保持原文，不得润色改写；仅允许移除换行、表格线噪声和列表编号。"
                else:
                    source_rule = "需求描述以原文为主，可做轻微重组以形成完整句子。"
                split_rule = "仅在语义完全独立且原文可明确拆分时拆分，禁止将同一主句拆成碎片。"
            else:
                source_rule = "需求描述以原文为主，可做轻微重组以形成完整句子。"
                split_rule = "仅在语义独立可验证时拆分。"

            prompt = f"""请从以下SRS文档章节中提取具体的软件需求。

章节编号：{section.number}
章节标题：{section.title}
章节内容：
{content_text}

章节内表格（若有）：
{table_text if table_text else '无'}

提取要求：
1. 同时提取正文与表格中的具体、可验证的软件需求
2. 不要提取系统描述、背景说明等非需求内容
3. {source_rule}
4. 严禁改变任何数值、阈值、状态名、信号名和逻辑条件
5. 去除原文中的多余换行符和表格格式符号，但保留语句内容
5. 每条需求应该是完整的句子
6. 如果有多条需求，请分别列出
6.1 去除“a）/b）/1）”等编号前缀
7. 如果一段需求描述内有多条需求点，按规则判断是否拆分
8. 拆分判定：{split_rule} 条件-动作链、并列限定语、因果承接不应强拆
8.1 对“具体包括/其中包括”后的短项，不要单独成条，必须并入主句
9. 优先保持需求语义完整，避免过度拆分导致碎片化
10. 过滤重复或过于相似的需求，只保留独特的需求
11. 若原文给出需求编号，请优先使用原文编号（req_id）

请以JSON格式输出，格式如下：
{{
    "requirements": [
        {{"req_id": "需求编号（如有）", "description": "需求描述1"}},
        {{"req_id": "需求编号（如有）", "description": "需求描述2"}}
    ]
}}

如果该章节没有可提取的需求，返回空数组：
{{"requirements": []}}

JSON输出："""
        
        try:
            response = self.llm.call(prompt)
            data = self._parse_llm_json_response(response)
            
            if data and 'requirements' in data:
                # 查找父需求编号（第一个合法完整编号的需求）
                parent_req_id = ""
                complete_id_pattern = r'^[A-Za-z0-9]{2,10}[-_].+$'
                for req_data in data['requirements']:
                    temp_id = self._normalize_req_id(req_data.get('req_id', '') or req_data.get('id', ''))
                    if not temp_id:
                        temp_desc = req_data.get('description', '').strip()
                        temp_id, _ = self._extract_requirement_id_from_text(temp_desc)
                    # 验证是否为合法的完整编号格式
                    if temp_id and re.match(complete_id_pattern, temp_id):
                        parent_req_id = temp_id.replace('_', '-')
                        break
                
                for i, req_data in enumerate(data['requirements'], 1):
                    desc = req_data.get('description', '').strip()
                    if desc and len(desc) > 5:
                        # 清理描述中的多余换行符和表格符号
                        desc = self._clean_description(desc)
                        split_descs = self._split_requirement_description(desc)
                        if not split_descs:
                            split_descs = [desc]
                        
                        # 需求ID优先使用文档给出的编号
                        doc_req_id = self._normalize_req_id(req_data.get('req_id', '') or req_data.get('id', ''))
                        if not doc_req_id:
                            doc_req_id, desc = self._extract_requirement_id_from_text(desc)

                        for split_idx, split_desc in enumerate(split_descs, 1):
                            # 生成最终的需求ID（支持拆分后后缀）
                            req_id = self._generate_requirement_id(
                                req_type,
                                section.number,
                                i,
                                doc_req_id,
                                parent_req_id,
                                split_idx,
                                len(split_descs),
                            )

                            # 接口需求提取额外字段
                            interface_name = ""
                            interface_type = ""
                            source = ""
                            destination = ""
                            if req_type == 'interface':
                                interface_name = self._normalize_interface_field(
                                    req_data.get('interface_name', self.settings.interface_unknown_fallback)
                                )
                                interface_type = self._normalize_interface_field(
                                    req_data.get('interface_type', self.settings.interface_unknown_fallback)
                                )
                                source = self._normalize_interface_field(
                                    req_data.get('source', self.settings.interface_unknown_fallback)
                                )
                                destination = self._normalize_interface_field(
                                    req_data.get('destination', self.settings.interface_unknown_fallback)
                                )

                            req = Requirement(
                                req_id=req_id,
                                description=split_desc,
                                req_type=req_type,
                                section_number=section.number,
                                section_title=section.title,
                                section_uid=section.uid,
                                interface_name=interface_name,
                                interface_type=interface_type,
                                source=source,
                                destination=destination
                            )
                            requirements.append(req)
        except Exception as e:
            raise RuntimeError(
                f"LLM提取需求失败: section={section.number} {section.title}, error={e}"
            ) from e
        
        return requirements

    def _llm_extract_table_requirements(
        self,
        section: Section,
        req_type: str,
        interface_rows: Optional[List[Dict[str, str]]] = None,
    ) -> List[Requirement]:
        """使用LLM语义化提取表格需求。"""
        if not self.llm:
            raise RuntimeError("LLM实例未初始化，当前版本仅支持LLM提取")
        if not section.tables:
            return []

        table = section.tables[0]
        is_sequence_table = self._is_time_series_table(table)
        table_text = self._format_tables_for_prompt([table])
        merge_hint = "是" if is_sequence_table and self.settings.sequence_table_merge == "single_requirement" else "否"

        prompt = f"""请从下列表格中提取并组织软件需求，要求以语义完整的需求句输出。

章节编号：{section.number}
章节标题：{section.title}
需求类型：{req_type}
该表是否按时间序列指令组织：{merge_hint}

表格内容：
{table_text}

提取规则：
1. 不是简单逐字抄表格，请结合列含义组织成完整需求句。
2. 保留原文大部分关键词、阈值、数值、状态名，不得改变逻辑和数值。
3. 允许轻微补充主语或上下文，使语义更完整。
4. 若为时间序列指令表，优先合并为1条需求，描述完整执行序列。
5. 若有明显独立语义点，可输出多条需求。

请输出JSON：
{{
  "requirements": [
    {{"req_id": "可为空", "description": "完整需求描述"}}
  ]
}}"""

        try:
            response = self.llm.call(prompt)
            data = self._parse_llm_json_response(response)
            requirements: List[Requirement] = []
            if data and isinstance(data.get("requirements"), list):
                for i, req_data in enumerate(data["requirements"], 1):
                    desc = self._clean_description(req_data.get("description", ""))
                    if not desc:
                        continue
                    doc_req_id = self._normalize_req_id(req_data.get("req_id", ""))
                    req_id = self._generate_requirement_id(req_type, section.number, i, doc_req_id, "")

                    row_info = {}
                    if req_type == "interface" and interface_rows and i - 1 < len(interface_rows):
                        row_info = interface_rows[i - 1]

                    requirements.append(
                        Requirement(
                            req_id=req_id,
                            description=desc,
                            req_type=req_type,
                            section_number=section.number,
                            section_title=section.title,
                            section_uid=section.uid,
                            interface_name=row_info.get("interface_name", self.settings.interface_unknown_fallback) if req_type == 'interface' else "",
                            interface_type=row_info.get("interface_type", self.settings.interface_unknown_fallback) if req_type == 'interface' else "",
                            source=row_info.get("source", self.settings.interface_unknown_fallback) if req_type == 'interface' else "",
                            destination=row_info.get("destination", self.settings.interface_unknown_fallback) if req_type == 'interface' else "",
                            source_type="table",
                        )
                    )

            # 小型表格兜底：若LLM漏行，则将剩余有效行补齐为需求，避免“6行只提4行”。
            expected_rows = [
                [self._clean_description(str(c or "")) for c in row]
                for row in (table[1:] if len(table) > 1 else table)
                if row and any(str(c or "").strip() for c in row)
            ]
            if req_type in {"functional", "other"} and expected_rows and len(expected_rows) <= 12 and len(requirements) < len(expected_rows):
                next_index = len(requirements) + 1
                for row in expected_rows[len(requirements):]:
                    row_desc = self._clean_description("，".join([c for c in row if c]))
                    if not row_desc or len(row_desc) < 6:
                        continue

                    # 避免补齐时引入明显重复。
                    duplicated = any(
                        SequenceMatcher(None, self._normalize_text_for_dedup(row_desc), self._normalize_text_for_dedup(r.description)).ratio() >= 0.92
                        for r in requirements
                    )
                    if duplicated:
                        continue

                    req_id = self._generate_requirement_id(req_type, section.number, next_index, "", "")
                    requirements.append(
                        Requirement(
                            req_id=req_id,
                            description=row_desc,
                            req_type=req_type,
                            section_number=section.number,
                            section_title=section.title,
                            section_uid=section.uid,
                            source_type="table",
                        )
                    )
                    next_index += 1

            if not requirements:
                logger.warning(
                    "LLM表格提取未产出需求: section=%s %s",
                    section.number,
                    section.title,
                )
                return []
            return requirements
        except Exception as e:
            raise RuntimeError(
                f"LLM表格语义化提取失败: section={section.number} {section.title}, error={e}"
            ) from e

    def _maybe_light_rewrite(self, description: str, source_type: str, req_type: str) -> str:
        """仅在LLM模式做轻微扩写，且通过保真校验。"""
        description = self._clean_description(description)
        if not description:
            return description
        if req_type != "interface":
            return description
        if source_type != "table":
            return description
        if not self.llm or not self.settings.llm_light_rewrite_enabled:
            return description

        need_rewrite = len(description) < 28
        if not need_rewrite:
            return description

        prompt = f"""请对下面需求做轻微扩写，使语义更完整。

原文：{description}

要求：
1. 保留原文大部分表述，不改变核心语义。
2. 不得修改任何数值、阈值、状态名称、信号名称。
3. 只允许补充必要主语/宾语，长度尽量控制在原文的1.25倍以内。
4. 仅返回改写后的单句文本。"""

        try:
            rewritten = self._clean_description(self.llm.call(prompt))
            if not rewritten:
                return description

            preserve_ratio = self._calculate_preserve_ratio(description, rewritten)
            growth_ratio = len(rewritten) / max(len(description), 1)
            if preserve_ratio < self.settings.preserve_ratio_min:
                return description
            if growth_ratio > self.settings.max_length_growth_ratio:
                return description
            if not self._numbers_consistent(description, rewritten):
                return description
            return rewritten
        except Exception:
            return description

    def _edit_distance(self, a: str, b: str) -> int:
        """计算字符串编辑距离（Levenshtein）。"""
        if a == b:
            return 0
        if not a:
            return len(b)
        if not b:
            return len(a)

        n, m = len(a), len(b)
        dp = [[0] * (m + 1) for _ in range(n + 1)]
        for i in range(n + 1):
            dp[i][0] = i
        for j in range(m + 1):
            dp[0][j] = j

        for i in range(1, n + 1):
            for j in range(1, m + 1):
                cost = 0 if a[i - 1] == b[j - 1] else 1
                dp[i][j] = min(
                    dp[i - 1][j] + 1,
                    dp[i][j - 1] + 1,
                    dp[i - 1][j - 1] + cost,
                )

        return dp[n][m]

    def _calculate_preserve_ratio(self, original: str, rewritten: str) -> float:
        original_tokens = [c for c in re.sub(r"\s+", "", original) if c]
        rewritten_tokens = set(c for c in re.sub(r"\s+", "", rewritten) if c)
        if not original_tokens:
            return 1.0
        hit = sum(1 for c in original_tokens if c in rewritten_tokens)
        return hit / max(len(original_tokens), 1)

    def _numbers_consistent(self, original: str, rewritten: str) -> bool:
        pattern = r"[<>≤≥]?\d+(?:\.\d+)?(?:[A-Za-z%]*)"
        orig_nums = set(re.findall(pattern, original))
        rewrite_nums = set(re.findall(pattern, rewritten))
        return orig_nums.issubset(rewrite_nums)

    def _semantic_integrity_postprocess(self, requirements: List[Requirement]) -> List[Requirement]:
        """语义完整性后处理：合并被误拆的紧耦合需求链。"""
        if not self.settings.semantic_guard_enabled or not requirements:
            return requirements

        merged: List[Requirement] = [requirements[0]]
        for req in requirements[1:]:
            prev = merged[-1]
            if self._should_merge_semantic(prev, req):
                prev.description = self._clean_description(
                    f"{prev.description.rstrip('；;。')}；{req.description.lstrip('；;。')}"
                )
            else:
                merged.append(req)
        return merged

    def _should_merge_semantic(self, prev: Requirement, curr: Requirement) -> bool:
        if prev.section_uid != curr.section_uid or prev.type != curr.type:
            return False

        prev_desc = prev.description
        curr_desc = curr.description

        if curr_desc.startswith(("该", "其", "上述", "此", "该报警", "该信号")):
            return True
        if self.settings.preserve_alarm_chain and ("报警" in prev_desc and "持续" in curr_desc):
            return True
        if self.settings.preserve_condition_action_chain:
            if "进入整星安全模式" in prev_desc and ("过放电模式" in curr_desc or "发送" in curr_desc):
                return True
            if "若蓄电池充电" in prev_desc and (
                "退出低功耗模式" in curr_desc or "热控" in curr_desc or "姿控" in curr_desc
            ):
                return True
        if ("产生" in prev_desc and "报警" in prev_desc and "持续" in curr_desc):
            return True
        return False

    def _renumber_requirements_continuous(self, requirements: List[Requirement]) -> List[Requirement]:
        """按文档顺序对去重后的需求重新连续编号。"""
        if not requirements:
            return requirements

        ordered = sorted(requirements, key=lambda r: (r.source_order, r.section_number or ""))
        counters: Dict[Tuple[str, str], int] = {}

        for req in ordered:
            section_key = req.section_uid or req.section_number or "NA"
            prefix = self.settings.type_prefix.get(req.type, "FR")
            counter_key = (section_key, prefix)
            counters[counter_key] = counters.get(counter_key, 0) + 1
            section_part = req.section_number if req.section_number else "NA"
            req.id = f"{prefix}-{section_part}-{counters[counter_key]}"

        return ordered
    
    def _identify_requirement_type(self, title: str, content: str) -> str:
        """
        通过标题和内容识别需求类型
        
        根据章节标题和内容判断需求类型：
        - 标题或内容中包含"接口"相关词汇 -> 接口需求
        - 其他情况 -> 功能需求（默认）
        
        注意：不能仅靠标题判断是否为功能需求，若无法识别具体类型，默认为功能需求
        """
        return self.settings.detect_requirement_type(title, content)
    
    def _generate_requirement_id(self, req_type: str, section_number: str, index: int,
                                 doc_req_id: str = "", parent_req_id: str = "",
                                 split_index: int = 1, split_total: int = 1) -> str:
        """
        生成需求ID（三级优先级）
        
        优先级规则：
        1. 如果doc_req_id是合法的完整编号（以2-10个字母或数字开头，后跟分隔符），直接使用
           例如: NY01-01、FR-3.1.2-1、AIRSAT07-GD03-04
        2. 如果doc_req_id是代号/序号，且有parent_req_id，则组合
           格式: {parent_req_id}-{doc_req_id}，例如: NY01-01-K101
        3. 否则自动生成
           格式: {PREFIX}-{section_number}-{index}，例如: IR-4.1.1-1（保留章节号中的点号）
        
        Args:
            req_type: 需求类型
            section_number: 章节编号
            index: 序号
            doc_req_id: 文档中提取的编号/代号
            parent_req_id: 父需求编号（用于子需求）
        """
        return self.id_generator.generate(
            req_type=req_type,
            section_number=section_number,
            index=index,
            doc_req_id=doc_req_id,
            parent_req_id=parent_req_id,
            split_index=split_index,
            split_total=split_total,
        )

    def _normalize_req_id(self, req_id: str) -> str:
        """规范化需求编号"""
        return self.id_generator.normalize(req_id)

    def _normalize_ocr_spacing(self, text: str) -> str:
        """归一化OCR导致的词内断裂空格。"""
        normalized = str(text or "")
        if not getattr(self.settings, "ocr_spacing_normalize", True):
            return normalized

        normalized = normalized.replace("\u3000", " ").replace("\xa0", " ")
        normalized = re.sub(r"(?<=[\u4e00-\u9fff])\s+(?=[\u4e00-\u9fff])", "", normalized)
        normalized = re.sub(r"(?<=[\u4e00-\u9fff])\s+(?=[A-Za-z0-9])", "", normalized)
        normalized = re.sub(r"(?<=[A-Za-z0-9])\s+(?=[\u4e00-\u9fff])", "", normalized)
        return normalized
    
    def _clean_description(self, text: str) -> str:
        """清理需求描述"""
        text = self._normalize_ocr_spacing(text)
        # 替换换行符为空格
        text = re.sub(r'\n+', ' ', text)
        # 替换多个空格为单个空格
        text = re.sub(r'\s+', ' ', text)
        # 去除表格噪声
        text = re.sub(r'[\|│┃]+', ' ', text)
        # 去除前缀枚举编号（如a）/b）/1）/①））但保留正文语义。
        text = self._strip_leading_enumeration(text)
        text = self._normalize_ocr_spacing(text)
        # 去除首尾空白
        text = text.strip()
        text = text.rstrip("；;，,")
        # 限制长度
        if len(text) > 1000:
            text = text[:1000] + '...'
        if self.settings.ensure_terminal_period:
            text = self._ensure_terminal_period(text)
        return text

    def _strip_leading_enumeration(self, text: str) -> str:
        cleaned = (text or "").strip()
        patterns = [
            r"^\d+\s*(?=[A-Za-z][\)）])",
            r"^(?:[A-Za-z]|\d+|[①②③④⑤⑥⑦⑧⑨⑩]|[一二三四五六七八九十]+)\s*[\)）、:]\s*",
            r"^(?:[A-Za-z]|\d+|[①②③④⑤⑥⑦⑧⑨⑩]|[一二三四五六七八九十]+)\s*\.\s*(?!\d)",
        ]
        for p in patterns:
            cleaned = re.sub(p, "", cleaned)
        return cleaned

    def _ensure_terminal_period(self, text: str) -> str:
        if not text:
            return text
        if text.endswith(("。", "！", "？")):
            return text
        return text + "。"

    def _normalize_interface_field(self, value: Any) -> str:
        text = self._normalize_ocr_spacing(str(value or "")).strip()
        if not text:
            return self.settings.interface_unknown_fallback
        if text in {"N/A", "NA", "None", "null", "NULL", "-", "--", "未知"}:
            return self.settings.interface_unknown_fallback
        text = text.rstrip("；;，,。")
        return text if text else self.settings.interface_unknown_fallback

    def _snap_to_source_sentence(self, description: str, source_text: str) -> str:
        """将非接口需求尽量贴合回原文句子。"""
        source_text = self._normalize_ocr_spacing(source_text or "").strip()
        if not description or not source_text:
            return description

        if description in source_text:
            return description

        normalized_desc = self._normalize_ocr_spacing(description)

        candidates = [
            self._clean_description(x)
            for x in re.split(r"[\n。；;]", source_text)
            if self._clean_description(x)
        ]
        if not candidates:
            return description

        best = min(candidates, key=lambda c: self._edit_distance(normalized_desc, self._normalize_ocr_spacing(c)))
        dist = self._edit_distance(normalized_desc, self._normalize_ocr_spacing(best))
        if dist <= self.settings.non_interface_max_edit_distance:
            return best
        return description

    def _format_tables_for_prompt(self, tables: List[List[List[str]]]) -> str:
        """格式化表格内容用于LLM提示词"""
        if not tables:
            return ""
        lines = []
        for idx, table in enumerate(tables, 1):
            lines.append(f"表格{idx}:")
            for row in table:
                row_text = " | ".join(self._clean_description(cell) for cell in row if cell)
                if row_text:
                    lines.append(row_text)
        return "\n".join(lines)

    def _extract_requirement_id_from_text(self, text: str) -> Tuple[Optional[str], str]:
        """
        从文本中提取需求编号
        
        支持的格式：
        1. 完整编号：NY01-01、FR-3.1.2-1
        2. 代号/序号：K101、D61、a)、1)
        """
        return self.id_generator.extract_from_text(text)

    def _split_requirement_description(self, text: str) -> List[str]:
        if not text:
            return []
        if any(h in text for h in ["具体包括", "其中包括", "包括但不限于", "主要包括"]):
            return [text]
        if "时间序列" in text and "执行指令" in text:
            return [text]
        if not self.splitter:
            return [text]
        return self.splitter.split(text)

    def _deduplicate_requirements(self, requirements: List[Requirement]) -> List[Requirement]:
        deduped: List[Requirement] = []
        threshold = self.settings.dedup_similarity_threshold
        for req in requirements:
            normalized_desc = self._normalize_text_for_dedup(req.description)
            drop = False
            for kept in deduped:
                if kept.type != req.type:
                    continue

                # 接口需求优先按结构化键去重（同章节同接口同源同目的）。
                if req.type == "interface" and kept.section_uid == req.section_uid:
                    if self._interface_dedup_key(kept) == self._interface_dedup_key(req):
                        # 优先保留字段更完整的条目。
                        if self._interface_field_completeness(req) > self._interface_field_completeness(kept):
                            kept.interface_name = req.interface_name
                            kept.interface_type = req.interface_type
                            kept.source = req.source
                            kept.destination = req.destination
                            kept.description = req.description
                            kept.source_type = req.source_type
                            kept.source_order = req.source_order
                        drop = True
                        break

                    # 同一来源与目的地的接口需求若语义高度重合，保留更完整一条。
                    kept_src = self._normalize_text_for_dedup(kept.source)
                    req_src = self._normalize_text_for_dedup(req.source)
                    kept_dst = self._normalize_text_for_dedup(kept.destination)
                    req_dst = self._normalize_text_for_dedup(req.destination)
                    unknown = self._normalize_text_for_dedup(self.settings.interface_unknown_fallback)
                    if kept_src and req_src and kept_dst and req_dst and kept_src == req_src and kept_dst == req_dst:
                        if kept_src != unknown and kept_dst != unknown:
                            name_sim = SequenceMatcher(
                                None,
                                self._normalize_text_for_dedup(kept.interface_name),
                                self._normalize_text_for_dedup(req.interface_name),
                            ).ratio()
                            sim_sd = SequenceMatcher(
                                None,
                                normalized_desc,
                                self._normalize_text_for_dedup(kept.description),
                            ).ratio()
                            if sim_sd >= 0.72 or name_sim >= 0.72:
                                if self._interface_field_completeness(req) >= self._interface_field_completeness(kept):
                                    kept.interface_name = req.interface_name
                                    kept.interface_type = req.interface_type
                                    kept.source = req.source
                                    kept.destination = req.destination
                                    if len(req.description) >= len(kept.description):
                                        kept.description = req.description
                                    kept.source_type = req.source_type
                                    kept.source_order = min(kept.source_order, req.source_order)
                                drop = True
                                break

                sim = SequenceMatcher(
                    None,
                    normalized_desc,
                    self._normalize_text_for_dedup(kept.description),
                ).ratio()
                if sim >= threshold:
                    # 同章节优先保留正文块。
                    if (
                        self.settings.prefer_text_over_table
                        and kept.section_uid == req.section_uid
                        and kept.source_type == "table"
                        and req.source_type == "text"
                    ):
                        kept.description = req.description
                        kept.source_type = req.source_type
                        kept.source_order = req.source_order
                    drop = True
                    break
            if not drop:
                deduped.append(req)
        return deduped

    def _interface_dedup_key(self, req: Requirement) -> Tuple[str, str, str, str]:
        return (
            self._normalize_text_for_dedup(req.interface_name),
            self._normalize_text_for_dedup(req.interface_type),
            self._normalize_text_for_dedup(req.source),
            self._normalize_text_for_dedup(req.destination),
        )

    def _interface_field_completeness(self, req: Requirement) -> int:
        fields = [req.interface_name, req.interface_type, req.source, req.destination]
        score = 0
        for f in fields:
            value = (f or "").strip()
            if value and value != self.settings.interface_unknown_fallback:
                score += 1
        return score

    def _drop_inferior_interface_duplicates(self, requirements: List[Requirement]) -> List[Requirement]:
        """同章节同接口名/来源/目的地重复时，保留字段更完整条目。"""
        if not requirements:
            return requirements

        grouped: Dict[Tuple[str, str, str, str], List[Requirement]] = {}
        others: List[Requirement] = []
        for req in requirements:
            if req.type != "interface":
                others.append(req)
                continue

            key = (
                req.section_uid or req.section_number or "",
                self._normalize_text_for_dedup(req.interface_name),
                self._normalize_text_for_dedup(req.source),
                self._normalize_text_for_dedup(req.destination),
            )
            grouped.setdefault(key, []).append(req)

        kept_interfaces: List[Requirement] = []
        for group in grouped.values():
            if len(group) == 1:
                kept_interfaces.append(group[0])
                continue

            best = group[0]
            for cand in group[1:]:
                best_score = self._interface_field_completeness(best)
                cand_score = self._interface_field_completeness(cand)
                if cand_score > best_score:
                    best = cand
                    continue
                if cand_score == best_score and len(cand.description) > len(best.description):
                    best = cand
                    continue
                if cand_score == best_score and len(cand.description) == len(best.description):
                    if cand.source_order < best.source_order:
                        best = cand
            kept_interfaces.append(best)

        # 第二阶段：同章节同来源/目的地下，删除与“已知类型”高度相似的“未知类型”重复项。
        by_section_src_dst: Dict[Tuple[str, str, str], List[Requirement]] = {}
        for req in kept_interfaces:
            key = (
                req.section_uid or req.section_number or "",
                self._normalize_text_for_dedup(req.source),
                self._normalize_text_for_dedup(req.destination),
            )
            by_section_src_dst.setdefault(key, []).append(req)

        pruned_interfaces: List[Requirement] = []
        unknown_norm = self._normalize_text_for_dedup(self.settings.interface_unknown_fallback)
        for key, group in by_section_src_dst.items():
            known = [
                r for r in group
                if self._normalize_text_for_dedup(r.interface_type) != unknown_norm
            ]
            if not known:
                pruned_interfaces.extend(group)
                continue

            for cand in group:
                cand_type = self._normalize_text_for_dedup(cand.interface_type)
                if cand_type != unknown_norm:
                    pruned_interfaces.append(cand)
                    continue

                should_drop = False
                for k in known:
                    name_sim = SequenceMatcher(
                        None,
                        self._normalize_text_for_dedup(cand.interface_name),
                        self._normalize_text_for_dedup(k.interface_name),
                    ).ratio()
                    desc_sim = SequenceMatcher(
                        None,
                        self._normalize_text_for_dedup(cand.description),
                        self._normalize_text_for_dedup(k.description),
                    ).ratio()
                    if name_sim >= 0.60 or desc_sim >= 0.60:
                        should_drop = True
                        break

                if not should_drop:
                    pruned_interfaces.append(cand)

        merged = others + pruned_interfaces
        return sorted(merged, key=lambda r: (r.source_order, r.section_number or ""))

    def _normalize_text_for_dedup(self, text: str) -> str:
        normalized = self._normalize_ocr_spacing(text or "").strip().lower()
        normalized = normalized.translate(str.maketrans({
            "，": ",",
            "；": ";",
            "：": ":",
            "（": "(",
            "）": ")",
            "。": ".",
        }))
        normalized = re.sub(r"\s+", " ", normalized)
        return normalized

    def _global_deduplicate_requirements(self, requirements: List[Requirement]) -> List[Requirement]:
        deduped: List[Requirement] = []
        threshold = self.settings.dedup_similarity_threshold
        for req in requirements:
            current_text = self._normalize_text_for_dedup(req.description)
            duplicate_idx = -1
            for i, kept in enumerate(deduped):
                if kept.type != req.type:
                    continue
                sim = SequenceMatcher(
                    None,
                    current_text,
                    self._normalize_text_for_dedup(kept.description),
                ).ratio()
                if sim >= threshold:
                    duplicate_idx = i
                    break

            if duplicate_idx < 0:
                deduped.append(req)
                continue

            kept = deduped[duplicate_idx]
            if self._prefer_requirement_for_dedup(kept, req):
                deduped[duplicate_idx] = req

        return sorted(deduped, key=lambda r: (r.source_order, r.section_number or ""))

    def _section_depth(self, section_number: str) -> int:
        sn = (section_number or "").strip()
        if not sn:
            return 0
        return sn.count(".") + 1

    def _prefer_requirement_for_dedup(self, kept: Requirement, candidate: Requirement) -> bool:
        # 同章节优先保留正文。
        if (
            self.settings.prefer_text_over_table
            and kept.section_uid == candidate.section_uid
            and kept.source_type == "table"
            and candidate.source_type == "text"
        ):
            return True

        # 优先保留更深层级章节（子章节）需求，抑制父级汇总重复。
        kept_depth = self._section_depth(kept.section_number)
        cand_depth = self._section_depth(candidate.section_number)
        if cand_depth > kept_depth:
            return True
        if cand_depth < kept_depth:
            return False

        # 接口需求中，优先保留非“泛引用”条目。
        if kept.type == "interface":
            kept_generic = self._is_generic_reference_requirement(kept.description)
            cand_generic = self._is_generic_reference_requirement(candidate.description)
            if kept_generic and not cand_generic:
                return True
            if not kept_generic and cand_generic:
                return False

        # 同层级时保留更早出现的原文顺序。
        return candidate.source_order < kept.source_order

    def _is_time_series_table(self, table: List[List[str]]) -> bool:
        if not table:
            return False

        header = " ".join(cell for cell in table[0] if cell)
        header_has_time = any(k in header for k in ["时间", "时刻", "time", "TIME", "T0"])
        header_has_action = any(k in header for k in ["指令", "动作", "行为", "操作", "名称"])

        time_pattern = re.compile(r"^T\s*0(?:\s*[+＋-]\s*\d+\s*[sS秒]?)?$")
        data_rows = table[1:] if len(table) > 1 else []
        time_like_rows = 0
        for row in data_rows:
            if not row:
                continue
            first_cell = (row[0] or "").strip() if row else ""
            if time_pattern.match(first_cell):
                time_like_rows += 1

        return (header_has_time and header_has_action) or (time_like_rows >= self.settings.merge_time_series_rows_min)

    def _parse_llm_json_response(self, response: str) -> Optional[Dict]:
        """解析LLM的JSON响应"""
        try:
            return json.loads(response)
        except json.JSONDecodeError:
            # 尝试提取JSON代码块
            try:
                json_match = re.search(r'```(?:json)?\s*(.*?)\s*```', response, re.DOTALL)
                if json_match:
                    return json.loads(json_match.group(1))
                
                # 尝试查找JSON对象
                json_match = re.search(r'\{.*\}', response, re.DOTALL)
                if json_match:
                    return json.loads(json_match.group(0))
            except Exception:
                pass
        
        logger.warning(f"无法解析LLM响应为JSON: {response[:200]}")
        return None
    
    def get_statistics(self) -> Dict:
        """获取需求统计信息"""
        stats = {
            'total': len(self.requirements),
            'by_type': {}
        }
        
        for req in self.requirements:
            req_type = req.type
            if req_type not in stats['by_type']:
                stats['by_type'][req_type] = 0
            stats['by_type'][req_type] += 1
        
        return stats
