from __future__ import annotations

from typing import Any, TypedDict

from langgraph.graph import END, START, StateGraph

from app.schemas.steward import (
    StewardRuntimeDecisionRequest,
    StewardRuntimeDecisionResponse,
    StewardSlotDecisionRequest,
    StewardSlotDecisionResponse,
)
from app.services.runtime_chat import RuntimeChatService
from app.services.steward_runtime_decision_agent import StewardRuntimeDecisionAgent
from app.services.steward_slot_decision_agent import StewardSlotDecisionAgent


class StewardSlotGraphState(TypedDict, total=False):
    request: StewardSlotDecisionRequest
    normalized_request: StewardSlotDecisionRequest
    decision: StewardSlotDecisionResponse
    model_call_traces: list[dict[str, Any]]
    fallback_reason: str


class StewardRuntimeGraphState(TypedDict, total=False):
    request: StewardRuntimeDecisionRequest
    normalized_request: StewardRuntimeDecisionRequest
    action_decision: StewardRuntimeDecisionResponse
    decision: StewardRuntimeDecisionResponse
    model_call_traces: list[dict[str, Any]]
    fallback_reason: str


class StewardGraphRuntime:
    """用 LangGraph 编排会话内槽位、记忆和行动决策。"""

    def __init__(self, runtime_chat_service: RuntimeChatService) -> None:
        self.slot_agent = StewardSlotDecisionAgent(runtime_chat_service)
        self.runtime_agent = StewardRuntimeDecisionAgent(runtime_chat_service)
        self._slot_graph = self._build_slot_graph()
        self._runtime_graph = self._build_runtime_graph()

    def decide_slot(self, request: StewardSlotDecisionRequest) -> StewardSlotDecisionResponse:
        final_state = self._slot_graph.invoke(
            {
                "request": request,
                "model_call_traces": [],
                "fallback_reason": "",
            }
        )
        decision = final_state.get("decision")
        if not isinstance(decision, StewardSlotDecisionResponse):
            raise RuntimeError("LangGraph 槽位决策未生成有效结果。")
        return decision

    def decide_runtime(self, request: StewardRuntimeDecisionRequest) -> StewardRuntimeDecisionResponse:
        final_state = self._runtime_graph.invoke(
            {
                "request": request,
                "model_call_traces": [],
                "fallback_reason": "",
            }
        )
        decision = final_state.get("decision")
        if not isinstance(decision, StewardRuntimeDecisionResponse):
            raise RuntimeError("LangGraph 运行时决策未生成有效结果。")
        return decision

    def _build_slot_graph(self):
        graph = StateGraph(StewardSlotGraphState)
        graph.add_node("slot_prepare_context", self._slot_prepare_context)
        graph.add_node("slot_tool_decision", self._slot_tool_decision)
        graph.add_node("slot_rule_fallback", self._slot_rule_fallback)

        graph.add_edge(START, "slot_prepare_context")
        graph.add_edge("slot_prepare_context", "slot_tool_decision")
        graph.add_conditional_edges(
            "slot_tool_decision",
            self._route_after_slot_tool_decision,
            {
                "done": END,
                "fallback": "slot_rule_fallback",
            },
        )
        graph.add_edge("slot_rule_fallback", END)
        return graph.compile()

    def _build_runtime_graph(self):
        graph = StateGraph(StewardRuntimeGraphState)
        graph.add_node("runtime_memory_context", self._runtime_memory_context)
        graph.add_node("runtime_action_decision", self._runtime_action_decision)
        graph.add_node("runtime_tool_decision", self._runtime_tool_decision)
        graph.add_node("runtime_rule_fallback", self._runtime_rule_fallback)

        graph.add_edge(START, "runtime_memory_context")
        graph.add_conditional_edges(
            "runtime_memory_context",
            self._route_after_runtime_memory_context,
            {
                "action": "runtime_action_decision",
                "tool": "runtime_tool_decision",
            },
        )
        graph.add_edge("runtime_action_decision", END)
        graph.add_conditional_edges(
            "runtime_tool_decision",
            self._route_after_runtime_tool_decision,
            {
                "done": END,
                "fallback": "runtime_rule_fallback",
            },
        )
        graph.add_edge("runtime_rule_fallback", END)
        return graph.compile()

    def _slot_prepare_context(self, state: StewardSlotGraphState) -> dict[str, Any]:
        return {
            "normalized_request": self.slot_agent._normalize_request(state["request"]),
        }

    def _slot_tool_decision(self, state: StewardSlotGraphState) -> dict[str, Any]:
        request = state.get("normalized_request") or state["request"]
        try:
            return {"decision": self.slot_agent.decide(request)}
        except Exception as exc:
            return {
                "model_call_traces": [
                    *state.get("model_call_traces", []),
                    self._build_failure_trace("langgraph_slot_decision", exc),
                ],
                "fallback_reason": f"LangGraph 槽位工具节点失败，已切换规则兜底：{exc}",
            }

    @staticmethod
    def _route_after_slot_tool_decision(state: StewardSlotGraphState) -> str:
        if isinstance(state.get("decision"), StewardSlotDecisionResponse):
            return "done"
        return "fallback"

    def _slot_rule_fallback(self, state: StewardSlotGraphState) -> dict[str, StewardSlotDecisionResponse]:
        request = state.get("normalized_request") or self.slot_agent._normalize_request(state["request"])
        return {
            "decision": self.slot_agent._build_rule_fallback(
                request,
                state.get("model_call_traces", []),
            )
        }

    def _runtime_memory_context(self, state: StewardRuntimeGraphState) -> dict[str, Any]:
        request = self.runtime_agent._normalize_request(state["request"])
        action_decision = self.runtime_agent._build_selected_flow_decision(request, [])
        update: dict[str, Any] = {"normalized_request": request}
        if action_decision is not None:
            update["action_decision"] = action_decision
        return update

    @staticmethod
    def _route_after_runtime_memory_context(state: StewardRuntimeGraphState) -> str:
        if isinstance(state.get("action_decision"), StewardRuntimeDecisionResponse):
            return "action"
        return "tool"

    def _runtime_action_decision(
        self,
        state: StewardRuntimeGraphState,
    ) -> dict[str, StewardRuntimeDecisionResponse]:
        return {"decision": state["action_decision"]}

    def _runtime_tool_decision(self, state: StewardRuntimeGraphState) -> dict[str, Any]:
        request = state.get("normalized_request") or state["request"]
        try:
            return {"decision": self.runtime_agent.decide(request)}
        except Exception as exc:
            return {
                "model_call_traces": [
                    *state.get("model_call_traces", []),
                    self._build_failure_trace("langgraph_runtime_decision", exc),
                ],
                "fallback_reason": f"LangGraph 运行时工具节点失败，已切换规则兜底：{exc}",
            }

    @staticmethod
    def _route_after_runtime_tool_decision(state: StewardRuntimeGraphState) -> str:
        if isinstance(state.get("decision"), StewardRuntimeDecisionResponse):
            return "done"
        return "fallback"

    def _runtime_rule_fallback(
        self,
        state: StewardRuntimeGraphState,
    ) -> dict[str, StewardRuntimeDecisionResponse]:
        request = state.get("normalized_request") or self.runtime_agent._normalize_request(state["request"])
        decision = self.runtime_agent._build_rule_fallback(
            request,
            state.get("model_call_traces", []),
        )
        return {
            "decision": self.runtime_agent._attach_updated_steward_state(
                decision,
                request,
            )
        }

    @staticmethod
    def _build_failure_trace(slot: str, exc: Exception) -> dict[str, Any]:
        return {
            "slot": slot,
            "provider": "langgraph",
            "model": "",
            "attempt": 1,
            "status": "failed",
            "error": str(exc),
        }
