import json
import os
import re

from dotenv import load_dotenv
from neo4j import AsyncGraphDatabase
from typing import Any, Dict, List, Optional

load_dotenv()

NEO4J_CONFIG = {
    "uri": os.getenv("NEO4J_URI", "bolt://192.168.0.46:57687"),
    "user": os.getenv("NEO4J_USER", "neo4j"),
    "password": os.getenv("NEO4J_PASSWORD","zdht123@"),
}


def _string_similarity(a: str, b: str) -> float:
    """
    简单字符串相似度：综合长度差、包含关系和公共字符比例。
    0~1 之间，越大越相似。
    """
    if not a or not b:
        return 0.0
    a = str(a).strip().lower()
    b = str(b).strip().lower()
    if not a or not b:
        return 0.0
    if a == b:
        return 1.0
    if a in b or b in a:
        # 明确包含关系，给较高分
        return 0.9

    # 公共字符集比例
    set_a, set_b = set(a), set(b)
    inter = len(set_a & set_b)
    union = len(set_a | set_b) or 1
    jaccard = inter / union

    # 长度差惩罚
    len_diff = abs(len(a) - len(b))
    len_penalty = max(0.0, 1.0 - len_diff / max(len(a), len(b), 1))

    return 0.5 * jaccard + 0.5 * len_penalty


def _pick_best_by_name(candidates: List[Dict[str, Any]], target_name: str) -> Optional[Dict[str, Any]]:
    """
    在候选节点中，根据名称相似度选择最优节点。
    约定每个节点为：{id, labels, props, display_name}
    """
    if not candidates or not target_name:
        return None

    best = None
    best_score = 0.0
    for item in candidates:
        name = (item.get("display_name") or "").strip()
        if not name:
            # 尝试从属性中推断名称
            props = item.get("props") or {}
            name = props.get("name") or props.get("名称") or props.get("设备名称") or ""
        score = _string_similarity(target_name, name)
        if score > best_score:
            best_score = score
            best = item

    # 设置一个最低阈值，过于不相似则认为未命中
    return best if best_score >= 0.3 else None


def _extract_mt_codes(text: str) -> List[str]:
    """
    从文本中抽取形如 MT007 的维修项目编号（MT + 3位数字）。
    """
    if not text:
        return []
    codes = re.findall(r"\bMT\d{3}\b", str(text))
    # 去重但保持顺序
    seen = set()
    ordered: List[str] = []
    for c in codes:
        if c not in seen:
            seen.add(c)
            ordered.append(c)
    return ordered


def _node_record_to_dict(record: Any) -> Dict[str, Any]:
    """
    将 Cypher 返回的 id/labels/props 结构统一为字典。
    约定 Cypher 中使用：
        RETURN id(n) AS id, labels(n) AS labels, properties(n) AS props, <可选的 display_name>
    """
    # neo4j.Record 提供 data() 方法；若不是 Record，则假定本身为 dict
    if hasattr(record, "data"):
        data = record.data()
    else:
        data = record or {}
    return {
        "id": data.get("id"),
        "labels": data.get("labels") or [],
        "props": data.get("props") or {},
        "display_name": data.get("display_name")
        if "display_name" in data
        else None,
    }


def _to_graph_display_node(node_dict: Dict[str, Any]) -> Dict[str, Any]:
    """将内部节点格式转为 graph_search 展示用节点：id、name、label。"""
    nid = node_dict.get("id")
    props = node_dict.get("props") or {}
    name = (
        node_dict.get("display_name")
        or props.get("名称")
        or props.get("name")
        or str(nid)
    )
    labels = node_dict.get("labels") or []
    label = labels[0] if labels else ""
    return {"id": nid, "name": name, "label": label}


def _build_fault_graph_display(
    ship_node: Optional[Dict[str, Any]],
    device_node: Optional[Dict[str, Any]],
    fault_mode_node: Optional[Dict[str, Any]],
    repair_projects: List[Dict[str, Any]],
) -> tuple:
    """
    构建与 graph_search 一致的图谱展示结构：nodes 与 links。
    返回 (nodes: list, links: list)。
    关系：舰艇-包含->设备，设备-存在->故障模式，设备-维修时使用->维修项目。
    """
    nodes: List[Dict[str, Any]] = []
    links: List[Dict[str, Any]] = []
    seen_ids = set()

    def add_node(nd: Optional[Dict[str, Any]]):
        if not nd or nd.get("id") in seen_ids:
            return
        seen_ids.add(nd["id"])
        nodes.append(_to_graph_display_node(nd))

    def add_link(source_id: Any, target_id: Any, rel_type: str = ""):
        links.append({"source": source_id, "target": target_id, "type": rel_type})

    if ship_node:
        add_node(ship_node)
    if device_node:
        add_node(device_node)
        if ship_node:
            add_link(ship_node["id"], device_node["id"], "包含")
    if fault_mode_node:
        add_node(fault_mode_node)
        if device_node:
            add_link(device_node["id"], fault_mode_node["id"], "存在")
    for p in repair_projects or []:
        add_node(p)
        if device_node:
            add_link(device_node["id"], p["id"], "维修时使用")

    return nodes, links


def _build_fault_graph_results_text(
    ship_node: Optional[Dict[str, Any]],
    device_node: Optional[Dict[str, Any]],
    fault_mode_node: Optional[Dict[str, Any]],
    fault_reason: str,
    repair_plan: str,
    mt_codes: List[str],
    repair_projects: List[Dict[str, Any]],
    ship_number: str,
    device_name: str,
    fault_symptom: str,
) -> str:
    """生成与 graph_rag_search 类似的一段话，供展示与下游使用。"""
    ship_name = ""
    if ship_node:
        ship_name = (ship_node.get("display_name") or (ship_node.get("props") or {}).get("名称") or "").strip()
    device_name_d = ""
    if device_node:
        device_name_d = (device_node.get("display_name") or (device_node.get("props") or {}).get("名称") or "").strip()
    fault_name = ""
    if fault_mode_node:
        fault_name = (fault_mode_node.get("display_name") or (fault_mode_node.get("props") or {}).get("名称") or "").strip()

    parts = [
        f"根据舷号「{ship_number}」、设备「{device_name}」、故障现象「{fault_symptom}」进行图谱检索：",
        f"定位到舰艇「{ship_name or '未知'}」、设备节点「{device_name_d or '未知'}」、故障模式「{fault_name or '未知'}」。",
    ]
    if fault_reason:
        parts.append(f"故障原因：{fault_reason}")
    if repair_plan:
        parts.append(f"维修方案摘要：{repair_plan[:500]}{'…' if len(repair_plan) > 500 else ''}")
    if mt_codes:
        parts.append(f"涉及维修项目编号：{'、'.join(mt_codes)}。")
    if repair_projects:
        proj_names = []
        for p in repair_projects:
            nm = (p.get("display_name") or (p.get("props") or {}).get("名称") or (p.get("props") or {}).get("维修项目编号") or "").strip()
            if nm:
                proj_names.append(nm)
        if proj_names:
            parts.append(f"匹配到的维修项目：{'、'.join(proj_names)}。")
    return "\n".join(parts)

async def fault_graph_reason_and_projects(
    ship_number: str,
    device_name: str,
    fault_symptom: str,
) -> Dict[str, Any]:
    """
    图谱检索工具
    """

    if not ship_number or not str(ship_number).strip():
        return {"success": False, "error": "舷号不能为空"}
    if not device_name or not str(device_name).strip():
        return {"success": False, "error": "设备名称不能为空"}
    if not fault_symptom or not str(fault_symptom).strip():
        return {"success": False, "error": "故障现象不能为空"}

    uri = NEO4J_CONFIG.get("uri")
    user = NEO4J_CONFIG.get("user")
    password = NEO4J_CONFIG.get("password")
    if not uri or not user or not password:
        return {"success": False, "error": "Neo4j 配置不完整，请检查 NEO4J_CONFIG"}

    try:
        async with AsyncGraphDatabase.driver(uri, auth=(user, password)) as driver:
            async with driver.session() as session:
                ship_key = str(ship_number).strip()
                device_key = str(device_name).strip()

                # 1) 先按舷号定位舰艇节点（限定范围）
                ship_query = """
                MATCH (s:舰艇)
                WHERE s.舷号 IN [$ship_key]
                RETURN DISTINCT
                       id(s) AS id,
                       labels(s) AS labels,
                       properties(s) AS props,
                       coalesce(s.名称, s.name, s.舷号) AS display_name
                LIMIT 10
                """
                ship_records = await session.run(ship_query, ship_key=ship_key)
                ship_candidates = [_node_record_to_dict(r) async for r in ship_records]
                if not ship_candidates:
                    return {"success": False, "error": "在图谱中未找到对应舷号的舰艇节点"}
                ship_node = ship_candidates[0]
                ship_id = ship_node["id"]

                # 2) 在该舰艇范围内定位设备候选节点
                device_query = """
                MATCH (ship:舰艇)
                WHERE id(ship) = $ship_id
                MATCH (ship)-[:包含*0..8]->(n:设备)
                WITH n, coalesce(n.名称, n.name, n.设备名称) AS nm
                WHERE nm IS NOT NULL AND (
                    toLower(nm) CONTAINS toLower($device_key)
                    OR toLower($device_key) CONTAINS toLower(nm)
                )
                RETURN DISTINCT
                       id(n) AS id,
                       labels(n) AS labels,
                       properties(n) AS props,
                       nm AS display_name
                LIMIT 50
                """
                dev_records = await session.run(
                    device_query, ship_id=ship_id, device_key=device_key
                )
                device_candidates = [_node_record_to_dict(r) async for r in dev_records]
                device_node = _pick_best_by_name(device_candidates, device_key)
                if not device_node:
                    return {"success": False, "error": "在该舰艇范围内未找到匹配的设备节点"}
                device_id = device_node["id"]

                # 3) 设备 -> （包含层级中的真实设备）-> 维修工作 -> 故障模式
                #    路径示例：
                #    (start_e:设备 {名称:'主发动机'})-[:包含*0..8]-(real_e:设备)
                #       -[:发生故障使用维修工作]-(w:维修工作)-[:包含故障]-(fm:故障模式)
                fault_mode_query = """
                MATCH (start_e:设备)
                WHERE id(start_e) = $device_id
                MATCH (start_e)-[:包含*0..8]-(real_e:设备)
                MATCH (real_e)-[:发生故障使用维修工作]-(w:维修工作)-[:包含故障]-(fm:故障模式)
                RETURN DISTINCT
                       id(fm) AS id,
                       labels(fm) AS labels,
                       properties(fm) AS props,
                       fm.名称 AS display_name
                """
                fm_records = await session.run(fault_mode_query, device_id=device_id)
                fault_modes = [_node_record_to_dict(r) async for r in fm_records]
                if not fault_modes:
                    return {"success": False, "error": "在该设备节点下未找到故障模式节点"}

                fault_mode_node = _pick_best_by_name(fault_modes, fault_symptom)
                if not fault_mode_node:
                    return {"success": False, "error": "未能在故障模式节点中找到与故障现象足够匹配的节点"}

                fault_mode_id = fault_mode_node.get("id")
                if fault_mode_id is None:
                    return {"success": False, "error": "故障模式节点缺少 id，无法继续检索维修项目"}

                fm_props = fault_mode_node.get("props") or {}
                fault_reason = fm_props.get("故障原因") or ""
                repair_plan = fm_props.get("维修方案") or ""
                mt_codes = _extract_mt_codes(repair_plan)

                # 4) 故障模式 -> 维修项目（维修方案）
                repair_project_query = """
                MATCH (fm:故障模式)
                WHERE id(fm) = $fault_mode_id
                OPTIONAL MATCH (fm)<-[:修理]-(p:维修项目)
                RETURN DISTINCT
                       id(p) AS id,
                       labels(p) AS labels,
                       properties(p) AS props,
                       p.名称 AS display_name
                """
                rp_records = await session.run(
                    repair_project_query, fault_mode_id=fault_mode_id
                )
                all_projects = [_node_record_to_dict(r) async for r in rp_records]

                repair_projects: List[Dict[str, Any]] = []
                if mt_codes:
                    for proj in all_projects:
                        props = proj.get("props") or {}
                        proj_code = (props.get("维修项目编号") or "").strip()
                        if proj_code and (
                            proj_code in mt_codes or any(c in proj_code for c in mt_codes)
                        ):
                            repair_projects.append(proj)
                        elif not proj_code:
                            values_text = " ".join(
                                [str(v) for v in props.values() if v is not None]
                            )
                            if any(code in values_text for code in mt_codes):
                                repair_projects.append(proj)
                else:
                    repair_projects = all_projects

                repair_ids = [
                    p.get("id") for p in repair_projects if p.get("id") is not None
                ]

                exclude_keys = {
                    "last_updated",
                    "created_at",
                    "knowledge_source",
                    "fulltext",
                    "embedding",
                }

                # 4) nodes：包含路径上的设备 / 维修工作 / 故障模式 / 维修项目
                graph_nodes_query = """
                MATCH (ship:舰艇)
                WHERE id(ship) = $ship_id
                MATCH (start_e:设备)
                WHERE id(start_e) = $device_id
                MATCH path0 = (ship)-[:包含*0..8]->(start_e)
                MATCH path1 = (start_e)-[:包含*0..8]-(real_e:设备)
                MATCH path2 = (real_e)-[:发生故障使用维修工作]-(w:维修工作)-[:包含故障]-(fm:故障模式)
                WHERE id(fm) = $fault_mode_id
                OPTIONAL MATCH (fm)<-[:修理]-(rp:维修项目)
                WHERE id(rp) IN $repair_ids
                WITH path0, path1, path2, collect(DISTINCT rp) AS rps
                WITH nodes(path0) + nodes(path1) + nodes(path2) + rps AS ns
                UNWIND ns AS n
                WITH DISTINCT n
                WHERE n IS NOT NULL
                RETURN elementId(n) AS id,
                       labels(n) AS labels,
                       coalesce(n.名称, n.name) AS name,
                       properties(n) AS props
                """
                node_records = await session.run(
                    graph_nodes_query,
                    ship_id=ship_id,
                    device_id=device_id,
                    fault_mode_id=fault_mode_id,
                    repair_ids=repair_ids or [-1],
                )

                nodes_g: List[Dict[str, Any]] = []
                async for r in node_records:
                    data_r = r.data()
                    nid = data_r.get("id")
                    if nid is None:
                        continue
                    labels = data_r.get("labels") or []
                    name = data_r.get("name") or str(nid)
                    props = data_r.get("props") or {}
                    props_filtered = {
                        k: v for k, v in props.items() if k not in exclude_keys
                    }
                    nodes_g.append(
                        {
                            "id": nid,
                            "name": name,
                            "labels": labels,
                            "properties": props_filtered,
                        }
                    )

                # 5) links：包含 / 发生故障使用维修工作 / 包含故障 / 修理
                graph_links_query = """
                MATCH (ship:舰艇)
                WHERE id(ship) = $ship_id
                MATCH (start_e:设备)
                WHERE id(start_e) = $device_id
                MATCH path0 = (ship)-[:包含*0..8]->(start_e)
                MATCH path1 = (start_e)-[:包含*0..8]-(real_e:设备)
                MATCH path2 = (real_e)-[:发生故障使用维修工作]-(w:维修工作)-[:包含故障]-(fm:故障模式)
                WHERE id(fm) = $fault_mode_id
                WITH relationships(path0) + relationships(path1) + relationships(path2) AS rs
                UNWIND rs AS r
                RETURN elementId(r) AS id,
                       type(r) AS label,
                       elementId(startNode(r)) AS source,
                       elementId(endNode(r)) AS target,
                       properties(r) AS props
                UNION
                MATCH (fm:故障模式)
                WHERE id(fm) = $fault_mode_id
                MATCH (fm)<-[r:修理]-(rp:维修项目)
                WHERE id(rp) IN $repair_ids
                RETURN elementId(r) AS id,
                       type(r) AS label,
                       elementId(startNode(r)) AS source,
                       elementId(endNode(r)) AS target,
                       properties(r) AS props
                """
                link_records = await session.run(
                    graph_links_query,
                    ship_id=ship_id,
                    device_id=device_id,
                    fault_mode_id=fault_mode_id,
                    repair_ids=repair_ids or [-1],
                )

                links_g: List[Dict[str, Any]] = []
                async for r in link_records:
                    data_r = r.data()
                    src = data_r.get("source")
                    tgt = data_r.get("target")
                    if src is None or tgt is None:
                        continue
                    rel_props = data_r.get("props") or {}
                    rel_props_filtered = {
                        k: v for k, v in rel_props.items() if k not in exclude_keys
                    }
                    links_g.append(
                        {
                            "id": data_r.get("id"),
                            "label": data_r.get("label", ""),
                            "source": src,
                            "target": tgt,
                            "properties": rel_props_filtered,
                        }
                    )

                node_ids = {n["id"] for n in nodes_g}
                links_g = [
                    l
                    for l in links_g
                    if l.get("source") in node_ids and l.get("target") in node_ids
                ]

                repair_plans_data: List[Dict[str, Any]] = []
                for p in repair_projects:
                    props = p.get("props") or {}
                    filtered = {k: v for k, v in props.items() if k not in exclude_keys}
                    if filtered:
                        repair_plans_data.append(filtered)

                results_obj = {"fault_reason": fault_reason, "repair_plans": repair_plans_data}
                results_text = json.dumps(results_obj, ensure_ascii=False)

                graph_item = {"nodes": nodes_g, "links": links_g, "results": results_text}
                return {"success": True, "xxxx.graph": [graph_item]}

    except Exception as e:
        return {"success": False, "error": f"Neo4j 查询失败: {str(e)}"}
