from __future__ import annotations

import json
import re
from typing import Any

from app.schemas.steward import (
    StewardFlowStatePatch,
    StewardRuntimeDecisionRequest,
    StewardRuntimeDecisionResponse,
)
from app.services.runtime_chat import RuntimeChatService
from app.services.steward_flow_state import StewardFlowStateService


STEWARD_RUNTIME_DECISION_FUNCTION_NAME = "submit_steward_runtime_decision"

RUNTIME_NEXT_ACTIONS = {
    "plan_new_tasks",
    "continue_selected_flow",
    "submit_current_application",
    "continue_next_task",
    "fill_current_slot",
    "ask_user",
    "cancel_current_action",
    "no_op",
}

FIELD_LABELS = {
    "transport_mode": "出行方式",
    "expense_type": "费用类型",
    "time_range": "时间",
    "location": "地点",
    "reason": "事由",
    "amount": "金额",
    "attachments": "附件",
}


class StewardRuntimeDecisionAgent:
    """用小财管家运行时上下文判断用户当前输入应落到哪个等待动作。"""

    def __init__(self, runtime_chat_service: RuntimeChatService) -> None:
        self.runtime_chat_service = runtime_chat_service

    def decide(self, request: StewardRuntimeDecisionRequest) -> StewardRuntimeDecisionResponse:
        normalized_request = self._normalize_request(request)
        selected_flow_decision = self._build_selected_flow_decision(normalized_request, [])
        if selected_flow_decision is not None:
            return selected_flow_decision
        result = self.runtime_chat_service.complete_with_tool_call(
            self._build_messages(normalized_request),
            tools=[self._build_tool_schema()],
            tool_choice={
                "type": "function",
                "function": {"name": STEWARD_RUNTIME_DECISION_FUNCTION_NAME},
            },
            max_tokens=1000,
            temperature=0.05,
            timeout_seconds=30,
            max_attempts=1,
        )
        traces = result.calls_as_dicts()
        if result.tool_call is not None and result.tool_call.name == STEWARD_RUNTIME_DECISION_FUNCTION_NAME:
            response = self._build_response_from_model_payload(result.tool_call.arguments, normalized_request, traces)
            if response is not None:
                return self._attach_updated_steward_state(response, normalized_request)
        return self._attach_updated_steward_state(
            self._build_rule_fallback(normalized_request, traces),
            normalized_request,
        )

    def _build_selected_flow_decision(
        self,
        request: StewardRuntimeDecisionRequest,
        traces: list[dict[str, Any]],
    ) -> StewardRuntimeDecisionResponse | None:
        selected_flow_id = self._resolve_selected_pending_flow_id(
            request.runtime_state,
            request.user_message,
        )
        if not selected_flow_id:
            return None
        next_state = StewardFlowStateService().confirm_flow(
            request.runtime_state.get("steward_state") if isinstance(request.runtime_state.get("steward_state"), dict) else {},
            selected_flow_id,
        )
        return StewardRuntimeDecisionResponse(
            decision_source="rule_fallback",
            next_action="continue_selected_flow",
            target_task_id=selected_flow_id,
            response_text=self._build_selected_flow_response_text(selected_flow_id),
            rationale="已按您选择的候选流程继续处理。",
            steward_state=next_state,
            model_call_traces=traces,
        )

    @staticmethod
    def _normalize_request(request: StewardRuntimeDecisionRequest) -> StewardRuntimeDecisionRequest:
        context_json = request.context_json if isinstance(request.context_json, dict) else {}
        runtime_state = request.runtime_state if isinstance(request.runtime_state, dict) else {}
        return StewardRuntimeDecisionRequest(
            user_message=str(request.user_message or "").strip(),
            session_type=str(request.session_type or "steward").strip() or "steward",
            runtime_state=StewardRuntimeDecisionAgent._hydrate_runtime_state(runtime_state, context_json),
            context_json=context_json,
        )

    @staticmethod
    def _hydrate_runtime_state(
        runtime_state: dict[str, Any],
        context_json: dict[str, Any],
    ) -> dict[str, Any]:
        hydrated = dict(runtime_state or {})
        steward_state = StewardRuntimeDecisionAgent._resolve_steward_state(context_json)
        if steward_state:
            hydrated.setdefault("steward_state", steward_state)
        if StewardRuntimeDecisionAgent._has_runtime_anchor(hydrated) or not steward_state:
            return hydrated

        active_flow = str(steward_state.get("active_flow") or "").strip()
        flows = steward_state.get("flows") if isinstance(steward_state.get("flows"), dict) else {}
        flow = flows.get(active_flow) if isinstance(flows, dict) else None
        if not isinstance(flow, dict):
            return hydrated

        missing_fields = [
            str(item or "").strip()
            for item in list(flow.get("missing_fields") or [])
            if str(item or "").strip()
        ]
        hydrated["current_task"] = {
            "task_id": active_flow,
            "task_type": "expense_application" if active_flow == "travel_application" else "reimbursement",
            "ontology_fields": dict(flow.get("fields") or {}),
            "missing_fields": missing_fields,
        }
        if missing_fields:
            hydrated["waiting_for"] = "steward_flow_field_completion"
        else:
            hydrated["waiting_for"] = "steward_flow_confirmation"
        return hydrated

    @staticmethod
    def _resolve_steward_state(context_json: dict[str, Any]) -> dict[str, Any]:
        direct_state = context_json.get("steward_state") or context_json.get("stewardState")
        if isinstance(direct_state, dict) and direct_state:
            return direct_state
        conversation_state = context_json.get("conversation_state")
        if isinstance(conversation_state, dict):
            nested_state = conversation_state.get("steward_state") or conversation_state.get("stewardState")
            if isinstance(nested_state, dict) and nested_state:
                return nested_state
        return {}

    @staticmethod
    def _has_runtime_anchor(runtime_state: dict[str, Any]) -> bool:
        if str(runtime_state.get("waiting_for") or "").strip():
            return True
        for key in ("pending_application", "pending_steward_action", "pending_slot_action", "current_task"):
            if isinstance(runtime_state.get(key), dict) and runtime_state[key]:
                return True
        return bool(runtime_state.get("remaining_tasks") or runtime_state.get("completed_tasks"))

    @staticmethod
    def _build_messages(request: StewardRuntimeDecisionRequest) -> list[dict[str, Any]]:
        payload = {
            "user_message": request.user_message,
            "session_type": request.session_type,
            "runtime_state": request.runtime_state,
            "context_json": request.context_json,
        }
        return [
            {
                "role": "system",
                "content": (
                    "你是 X-Financial 小财管家的运行时决策智能体。"
                    "你必须基于 runtime_state 判断用户当前输入对应哪个等待动作，不能把每次输入都当成全新任务。"
                    "runtime_state 会包含 current_task、remaining_tasks、completed_tasks、pending_application、"
                    "pending_steward_action、waiting_for、recent_structured_result 等上下文。"
                    "如果用户是在确认当前申请核对表无误，应返回 submit_current_application；"
                    "如果用户是在确认继续下一项，应返回 continue_next_task；"
                    "如果用户补充了当前等待字段，应返回 fill_current_slot；"
                    "如果当前结构化结果仍缺字段，应返回 ask_user；"
                    "只有当前没有可匹配上下文，且用户输入明显是新财务事项时，才返回 plan_new_tasks。"
                    "提交、入库、绑定、审批等高风险动作只返回结构化意图，实际执行由系统安全校验完成。"
                    "rationale 和 response_text 必须面向用户，不暴露内部推理链。"
                ),
            },
            {"role": "user", "content": json.dumps(payload, ensure_ascii=False)},
        ]

    @staticmethod
    def _build_tool_schema() -> dict[str, Any]:
        return {
            "type": "function",
            "function": {
                "name": STEWARD_RUNTIME_DECISION_FUNCTION_NAME,
                "description": "提交小财管家基于运行时上下文的下一步动作决策。",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "next_action": {
                            "type": "string",
                            "enum": sorted(RUNTIME_NEXT_ACTIONS),
                        },
                        "target_task_id": {"type": "string"},
                        "target_message_id": {"type": "string"},
                        "field_key": {"type": "string"},
                        "field_value": {"type": "string"},
                        "confirmation_required": {"type": "boolean"},
                        "question": {"type": "string"},
                        "response_text": {"type": "string"},
                        "rationale": {"type": "string"},
                    },
                    "required": [
                        "next_action",
                        "target_task_id",
                        "target_message_id",
                        "field_key",
                        "field_value",
                        "confirmation_required",
                        "question",
                        "response_text",
                        "rationale",
                    ],
                },
            },
        }

    def _build_response_from_model_payload(
        self,
        payload: dict[str, Any],
        request: StewardRuntimeDecisionRequest,
        traces: list[dict[str, Any]],
    ) -> StewardRuntimeDecisionResponse | None:
        next_action = str(payload.get("next_action") or "").strip()
        if next_action not in RUNTIME_NEXT_ACTIONS:
            return None
        return StewardRuntimeDecisionResponse(
            decision_source="llm_function_call",
            next_action=next_action,  # type: ignore[arg-type]
            target_task_id=self._clean_text(payload.get("target_task_id")),
            target_message_id=self._clean_text(payload.get("target_message_id")),
            field_key=self._clean_text(payload.get("field_key")),
            field_value=self._clean_text(payload.get("field_value")),
            confirmation_required=bool(payload.get("confirmation_required")),
            question=self._clean_text(payload.get("question")),
            response_text=self._clean_text(payload.get("response_text")),
            rationale=self._clean_text(payload.get("rationale")),
            model_call_traces=traces,
        )

    def _build_rule_fallback(
        self,
        request: StewardRuntimeDecisionRequest,
        traces: list[dict[str, Any]],
    ) -> StewardRuntimeDecisionResponse:
        state = request.runtime_state
        pending_application = state.get("pending_application") if isinstance(state.get("pending_application"), dict) else {}
        pending_steward_action = state.get("pending_steward_action") if isinstance(state.get("pending_steward_action"), dict) else {}
        waiting_for = str(state.get("waiting_for") or "").strip()
        message = request.user_message.replace(" ", "")
        confirmation_text = message in {"确认", "确定", "无误", "确认提交", "可以提交", "提交", "没问题"}
        if confirmation_text and pending_application.get("ready_to_submit"):
            return StewardRuntimeDecisionResponse(
                decision_source="rule_fallback",
                next_action="submit_current_application",
                target_message_id=str(pending_application.get("message_id") or ""),
                target_task_id=str(pending_application.get("task_id") or ""),
                rationale="模型运行时决策暂不可用，我先按当前待提交申请单的上下文处理您的确认。",
                model_call_traces=traces,
            )
        if confirmation_text and pending_steward_action:
            return StewardRuntimeDecisionResponse(
                decision_source="rule_fallback",
                next_action="continue_next_task",
                target_message_id=str(pending_steward_action.get("message_id") or ""),
                target_task_id=str(pending_steward_action.get("target_task_id") or ""),
                rationale="模型运行时决策暂不可用，我先按当前待确认的下一项任务继续处理。",
                model_call_traces=traces,
            )
        if waiting_for == "steward_flow_field_completion":
            current_task = state.get("current_task") if isinstance(state.get("current_task"), dict) else {}
            missing_fields = [
                str(item or "").strip()
                for item in list(current_task.get("missing_fields") or [])
                if str(item or "").strip()
            ]
            field_key = missing_fields[0] if missing_fields else ""
            if field_key and request.user_message:
                return StewardRuntimeDecisionResponse(
                    decision_source="rule_fallback",
                    next_action="fill_current_slot",
                    target_task_id=str(current_task.get("task_id") or ""),
                    field_key=field_key,
                    field_value=request.user_message,
                        rationale="模型运行时决策暂不可用，我先把您的补充写入当前小财管家流程字段。",
                    model_call_traces=traces,
                )
            if field_key:
                return StewardRuntimeDecisionResponse(
                    decision_source="rule_fallback",
                    next_action="ask_user",
                    target_task_id=str(current_task.get("task_id") or ""),
                    field_key=field_key,
                    question=f"请补充{FIELD_LABELS.get(field_key, field_key)}。",
                    rationale="当前小财管家流程仍缺少必要字段。",
                    model_call_traces=traces,
                )
        if waiting_for:
            return StewardRuntimeDecisionResponse(
                decision_source="rule_fallback",
                next_action="ask_user",
                question="我需要先确认当前等待事项，请补充或选择当前问题对应的信息。",
                rationale="模型运行时决策暂不可用，当前仍存在等待用户补充的信息。",
                model_call_traces=traces,
            )
        return StewardRuntimeDecisionResponse(
            decision_source="rule_fallback",
            next_action="plan_new_tasks",
            rationale="模型运行时决策暂不可用，当前没有可安全匹配的等待动作，回到任务规划。",
            model_call_traces=traces,
        )

    @staticmethod
    def _resolve_selected_pending_flow_id(runtime_state: dict[str, Any], user_message: str) -> str:
        steward_state = runtime_state.get("steward_state")
        if not isinstance(steward_state, dict):
            return ""
        pending = steward_state.get("pending_flow_confirmation")
        if not isinstance(pending, dict) or pending.get("status") != "pending":
            return ""
        message = re.sub(r"\s+", "", str(user_message or ""))
        if not message:
            return ""
        candidates = pending.get("candidate_flows") if isinstance(pending.get("candidate_flows"), list) else []
        for candidate in candidates:
            if not isinstance(candidate, dict):
                continue
            flow_id = str(candidate.get("flow_id") or "").strip()
            label = re.sub(r"\s+", "", str(candidate.get("label") or ""))
            if flow_id == "travel_application" and (
                message in {"补办出差申请", "出差申请", "申请", "补申请"}
                or (label and message == label)
            ):
                return flow_id
            if flow_id == "travel_reimbursement" and (
                message in {"发起费用报销", "费用报销", "报销", "发起报销"}
                or (label and message == label)
            ):
                return flow_id
        return ""

    @staticmethod
    def _build_selected_flow_response_text(flow_id: str) -> str:
        if flow_id == "travel_application":
            return "已确认按 **补办出差申请** 继续，我会基于当前出差信息整理申请材料。"
        return "已确认按 **发起费用报销** 继续，我会基于当前出差信息整理报销材料。"

    @staticmethod
    def _clean_text(value: Any) -> str:
        return str(value or "").strip()

    def _attach_updated_steward_state(
        self,
        response: StewardRuntimeDecisionResponse,
        request: StewardRuntimeDecisionRequest,
    ) -> StewardRuntimeDecisionResponse:
        steward_state = request.runtime_state.get("steward_state")
        if not isinstance(steward_state, dict) or not steward_state:
            return response
        if response.next_action == "continue_selected_flow":
            flow_id = self._resolve_target_flow_id(response, steward_state)
            if flow_id:
                next_state = StewardFlowStateService().confirm_flow(steward_state, flow_id)
                return response.model_copy(update={"steward_state": next_state})
            return response.model_copy(update={"steward_state": steward_state})
        if response.next_action != "fill_current_slot" or not response.field_key:
            return response.model_copy(update={"steward_state": steward_state})

        flow_id = self._resolve_target_flow_id(response, steward_state)
        if not flow_id:
            return response.model_copy(update={"steward_state": steward_state})
        current_flow = self._resolve_flow(steward_state, flow_id)
        remaining_missing_fields = [
            key
            for key in list(current_flow.get("missing_fields") or [])
            if str(key or "").strip() and str(key or "").strip() != response.field_key
        ]
        next_state = StewardFlowStateService().merge_state(
            steward_state,
            StewardFlowStatePatch(
                active_flow=flow_id,  # type: ignore[arg-type]
                flow_id=flow_id,  # type: ignore[arg-type]
                intent=str(current_flow.get("intent") or "").strip(),
                status="collecting" if remaining_missing_fields else "ready_for_confirmation",
                fields={response.field_key: response.field_value},
                missing_fields=remaining_missing_fields,
                evidence=[
                    {
                        "source": "runtime_user_message",
                        "field": response.field_key,
                        "text": request.user_message,
                    }
                ],
            ),
        )
        return response.model_copy(update={"steward_state": next_state})

    @staticmethod
    def _resolve_target_flow_id(
        response: StewardRuntimeDecisionResponse,
        steward_state: dict[str, Any],
    ) -> str:
        target = str(response.target_task_id or "").strip()
        if target in {"travel_application", "travel_reimbursement"}:
            return target
        active_flow = str(steward_state.get("active_flow") or "").strip()
        return active_flow if active_flow in {"travel_application", "travel_reimbursement"} else ""

    @staticmethod
    def _resolve_flow(steward_state: dict[str, Any], flow_id: str) -> dict[str, Any]:
        flows = steward_state.get("flows") if isinstance(steward_state.get("flows"), dict) else {}
        flow = flows.get(flow_id) if isinstance(flows, dict) else {}
        return dict(flow) if isinstance(flow, dict) else {}
