# issue_filter.py
"""
问题单过滤模块
基于知识库和LLM对静态分析工具生成的Excel问题单进行过滤、优先级排序和影响域分析。
"""
import os
import json
import re
import time
import pandas as pd
from typing import List, Dict, Optional, Tuple
import numpy as np
import faiss
from openai import OpenAI
from tree_sitter import Language, Parser
import tree_sitter_cpp
from config import QWEN_API_KEY, QWEN_API_URL, VECTOR_DB_PATH, METADATA_PATH, PROJECT_ROOT
import logging
logger = logging.getLogger(__name__)
# 配置指向 DashScope 的 OpenAI 兼容客户端
# 注意：config.py 中为 QWEN_API_URL，此处变量名应保持一致
client = OpenAI(
    api_key=QWEN_API_KEY,
    base_url=QWEN_API_URL
)
EMBEDDING_MODEL_NAME = "text-embedding-v4"
LLM_MODEL_NAME = "qwen-max"

# 从 config 加载知识库路径
KB_INDEX_PATH = VECTOR_DB_PATH
KB_META_PATH = METADATA_PATH
Project_path = str(PROJECT_ROOT)  # 从 config 读取项目根路径

# 初始化 C/C++ 语法解析器
CPP_LANGUAGE = Language(tree_sitter_cpp.language())
parser = Parser()
parser.language = CPP_LANGUAGE

# 全局变量，用于存储加载的知识库
_index = None
_kb_meta = None
KB_INDEX_PATH = ""  # FAISS索引文件路径
KB_META_PATH = ""   # 元数据文件路径


def _load_knowledge_base():
    """加载FAISS知识库和元数据（惰性加载）"""
    global _index, _kb_meta

    # 添加路径验证
    if not KB_INDEX_PATH or not KB_INDEX_PATH.strip():
        logger.error("KB_INDEX_PATH 为空，无法加载知识库")
        raise ValueError("知识库向量数据库路径未设置")

    if not KB_META_PATH or not KB_META_PATH.strip():
        logger.error("KB_META_PATH 为空，无法加载知识库")
        raise ValueError("知识库元数据文件路径未设置")

    if _index is None or _kb_meta is None:
        print("Loading FAISS index...")
        print(f"[DEBUG] 尝试加载FAISS文件: {KB_INDEX_PATH}")

        # 再次验证文件存在
        if not os.path.exists(KB_INDEX_PATH):
            logger.error(f"FAISS文件不存在: {KB_INDEX_PATH}")
            raise FileNotFoundError(f"FAISS索引文件不存在: {KB_INDEX_PATH}")

        if not os.path.exists(KB_META_PATH):
            logger.error(f"元数据文件不存在: {KB_META_PATH}")
            raise FileNotFoundError(f"元数据文件不存在: {KB_META_PATH}")

        try:
            _index = faiss.read_index(KB_INDEX_PATH)
            with open(KB_META_PATH, "r", encoding="utf-8") as f:
                _kb_meta = json.load(f)

            # 校验维度
            assert _index.d == 1024, f"FAISS 维度应为 1024，但实际为 {_index.d}。请确认由 text-embedding-v4 构建！"
            assert len(_kb_meta) == _index.ntotal, "meta.json 条目数与 FAISS 向量数不一致！"
            print(f"Knowledge base loaded: {len(_kb_meta)} entries, dim={_index.d}")

        except Exception as e:
            logger.error(f"加载知识库失败: {e}")
            _index = None
            _kb_meta = None
            raise

    return _index, _kb_meta

def embed_text(text: str) -> np.ndarray:
    """使用新版 OpenAI 客户端调用 DashScope embedding"""
    try:
        response = client.embeddings.create(
            model=EMBEDDING_MODEL_NAME,
            input=text
        )
        embedding = response.data[0].embedding
        emb_np = np.array(embedding, dtype=np.float32).reshape(1, -1)
        return emb_np
    except Exception as e:
        print(f"Embedding API error: {e}")
        return np.zeros((1, 1024), dtype=np.float32)

def get_function_context(file_path: str, line_number: int) -> Optional[Tuple[str, str]]:
    import chardet
    try:
        with open(file_path, 'rb') as f:
            raw_data = f.read()
        detected = chardet.detect(raw_data)
        encoding = detected['encoding']
        if encoding is None:
            encoding = 'utf-8'
        if encoding.lower() in ('ascii', 'utf-8', 'utf-8-sig'):
            encoding = 'utf-8'
        elif 'gb' in encoding.lower():
            encoding = 'gb18030'
        else:
            encoding = 'utf-8'
        code = raw_data.decode(encoding, errors='replace')
    except Exception as e:
        print(f"Failed to read {file_path}: {e}")
        return None
    tree = parser.parse(bytes(code, 'utf-8'))
    def find_function_node(node):
        if node.type == "function_definition":
            start_line = node.start_point[0] + 1
            end_line = node.end_point[0] + 1
            if start_line <= line_number <= end_line:
                func_name_node = node.child_by_field_name("declarator")
                if func_name_node:
                    name = func_name_node.text.decode("utf-8").split("(")[0].strip().split()[-1]
                    func_code = code[node.start_byte: node.end_byte]
                    return name, func_code
        for child in node.children:
            res = find_function_node(child)
            if res:
                return res
        return None
    return find_function_node(tree.root_node)

def get_fallback_context(file_path: str, line_number: int, window: int = 10) -> str:
    try:
        with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
            lines = f.readlines()
    except Exception:
        return f"// Failed to read file around line {line_number}"
    start = max(0, line_number - 1 - window)
    end = min(len(lines), line_number - 1 + window + 1)
    snippet = "".join(lines[start:end])
    return f"// Context around line {line_number} (non-function):\n{snippet}"

def retrieve_knowledge(query_text: str, top_k: int = 1) -> List[Dict]:
    index, kb_meta = _load_knowledge_base()
    emb = embed_text(query_text)
    D, I = index.search(emb, top_k)
    results = []
    for idx in I[0]:
        if 0 <= idx < len(kb_meta):
            results.append(kb_meta[idx])
    return results

def retrieve_related_summaries(main_func_info: Dict, max_related: int = 3) -> Dict[str, str]:
    related = {"called_by": [], "calls": []}
    def fetch_summary(func_name, file_path):
        query = f"{func_name} in {file_path}"
        hits = retrieve_knowledge(query, top_k=1)
        if hits:
            hit = hits[0]
            return f"{hit['function_name']} in {hit['file_path']}: {hit.get('summary', 'No summary')}"
        return f"{func_name} in {file_path}: Summary not found"
    for item in main_func_info.get("called_by", [])[:max_related]:
        if isinstance(item, dict) and "function" in item and "file" in item:
            related["called_by"].append(fetch_summary(item["function"], item["file"]))
    for item in main_func_info.get("calls", [])[:max_related]:
        if isinstance(item, dict) and "function" in item and "file" in item:
            related["calls"].append(fetch_summary(item["function"], item["file"]))
    return related

def get_urgency_score_for_A(defect_desc: str, reason: str) -> int:
    """为高风险缺陷计算紧急修复分数"""
    prompt = f"""你是一位资深 C/C++ 静态分析专家和航天嵌入式系统安全工程师。
以下是一个已被判定为高风险缺陷的问题，请根据其**严重性、可触发概率、后果影响（如崩溃、数据损坏、安全漏洞等）**，
给出一个 0 到 100 的紧急修复分数（urgency score）：
- 100 分：必然触发、导致系统崩溃或严重安全漏洞（如缓冲区溢出、除零、空指针解引用在关键路径）
- 70～90 分：高概率触发，影响核心功能
- 40～60 分：可能触发，影响次要功能
- 0～30 分：极难触发，或后果轻微

缺陷描述：
{defect_desc}

分析理由：
{reason}

请仅输出一个整数（0 到 100 之间），不要包含任何其他文字。"""
    try:
        response = client.chat.completions.create(
            model=LLM_MODEL_NAME,
            messages=[{"role": "user", "content": prompt}],
            temperature=0.0,
            max_tokens=5
        )
        answer = response.choices[0].message.content.strip()
        match = re.search(r'\d+', answer)
        if match:
            score = int(match.group())
            score = max(0, min(100, score))
            return score
        else:
            print(f"无法从模型响应中提取数字，使用默认值 50。原始响应: '{answer}'")
            return 50
    except Exception as e:
        print(f"获取 urgency_score 出错，使用默认值 50: {e}")
        return 50

def retrieve_relevant_functions(query_text: str, top_k: int = 5) -> List[str]:
    """根据查询文本，从知识库中检索最相关的函数 context_text 列表"""
    try:
        index, kb_meta = _load_knowledge_base()
        emb = embed_text(query_text)
        D, I = index.search(emb, top_k)
        contexts = []
        for idx in I[0]:
            if 0 <= idx < len(kb_meta):
                meta = kb_meta[idx]
                context = (
                    f"【实体类型】函数\n"
                    f"【函数名】{meta.get('function_name', 'N/A')}\n"
                    f"【所在文件】{os.path.basename(meta.get('file_path', 'N/A'))}\n"
                    f"【功能摘要】{meta.get('summary', '无')}\n"
                    f"【调用的函数】{', '.join(meta.get('calls', [])) if meta.get('calls') else '无'}\n"
                    f"【被以下函数调用】{', '.join(meta.get('called_by', [])) if meta.get('called_by') else '无'}\n"
                    f"【包含的头文件】{', '.join(meta.get('includes', [])) if meta.get('includes') else '无'}\n"
                    f"{'-' * 40}"
                )
                contexts.append(context)
        return contexts
    except Exception as e:
        print(f"知识库检索出错: {e}")
        return []

def get_functions_to_modify_with_knowledge(
        defect_desc: str,
        reason: str,
        file_path: str,
        line_number: int
) -> List[str]:
    """利用知识库检索上下文，让大模型返回需修改的函数列表"""
    query_text = f"{defect_desc}\n{reason}\n文件: {os.path.basename(file_path)}"
    retrieved_contexts = retrieve_relevant_functions(query_text, top_k=5)
    knowledge_context_block = "\n".join(retrieved_contexts) if retrieved_contexts else "无相关函数知识库条目。"
    prompt = f"""你是一位资深 C/C++ 航天嵌入式软件工程师。请根据以下缺陷信息和**知识库检索到的函数上下文**，分析此缺陷影响了哪些功能实现和组件的工作。

【缺陷描述】
{defect_desc}

【分析理由】
{reason}

【缺陷位置】
文件: {file_path}
行号: {line_number}

【知识库检索结果】
{knowledge_context_block}

请严格按以下格式输出（示例）：
ObtSunVecI
InitAttEnv

（若无，直接返回空）"""
    try:
        response = client.chat.completions.create(
            model=LLM_MODEL_NAME,
            messages=[{"role": "user", "content": prompt}],
            temperature=0.0,
            max_tokens=200
        )
        answer = response.choices[0].message.content.strip()
        if not answer or any(w in answer for w in ["无", "空", "没有", "未找到"]):
            return []
        functions = []
        for line in answer.splitlines():
            func = line.strip()
            if func and (func[0].isalpha() or func[0] == '_'):
                func = re.split(r'[^a-zA-Z0-9_]', func)[0]
                if func:
                    functions.append(func)
        return functions
    except Exception as e:
        print(f"获取 functions_to_modify_with_knowledge 出错: {e}")
        return []

def build_enhanced_prompt(
        defect_desc: str,
        file_path: str,
        line_number: int,
        main_context: str,
        main_knowledge: Optional[Dict],
        related_summaries: Optional[Dict],
        is_in_function: bool,
) -> str:
    prompt = f"""你是一名资深 航天软件测试专家、C语言测试专家，请根据以下信息判断给出的缺陷告警是否为真实缺陷（True Positive），注意只关注当下源代码和知识库中的信息以确认该缺陷是否是真正的逻辑硬伤，对于可能导致潜在问题的非直接缺陷以及单纯的编码不规范问题进行忽视。

【缺陷描述】
{defect_desc}

【缺陷位置】
文件：{file_path}
行号：{line_number}

【代码上下文】
{main_context}
"""
    if is_in_function and main_knowledge:
        prompt += f"""【主函数知识库信息】
- 函数名: {main_knowledge.get('function_name', 'N/A')}
- 文件: {main_knowledge.get('file_path', 'N/A')}
- 功能摘要: {main_knowledge.get('summary', 'N/A')}
- 包含头文件: {', '.join(main_knowledge.get('includes', [])) or 'None'}
"""
        if related_summaries:
            if related_summaries["called_by"]:
                prompt += "\n【调用此函数的关键函数摘要】\n" + "\n".join(related_summaries["called_by"])
            if related_summaries["calls"]:
                prompt += "\n\n【此函数调用的关键函数摘要】\n" + "\n".join(related_summaries["calls"])
    else:
        prompt += "注意：该缺陷位于非函数上下文（如全局变量、宏定义等），请谨慎判断。\n"
    prompt += """

请严格按以下 JSON 格式输出，不要包含其他内容：
{
  "is_real_defect": true 或 false,
  "reason": "简要说明原因",
  "risk_zone": ["影响域分析"],
  "suggestion": "修复建议"
}
"""
    return prompt

def is_pure_style_issue(defect_desc: str) -> bool:
    """快速判断缺陷描述是否仅为编码风格/规范问题（非逻辑缺陷）"""
    style_prompt = f"""你是一名资深 C 语言航天软件测试专家。请判断以下静态分析工具报告的缺陷描述是否**仅涉及编码风格、格式、命名规范等非功能性问题**，而不涉及任何逻辑错误、内存安全、数值计算、状态机、控制流等实质性风险。

【缺陷描述】
{defect_desc}

请严格按以下 JSON 格式输出：
{{ "is_pure_style": true 或 false, "reason": "简要说明" }}"""
    try:
        completion = client.chat.completions.create(
            model=LLM_MODEL_NAME,
            messages=[{"role": "user", "content": style_prompt}],
            temperature=0.0,
            max_tokens=128
        )
        content = completion.choices[0].message.content
        json_match = re.search(r"```(?:json)?\s*({.*?})\s*```", content, re.DOTALL)
        if json_match:
            result = json.loads(json_match.group(1))
        else:
            result = json.loads(content)
        return bool(result.get("is_pure_style", False))
    except Exception as e:
        print(f"Style filter LLM call failed: {e}. Treating as NOT pure style (proceed to full analysis).")
        return False

def analyze_defect(defect_desc: str, file_path: str, line_number: int) -> Dict:
    """分析单个缺陷的核心函数"""
    func_info = get_function_context(file_path, line_number)
    if func_info:
        func_name, func_code = func_info
        main_context = f"// Function: {func_name}\n{func_code}"
        is_in_function = True
        query = f"{func_name} in {file_path}"
        main_knowledge_hits = retrieve_knowledge(query, top_k=1)
        main_knowledge = main_knowledge_hits[0] if main_knowledge_hits else None
        related_summaries = retrieve_related_summaries(main_knowledge) if main_knowledge else None
        print("当前缺陷 " + defect_desc + " 缺陷位置 " + file_path)
        print("正在查阅核心函数：" + func_name + "， 及相关函数摘要")
    else:
        main_context = get_fallback_context(file_path, line_number)
        is_in_function = False
        main_knowledge = None
        related_summaries = None
    prompt = build_enhanced_prompt(
        defect_desc=defect_desc,
        file_path=file_path,
        line_number=line_number,
        main_context=main_context,
        main_knowledge=main_knowledge,
        related_summaries=related_summaries,
        is_in_function=is_in_function
    )
    try:
        completion = client.chat.completions.create(
            model=LLM_MODEL_NAME,
            messages=[{"role": "user", "content": prompt}],
            temperature=0.0,
            max_tokens=512
        )
        content = completion.choices[0].message.content
        json_match = re.search(r"```(?:json)?\s*({.*?})\s*```", content, re.DOTALL)
        if json_match:
            result = json.loads(json_match.group(1))
        else:
            result = json.loads(content)
        if result.get("is_real_defect") is True:
            urgency_score = get_urgency_score_for_A(defect_desc, result.get("reason", ""))
            result["urgency_score"] = urgency_score
            if urgency_score > 70:
                affected_functions = get_functions_to_modify_with_knowledge(
                    defect_desc, result.get("reason", ""), file_path, line_number
                )
                result["affected_functions"] = affected_functions
            else:
                result["affected_functions"] = []
        else:
            result["urgency_score"] = 0
            result["affected_functions"] = []
        return result
    except Exception as e:
        return {
            "is_real_defect": None,
            "reason": f"LLM call failed: {str(e)}",
            "risk_points": [],
            "suggestion": "大模型调用失败",
            "urgency_score": 0,
            "affected_functions": []
        }

def process_defects_from_excel(input_xlsx: str, output_json: str, project_path: str = None) -> None:
    """主处理函数：从Excel读取问题单，分析并输出JSON结果"""
    global Project_path
    if project_path:
        Project_path = project_path
    print(f"Loading all sheets from {input_xlsx} (excluding the first two)...")
    all_sheets = pd.read_excel(input_xlsx, sheet_name=None, engine="openpyxl")
    sheet_names = list(all_sheets.keys())
    target_sheet_names = sheet_names[2:] if len(sheet_names) > 2 else []
    if not target_sheet_names:
        print("警告：输入文件的工作表数量少于3个，没有需要处理的工作表。")
        with open(output_json, "w", encoding="utf-8") as f:
            json.dump([], f, indent=2, ensure_ascii=False)
        print(f"Completed! 空结果已保存至 {output_json}")
        return
    results = []
    total_defects_processed = 0
    for sheet_name in target_sheet_names:
        df = all_sheets[sheet_name]
        print(f"\n--- 开始处理工作表: '{sheet_name}' (共 {len(df)} 行) ---")
        if df.shape[1] < 13:
            print(f"  警告：工作表 '{sheet_name}' 的列数少于13，跳过。")
            continue
        for idx, row in df.iterrows():
            try:
                raw_path_str = str(row.iloc[10]).strip()
                if not raw_path_str or pd.isna(row.iloc[10]):
                    print(f"  Skip row {idx + 2} in sheet '{sheet_name}': missing file path")
                    continue
                is_absolute = False
                if re.match(r'^[a-zA-Z]:[\\/]', raw_path_str) or raw_path_str.startswith('\\\\') or raw_path_str.startswith('/'):
                    is_absolute = True
                if is_absolute:
                    file_path = raw_path_str
                else:
                    file_path = os.path.join(Project_path, raw_path_str)
                line_str = row.iloc[11]
                defect_desc = row.iloc[12]
                if pd.isna(file_path) or pd.isna(defect_desc):
                    print(f"  Skip row {idx + 2} in sheet '{sheet_name}': missing file or description")
                    continue
                file_path = str(file_path).strip()
                defect_desc = str(defect_desc).strip()
                try:
                    line_number = int(float(line_str))
                except (ValueError, TypeError):
                    print(f"  Invalid line number at row {idx + 2} in sheet '{sheet_name}': {line_str}")
                    continue
                print(f"  Processing row {idx + 2} in sheet '{sheet_name}': {file_path}:{line_number}")
                if is_pure_style_issue(defect_desc):
                    print(f"    → Skipped (pure style issue): {defect_desc[:60]}...")
                    analysis = {
                        "is_real_defect": False,
                        "reason": "该问题仅为编码风格或规范问题，无实际逻辑风险。",
                        "risk_points": [],
                        "suggestion": "可忽略此类静态分析告警，或通过代码格式化工具统一处理。",
                        "urgency_score": 0,
                        "affected_functions": []
                    }
                    print(f"该问题仅为编码风格或规范问题，无实际逻辑风险。")
                else:
                    analysis = analyze_defect(defect_desc, file_path, line_number)
                results.append({
                    "sheet_name": sheet_name,
                    "row_index": idx + 2,
                    "file_path": file_path,
                    "line_number": line_number,
                    "defect_description": defect_desc,
                    "analysis_result": analysis
                })
                total_defects_processed += 1
                time.sleep(0.1)
            except Exception as e:
                print(f"  Error processing row {idx + 2} in sheet '{sheet_name}': {e}")
                results.append({
                    "sheet_name": sheet_name,
                    "row_index": idx + 2,
                    "file_path": str(row.iloc[10]) if not pd.isna(row.iloc[10]) else "",
                    "line_number": str(row.iloc[11]) if not pd.isna(row.iloc[11]) else "",
                    "defect_description": str(row.iloc[12]) if not pd.isna(row.iloc[12]) else "",
                    "analysis_result": {
                        "is_real_defect": None,
                        "reason": f"Unexpected error: {str(e)}",
                        "risk_points": [],
                        "suggestion": "处理过程中发生异常",
                        "urgency_score": 0,
                        "affected_functions": []
                    }
                })
    with open(output_json, "w", encoding="utf-8") as f:
        json.dump(results, f, indent=2, ensure_ascii=False)
    print(f"\n处理完成！共处理了 {len(target_sheet_names)} 个工作表，分析了 {total_defects_processed} 个缺陷。")
    print(f"结果已保存至 {output_json}")
    true_positives = sum(1 for r in results if r["analysis_result"].get("is_real_defect") is True)
    false_positives = sum(1 for r in results if r["analysis_result"].get("is_real_defect") is False)
    unknown = len(results) - true_positives - false_positives
    high_urgency = sum(1 for r in results if r["analysis_result"].get("urgency_score", 0) >= 70)
    medium_urgency = sum(1 for r in results if 40 <= r["analysis_result"].get("urgency_score", 0) < 70)
    low_urgency = sum(1 for r in results if 0 < r["analysis_result"].get("urgency_score", 0) < 40)
    total_affected_functions = sum(len(r["analysis_result"].get("affected_functions", [])) for r in results)
    defects_with_affected_functions = sum(1 for r in results if r["analysis_result"].get("affected_functions"))
    print(f"\n汇总统计：")
    print(f"  真实缺陷: {true_positives} 条")
    print(f"  误报: {false_positives} 条")
    print(f"  未知/错误: {unknown} 条")
    print(f"  紧急程度分布：高紧急({high_urgency}条) 中紧急({medium_urgency}条) 低紧急({low_urgency}条)")
    print(f"  影响域分析：{defects_with_affected_functions} 个缺陷影响了 {total_affected_functions} 个函数")


def run_issue_filtering(input_xlsx_path, output_json_path,
                        project_path_override=None,
                        kb_vector_path=None, kb_metadata_path=None):
    """问题单过滤主函数

    Args:
        input_xlsx_path: 输入Excel文件路径
        output_json_path: 输出JSON文件路径
        project_path_override: 项目路径覆盖
        kb_vector_path: 知识库向量数据库路径
        kb_metadata_path: 知识库元数据文件路径
    """
    global KB_INDEX_PATH, KB_META_PATH

    # 设置知识库路径
    if kb_vector_path:
        KB_INDEX_PATH = kb_vector_path
        print(f"[DEBUG] 设置KB_INDEX_PATH: {KB_INDEX_PATH}")

    if kb_metadata_path:
        KB_META_PATH = kb_metadata_path
        print(f"[DEBUG] 设置KB_META_PATH: {KB_META_PATH}")

    # 验证知识库路径
    if not KB_INDEX_PATH or not KB_INDEX_PATH.strip():
        logger.error("知识库向量数据库路径为空！")
        print("错误: 知识库向量数据库路径为空")
        return False

    if not KB_META_PATH or not KB_META_PATH.strip():
        logger.error("知识库元数据文件路径为空！")
        print("错误: 知识库元数据文件路径为空")
        return False

    if not os.path.exists(KB_INDEX_PATH):
        logger.error(f"向量数据库文件不存在: {KB_INDEX_PATH}")
        print(f"错误: 向量数据库文件不存在: {KB_INDEX_PATH}")
        return False

    if not os.path.exists(KB_META_PATH):
        logger.error(f"元数据文件不存在: {KB_META_PATH}")
        print(f"错误: 元数据文件不存在: {KB_META_PATH}")
        return False

    print(f"知识库配置验证通过:")
    print(f"  - 向量数据库: {KB_INDEX_PATH}")
    print(f"  - 元数据文件: {KB_META_PATH}")
    try:
        # 1. 检查输入文件
        if not os.path.exists(input_xlsx_path):
            print(f"错误: 输入文件不存在: {input_xlsx_path}")
            return False
        # 2. 加载知识库 (惰性加载，会在首次使用时触发)
        _load_knowledge_base()
        # 3. 处理问题单
        process_defects_from_excel(input_xlsx_path, output_json_path, project_path_override)
        return True
    except Exception as e:
        print(f"问题单过滤流程执行失败: {e}")
        import traceback
        traceback.print_exc()
        return False