"""把 nodes/ 下的节点串成一张 LangGraph 图

    START → 语义转写节点 → 中医意图识别节点
              ├─ 非中医 ─→ 直接回答节点 ─────────────────────────→ END
              └─ 中医  ─→ 实体抽取节点 → 实体匹配节点 → cypher生成节点 ←──┐
                              → cypher检查节点                          │
                                   ├─ 全部合法 ─→ cypher执行节点         │
                                   │                  → 图谱答案节点 → END│
                                   ├─ 不合法且未超 3 次 ─→ 重新生成 ──────┘
                                   └─ 不合法且已 3 次 ─→ 直接回答节点 → END

语义转写排在最前面，是因为它后面每一个节点都依赖它的产出：多轮对话里"那第二呢？"
这种话单看一句没有任何信息，意图识别会判成非中医、实体抽取一个词也抽不出来。转写节点
结合 history_messages 把它补成"珠穆朗玛峰之后第二高的山峰是什么？"，下游才有东西可用。

所以 `input` 只有转写节点自己读（它的输入就是原话），其余节点一律读
`input_semantic_trans`。两者的区别是"用户打了什么"和"用户想问什么"。

两个条件边，两处兜底，一个环：

- 意图识别之后：跟中医无关的提问查图谱只会白跑一圈，直接交给大模型。
- cypher 检查之后：不合法先回到生成节点重写，最多生成 3 次（首次 + 2 次重试）；
  还是不行才退回大模型。计数在 `cypher_retry_count`，生成节点里自增 —— 路由函数
  只读 state，没法自己记账，所以计数必须由节点写。
- `直接回答节点` 被两条兜底路径共用（LangGraph 允许多个前驱指向同一节点）。

重试时会把上一轮不合法的语句回喂给模型（见生成节点里的 `retry_block`），否则模型
看到的输入和上次一模一样，大概率原样再生成一遍。

还有一处兜底没有做成条件边：cypher 生成为空（一个实体都没匹配上）。这种情况
`cypher_query` 是 `[]`，而 `check_cypher_node` 对空列表判 False，于是会走重试
那条边 —— 但生成节点在没有实体时直接返回不调大模型，所以这两次重试是空转、不花钱，
最后由计数刹住落到兜底。省下了为空列表单独写一条分支。

节点名用中文，和 `_000_demo/04_langgraph_demo.py` 保持一致。
"""
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.constants import START
from langgraph.graph import END, StateGraph

from _004_langgraph_more_nodes.agent_state import AgentState
from _004_langgraph_more_nodes.nodes.check_cypher_node import check_cypher_node
from _004_langgraph_more_nodes.nodes.extract_entity_from_user_input_node import (
    extract_entity_from_user_input_node,
)
from _004_langgraph_more_nodes.nodes.generate_neo4j_cypher_node import (
    generate_neo4j_cypher_node,
)
from _004_langgraph_more_nodes.nodes.llm_direct_out_node import llm_direct_out_node
from _004_langgraph_more_nodes.nodes.match_entity_from_neo4j_node import (
    match_entity_from_neo4j_node,
)
from _004_langgraph_more_nodes.nodes.neo4j_answer_generate_node import (
    neo4j_answer_generate_node,
)
from _004_langgraph_more_nodes.nodes.run_cypher_node import run_cypher_node
from _004_langgraph_more_nodes.nodes.semantic_transcription_node import (
    semantic_transcription_node,
)
from _004_langgraph_more_nodes.nodes.zhongyi_intent_node import zhongyi_intent_node

# 节点名集中在这里，路由函数和加边都引用它，避免字符串写错了却要到运行时才报错
SEMANTIC_TRANS = "语义转写节点"
INTENT = "中医意图识别节点"
EXTRACT_ENTITY = "实体抽取节点"
MATCH_ENTITY = "实体匹配节点"
GENERATE_CYPHER = "cypher生成节点"
CHECK_CYPHER = "cypher检查节点"
RUN_CYPHER = "cypher执行节点"
NEO4J_ANSWER = "图谱答案节点"
DIRECT_OUT = "直接回答节点"

# cypher 最多生成 3 次：首次 + 2 次重试。超了就别再烧钱了，退回大模型直接回答。
MAX_CYPHER_RETRY = 3


# ======================
# 路由函数
# ======================
def zhongyi_intent_control(state: AgentState) -> str:
    """意图分流：和中医相关走图谱，否则直接回答"""
    if state.get("is_zhongyi_intent"):
        return EXTRACT_ENTITY
    return DIRECT_OUT


def check_cypher_control(state: AgentState) -> str:
    """cypher 分流，三条出路：

    - 全部合法       -> 去查库
    - 不合法且还有额度 -> 回到生成节点重写（这是图里唯一的环）
    - 不合法且额度用尽 -> 退回大模型直接回答

    环的刹车靠 cypher_retry_count，它在生成节点里自增。只看 is_all_validate_cypher
    的话，模型一直生成不出合法语句就会无限打转。
    """
    if state.get("is_all_validate_cypher"):
        return RUN_CYPHER

    retry_count = state.get("cypher_retry_count") or 0
    if retry_count < MAX_CYPHER_RETRY:
        print(f"cypher 不合法，重新生成（已生成 {retry_count} 次，上限 {MAX_CYPHER_RETRY} 次）")
        return GENERATE_CYPHER

    print(f"cypher 已生成 {retry_count} 次仍不合法，转由大模型直接回答")
    return DIRECT_OUT


# ======================
# 建图
# ======================
def build_langgraph():
    """把节点和边装好并编译，返回可以 invoke 的图"""
    graph_builder = StateGraph(AgentState)

    # 加节点：名字 -> 函数。函数签名都是 (state) -> state，直接传进来即可
    graph_builder.add_node(SEMANTIC_TRANS, semantic_transcription_node)
    graph_builder.add_node(INTENT, zhongyi_intent_node)
    graph_builder.add_node(EXTRACT_ENTITY, extract_entity_from_user_input_node)
    graph_builder.add_node(MATCH_ENTITY, match_entity_from_neo4j_node)
    graph_builder.add_node(GENERATE_CYPHER, generate_neo4j_cypher_node)
    graph_builder.add_node(CHECK_CYPHER, check_cypher_node)
    graph_builder.add_node(RUN_CYPHER, run_cypher_node)
    graph_builder.add_node(NEO4J_ANSWER, neo4j_answer_generate_node)
    graph_builder.add_node(DIRECT_OUT, llm_direct_out_node)

    # 入口：先转写，再判意图。顺序不能反 —— 意图识别读的是转写结果
    graph_builder.add_edge(START, SEMANTIC_TRANS)
    graph_builder.add_edge(SEMANTIC_TRANS, INTENT)

    # 条件边 1：意图识别之后分两路。
    # 第三个参数（路由表）必须给：不给的话 LangGraph 不知道这条边可能通向哪些节点，
    # 画出来的图会只剩 START -> 意图识别 -> END，后面的边全看不到。
    graph_builder.add_conditional_edges(
        INTENT,
        zhongyi_intent_control,
        {EXTRACT_ENTITY: EXTRACT_ENTITY, DIRECT_OUT: DIRECT_OUT},
    )

    # 中医这一路的固定顺序
    graph_builder.add_edge(EXTRACT_ENTITY, MATCH_ENTITY)
    graph_builder.add_edge(MATCH_ENTITY, GENERATE_CYPHER)
    graph_builder.add_edge(GENERATE_CYPHER, CHECK_CYPHER)

    # 条件边 2：cypher 合法去查库，不合法回生成节点重试，重试用尽走兜底。
    # 路由表里的 GENERATE_CYPHER 就是那条回边，图里唯一的环。
    graph_builder.add_conditional_edges(
        CHECK_CYPHER,
        check_cypher_control,
        {
            RUN_CYPHER: RUN_CYPHER,
            GENERATE_CYPHER: GENERATE_CYPHER,
            DIRECT_OUT: DIRECT_OUT,
        },
    )

    graph_builder.add_edge(RUN_CYPHER, NEO4J_ANSWER)

    # 三个出口
    graph_builder.add_edge(NEO4J_ANSWER, END)
    graph_builder.add_edge(DIRECT_OUT, END)
    # 添加记忆
    memory = InMemorySaver()
    # compile 之后才是可执行的图
    return graph_builder.compile(memory)


# 模块级单例：_005_ 的接口层导入这个，不用每个请求重建一次图
graph = build_langgraph()


def ask(question: str) -> dict:
    """问一句，返回跑完的完整 state"""
    return graph.invoke({"input": question})


# ======================
# 测试用例
# ======================
# (提问, 期望走图谱那一路)。每条覆盖一种路径或一类实体：
TEST_CASES = [
    # 图谱路：单个药材 + 属性
    ("人参有什么功效？", True),
    # 图谱路：方剂 → 组成药材，全图最主要的关系
    ("四君子汤由哪些药材组成？", True),
    # 图谱路：出处反查，考的是关系方向没写反
    ("《伤寒论》里记载了哪些方剂？", True),
    # 图谱路：口语症状词，考实体标准化（拉肚子 -> 腹泻）
    ("我最近总是拉肚子，有什么方剂可以调理？", True),
    # 兜底路：非中医提问，意图识别就该分流走
    ("帮我写一段 Python 快排代码", False),
    # 兜底路：像中医但图里没有的实体，匹配不到 -> cypher 为空 -> 落到兜底
    ("阿司匹林的功效是什么？", False),
]


def _summarize(state: dict) -> str:
    """一行讲清这一轮走了哪条路"""
    if state.get("neo4j_answer"):
        rows = sum(len(r) for r in state.get("cypher_results") or [])
        return f"图谱路（{len(state.get('cypher_query') or [])} 条 cypher / {rows} 行）"
    return "兜底路（大模型直接回答）"


if __name__ == "__main__":
    # 图结构：不调用大模型也能看，改完边先跑这段确认接线对不对
    # print("=== 图结构 ===")
    # print(graph.get_graph().draw_ascii())

    print("\n=== 端到端测试 ===")
    wrong = 0
    for question, expect_graph_path in TEST_CASES:
        print(f"\n{'-' * 70}\n提问: {question}")
        state = ask(question)

        went_graph_path = bool(state.get("neo4j_answer"))
        flag = "OK " if went_graph_path == expect_graph_path else "MISS"
        if went_graph_path != expect_graph_path:
            wrong += 1

        print(f"[{flag}] {_summarize(state)}")
        print(f"  意图: {state.get('is_zhongyi_intent')}")
        if state.get("is_zhongyi_intent"):
            matched = {
                key.replace("matched_", ""): value
                for key, value in state.items()
                if key.startswith("matched_") and value
            }
            print(f"  匹配实体: {matched or '<无>'}")
            print(f"  cypher 合法: {state.get('is_all_validate_cypher')}")
            for i, cypher in enumerate(state.get("cypher_query") or [], 1):
                print(f"    {i}. {cypher[:100]}")
        print(f"  回答: {(state.get('output') or '')[:180]}")

    print(f"\n{'=' * 70}\n{len(TEST_CASES)} 条用例，路径与预期不符 {wrong} 条")
