"""中医知识图谱问答 —— Streamlit 前端

用户在页面上提问，调 _005_ 后端的 POST /process，把回答显示出来。

运行（不能用 python 直接跑，streamlit 应用必须由 streamlit 启动）:

    # 1. 先起后端
    python -X utf8 -m _005_fastapi.main
    # 2. 再起前端（哪个目录都行，下面的 sys.path 补丁会自己找到仓库根）
    streamlit run _006_streamlit/app.py

网络那一层在 api_client.py，这里只管界面。

两点和 Streamlit 执行模型有关的事：

- **每次交互，整个脚本从头重跑一遍。** 所以历史对话必须放 st.session_state，
  普通变量会在下一次 rerun 时归零。
- **user_id 每个浏览器会话生成一次**并存进 session_state，正好对上接口文档
  "同一用户多次请求传相同 user_id"。虽然后端目前还没实现会话记忆
  （AgentState 里没有这个字段），前端先把契约守住。
"""

import sys
import uuid
from pathlib import Path

# streamlit run 把**脚本自己所在的目录**放进 sys.path（不是仓库根目录，也不是 cwd），
# 和其它阶段用的 python -m 不一样。所以 `_006_streamlit.api_client` 这个绝对导入
# 在这里必然 ModuleNotFoundError —— 无论从哪个目录启动。
# 手动把仓库根补进去，路径从 __file__ 算，跟 common/path_utils.py 同一个思路。
# 这几行必须排在项目内的 import 之前，所以下面的 E402 是故意的。
_REPO_ROOT = str(Path(__file__).resolve().parent.parent)
if _REPO_ROOT not in sys.path:
    sys.path.insert(0, _REPO_ROOT)

import streamlit as st

from _006_streamlit.api_client import (
    DEFAULT_BASE_URL,
    DEFAULT_TIMEOUT,
    BackendError,
    ask_backend,
    check_health,
)

# 示例问题，取自 API_INTERFACE.md 的"常用测试问题"，点一下就能提问，省得手打中文
SAMPLE_QUESTIONS = [
    "人参有什么功效？",
    "四君子汤由哪些药材组成？",
    "《伤寒论》里记载了哪些方剂？",
    "我最近总是拉肚子，有什么方剂可以调理？",
]

st.set_page_config(page_title="中医知识图谱问答", page_icon="🌿", layout="centered")


def init_state() -> None:
    """初始化 session_state。rerun 会重跑整个脚本，所以每个键都要判存在再写。"""
    if "messages" not in st.session_state:
        # [(角色, 内容)]，角色是 "user" / "assistant"
        st.session_state.messages = []
    if "user_id" not in st.session_state:
        # 每个浏览器会话一个稳定 id，满足接口文档对 user_id 的要求
        st.session_state.user_id = f"streamlit-{uuid.uuid4().hex[:12]}"
    if "pending" not in st.session_state:
        # 点示例按钮时把问题放这里，让下一次 rerun 去处理。
        # 按钮回调发生在页面渲染中途，直接在那儿发请求会让聊天记录的渲染顺序错乱
        st.session_state.pending = None


def render_sidebar() -> tuple[str, int]:
    """侧边栏：后端地址、超时、状态灯、清空按钮。返回 (base_url, timeout)"""
    with st.sidebar:
        st.header("⚙️ 设置")
        base_url = st.text_input(
            "后端地址",
            value=DEFAULT_BASE_URL,
            help="_005_fastapi 服务的地址。端口被占用时改这里，不用改代码",
        )
        timeout = st.number_input(
            "超时（秒）",
            min_value=10,
            max_value=600,
            value=DEFAULT_TIMEOUT,
            step=10,
            help="单次问答通常 5-30 秒，接口文档建议 120 秒",
        )

        # 手动触发，不自动探活：每次 rerun 都发一个请求属于白费
        if st.button("检查后端连接", use_container_width=True):
            if check_health(base_url):
                st.success("后端正常")
            else:
                st.error("连不上后端，请先启动 _005_fastapi")

        st.divider()
        if st.button("🗑️ 清空对话", use_container_width=True):
            st.session_state.messages = []
            st.rerun()

        st.caption(f"会话 user_id：`{st.session_state.user_id}`")

    return base_url, int(timeout)


def handle_question(question: str, base_url: str, timeout: int) -> None:
    """把一轮问答追加进历史。

    先把用户消息写进历史再发请求，这样请求失败时提问也留在页面上，
    用户不用重新打一遍。
    """
    st.session_state.messages.append(("user", question))
    with st.chat_message("user"):
        st.markdown(question)

    with st.chat_message("assistant"):
        # 后端要跑 LLM + Neo4j，几十秒没有反馈会让人以为页面卡死了
        with st.spinner("正在查询中医知识图谱…"):
            try:
                answer = ask_backend(
                    question,
                    user_id=st.session_state.user_id,
                    base_url=base_url,
                    timeout=timeout,
                )
            except BackendError as exc:
                answer = f"⚠️ {exc}"
        # 回答里带 markdown（后端的 LLM 会输出加粗、列表、代码块），用 markdown 渲染
        st.markdown(answer)

    st.session_state.messages.append(("assistant", answer))


def main() -> None:
    init_state()
    base_url, timeout = render_sidebar()

    st.title("🌿 中医知识图谱问答")
    st.caption("基于 Neo4j 知识图谱 + LangGraph + 大模型，可以问中药、方剂、症状、出处")

    # 空历史时给几个示例，用户不必自己想第一个问题该怎么问
    if not st.session_state.messages:
        st.markdown("**试试这些问题：**")
        for i, sample in enumerate(SAMPLE_QUESTIONS):
            # key 必须唯一，否则 Streamlit 会报重复 key
            if st.button(sample, key=f"sample-{i}", use_container_width=True):
                # 存起来交给下一次 rerun 处理，别在渲染中途发请求
                st.session_state.pending = sample
                st.rerun()

    # 重放历史。rerun 会重跑整个脚本，页面上的消息全靠这一段重新画出来
    for role, content in st.session_state.messages:
        with st.chat_message(role):
            st.markdown(content)

    # 输入框固定在页面底部，回车提交
    question = st.chat_input("请输入你的问题，例如：人参有什么功效？")

    # 示例按钮排在输入框之后处理，两条来源汇成一个入口
    if st.session_state.pending:
        question = st.session_state.pending
        st.session_state.pending = None

    if question and question.strip():
        handle_question(question.strip(), base_url, timeout)


main()
