"""调 _005_ 后端 /process、/process_stream 的客户端

单独一个模块、不 import streamlit，为的是能被测试直接导入 —— 导入 streamlit 应用
会执行页面上所有的 st.* 调用，没法当函数测。UI 在 app.py / pages/，网络这一层在这里。

这一层只跟 HTTP 打交道，不 import 项目里的任何东西（common、_004_ 都不碰）。
前端进程因此不会去加载 76 MB 的 FAISS 索引和 embedding 模型 —— 那些只属于后端。
前后端唯一的耦合就是两个接口的请求/响应契约。
"""

import json

import requests

# 后端默认地址。_005_fastapi/main.py 里 uvicorn 监听的是 8001（接口文档写的 8000 是旧值），
# 端口不对时侧边栏可以直接改，不用动代码
DEFAULT_BASE_URL = "http://127.0.0.1:8001"

# 单次问答 5-30 秒，复杂问题更久，接口文档建议 120 秒
DEFAULT_TIMEOUT = 120

# 后端出错时返回的固定文案（_005_fastapi.main.ERROR_OUTPUT），前端照原样展示
ERROR_OUTPUT = "系统出错了，请重试！"


class BackendError(Exception):
    """调后端失败。message 是可以直接显示给用户的中文说明。

    包装成一个异常类型，UI 就只需要 catch 一次，不用分别处理 ConnectionError /
    Timeout / HTTPError / JSONDecodeError 四种情况。
    """


def ask_backend(
    question: str,
    user_id: str,
    base_url: str = DEFAULT_BASE_URL,
    timeout: int = DEFAULT_TIMEOUT,
) -> str:
    """POST /process，返回 output 里的回答文本。

    失败一律抛 BackendError，附一句能直接显示的中文原因。
    """
    url = f"{base_url.rstrip('/')}/process"
    payload = {"input": question, "user_id": user_id}

    try:
        response = requests.post(url, json=payload, timeout=timeout)
    except requests.Timeout:
        raise BackendError(f"后端响应超时（超过 {timeout} 秒），请稍后重试。")
    except requests.ConnectionError:
        raise BackendError(
            f"连不上后端 {base_url}。请先启动服务："
            "python -X utf8 -m _005_fastapi.main"
        )
    except requests.RequestException as exc:
        raise BackendError(f"请求后端失败：{exc}")

    # 422 是入参校验没过，报错信息藏在 detail 里，对着提示改比看到 "422" 有用
    if response.status_code == 422:
        raise BackendError(f"请求参数不合法：{response.text}")

    # 500 时后端仍然返回 {"input", "output"} 这个结构，把它的文案透出来
    if response.status_code >= 400:
        try:
            output = (response.json() or {}).get("output")
        except ValueError:
            output = None
        raise BackendError(output or f"后端返回 {response.status_code}：{response.text[:200]}")

    try:
        body = response.json()
    except ValueError:
        raise BackendError(f"后端返回的不是 JSON：{response.text[:200]}")

    answer = (body.get("output") or "").strip()
    if not answer:
        raise BackendError("后端返回了空回答，请重试。")
    return answer


def ask_backend_stream(
    question: str,
    user_id: str,
    base_url: str = DEFAULT_BASE_URL,
    timeout: int = DEFAULT_TIMEOUT,
):
    """POST /process_stream，逐条 yield 后端推来的消息字典。

    这是个**生成器**：调用方 for 一次拿一条，才能边收边往页面上画。
    如果这里先把消息攒成 list 再返回，流式就白做了 —— 用户还是要等满 30 秒。

    yield 出来的消息就是接口文档里那三种 type：

        {"type": "thinking", "node": 节点名, "data": {该节点的状态增量}}
        {"type": "final",    "input": 原始问题, "output": 最终回答}
        {"type": "error",    "input": 原始问题, "output": 固定错误文案}

    error 消息是**后端正常返回的一行数据**（图跑挂了，但 HTTP 是 200，头早发出去了），
    所以不在这里抛异常，照原样 yield，让 UI 决定怎么显示。
    只有连不上、超时、状态码不对这类**没能开始流**的情况才抛 BackendError。
    """
    url = f"{base_url.rstrip('/')}/process_stream"
    payload = {"input": question, "user_id": user_id}

    try:
        # stream=True 是关键：不加的话 requests 会把整个响应体读完才返回，
        # 客户端这边就退化成一次性接口。with 保证连接一定被释放（包括中途 break）
        with requests.post(url, json=payload, stream=True, timeout=timeout) as response:
            if response.status_code == 422:
                raise BackendError(f"请求参数不合法：{response.text}")
            if response.status_code >= 400:
                # 出错响应不会是流，直接读完取文案
                try:
                    output = (response.json() or {}).get("output")
                except ValueError:
                    output = None
                raise BackendError(
                    output or f"后端返回 {response.status_code}：{response.text[:200]}"
                )

            got_any = False
            # 后端是换行分隔的 JSON（不是严格 SSE），iter_lines 正好对上
            for line in response.iter_lines(decode_unicode=True):
                if not line or not line.strip():
                    continue
                try:
                    message = json.loads(line)
                except ValueError:
                    # 单行坏了不该让整轮问答报废，跳过继续读后面的
                    continue
                got_any = True
                yield message

            if not got_any:
                raise BackendError("后端没有返回任何消息，请重试。")
    except requests.Timeout:
        raise BackendError(f"后端响应超时（超过 {timeout} 秒），请稍后重试。")
    except requests.ConnectionError:
        raise BackendError(
            f"连不上后端 {base_url}。请先启动服务：python -X utf8 -m _005_fastapi.main"
        )
    except requests.RequestException as exc:
        raise BackendError(f"请求后端失败：{exc}")


def check_health(base_url: str = DEFAULT_BASE_URL, timeout: int = 5) -> bool:
    """GET /，后端活着返回 True。用于侧边栏那个状态灯，所以超时给得很短。"""
    try:
        response = requests.get(f"{base_url.rstrip('/')}/", timeout=timeout)
        return response.status_code == 200 and response.json().get("status") == "ok"
    except (requests.RequestException, ValueError):
        return False
