#!/usr/bin/env python3
"""
CloudLLMNode - 云端大模型节点

职责：
1. 订阅 /planner/llm_request，只处理 route_hint == "cloud" 的请求
2. 调用 DeepSeek 云端 API
3. 解析模型输出：自然语言 + <FINAL_JSON> + JSON
4. 发布 /planner/llm_response，与 OmniNode 兼容
5. 失败时返回安全 fallback JSON

设计原则：
- CloudLLMNode 是"模型调用适配层"，不是"业务提示词生成器"
- 严格依赖 /planner/llm_request 中已有内容
- 不在节点内部定义任何机器人业务提示词
"""

import json
import re
import time
import uuid
from typing import Optional, Dict, Any, Tuple, List

import rclpy
from rclpy.node import Node
from std_msgs.msg import String


class CloudLLMNode(Node):
    """云端大模型节点"""

    # 极薄协议约束 - 不包含任何业务内容
    DEFAULT_SYSTEM_PROMPT = """你是一个通用模型助手。
请严格基于输入内容回答。

==输出协议（必须严格遵守）==
1. 第一段：自然语言回复（可直接播报给用户）
2. 单独一行：<FINAL_JSON>
3. 第三段：严格 JSON 对象

==禁止事项==
1. 禁止使用 markdown 代码块（如 ```json）
2. 禁止在 JSON 后追加任何解释文字
3. 禁止输出纯 JSON（必须包含自然语言前缀）
4. JSON 必须是合法可解析的
"""

    def __init__(self, node_name: str = "cloud_llm_node"):
        super().__init__(node_name)

        self._default_api_key = ""
        self._default_base_url = "https://api.deepseek.com"
        self._default_model = "deepseek-chat"
        self._default_timeout = 30.0
        self._default_enabled = True
        self._default_separator = "<FINAL_JSON>"
        self._default_system_prompt_path = ""
        self._default_debug = False

        self.declare_parameter("cloud_llm_api_key", self._default_api_key)
        self.declare_parameter("cloud_llm_base_url", self._default_base_url)
        self.declare_parameter("cloud_llm_model", self._default_model)
        self.declare_parameter("cloud_llm_timeout_sec", self._default_timeout)
        self.declare_parameter("cloud_llm_enabled", self._default_enabled)
        self.declare_parameter("model_response_separator", self._default_separator)
        self.declare_parameter("cloud_system_prompt_path", self._default_system_prompt_path)
        self.declare_parameter("cloud_debug", self._default_debug)

        self.declare_parameter("llm_request_topic", "/planner/llm_request")
        self.declare_parameter("llm_response_topic", "/planner/llm_response")

        def _get_param(name: str, default: Any) -> Any:
            value = self.get_parameter(name).value
            if value is None or value == "":
                return default
            if isinstance(default, float) and isinstance(value, str):
                try:
                    return float(value)
                except (ValueError, TypeError):
                    return default
            if isinstance(default, bool) and isinstance(value, str):
                return value.lower() in ('true', '1', 'yes')
            return value

        self.api_key: str = _get_param("cloud_llm_api_key", self._default_api_key)
        self.base_url: str = _get_param("cloud_llm_base_url", self._default_base_url)
        self.model: str = _get_param("cloud_llm_model", self._default_model)
        self.timeout_sec: float = _get_param("cloud_llm_timeout_sec", self._default_timeout)
        self.enabled: bool = _get_param("cloud_llm_enabled", self._default_enabled)
        self.separator: str = _get_param("model_response_separator", self._default_separator)
        self.system_prompt_path: str = _get_param("cloud_system_prompt_path", self._default_system_prompt_path)
        self.debug: bool = _get_param("cloud_debug", self._default_debug)

        self.request_topic: str = self.get_parameter("llm_request_topic").value
        self.response_topic: str = self.get_parameter("llm_response_topic").value

        if not self.enabled:
            self.get_logger().warn("[CloudLLMNode] cloud_llm_enabled=False，节点已禁用")
        elif not self.api_key:
            self.get_logger().warn("[CloudLLMNode] cloud_llm_api_key 未配置，请检查配置文件")
            self.enabled = False
        else:
            self.get_logger().info(f"[CloudLLMNode] 配置加载完成:")
            self.get_logger().info(f"  - Base URL: {self.base_url}")
            self.get_logger().info(f"  - Model: {self.model}")
            self.get_logger().info(f"  - Timeout: {self.timeout_sec}s")
            self.get_logger().info(f"  - Separator: {self.separator}")

        self.system_prompt = self._load_system_prompt()

        qos = rclpy.qos.QoSProfile(depth=10)

        self.sub_request = self.create_subscription(
            String, self.request_topic, self._on_llm_request, qos
        )
        self.pub_response = self.create_publisher(String, self.response_topic, qos)
        self.pub_natural_response = self.create_publisher(String, "/llm_response", qos)

        self.get_logger().info(f"[CloudLLMNode] 订阅 topic: {self.request_topic}")
        self.get_logger().info(f"[CloudLLMNode] 发布 topic: {self.response_topic}")
        self.get_logger().info(f"[CloudLLMNode] 发布自然语言 topic: /llm_response")

        self.waiting_for_response = False
        self.current_request_id = None
        self.current_session_id = None

        self.get_logger().info("[CloudLLMNode] 节点初始化完成")

    def _load_system_prompt(self) -> str:
        if self.system_prompt_path:
            try:
                with open(self.system_prompt_path, 'r', encoding='utf-8') as f:
                    content = f.read().strip()
                    if content:
                        self.get_logger().info(f"[CloudLLMNode] 从文件加载 system prompt: {self.system_prompt_path}")
                        return content
            except Exception as e:
                self.get_logger().warn(f"[CloudLLMNode] 加载 system prompt 文件失败: {e}，使用内置默认")
        return self.DEFAULT_SYSTEM_PROMPT

    def _on_llm_request(self, msg: String) -> None:
        try:
            request_data = json.loads(msg.data)
        except json.JSONDecodeError as e:
            self.get_logger().error(f"[CloudLLMNode] JSON 解析失败: {e}")
            return

        route_hint = request_data.get("route_hint", "")
        request_id = request_data.get("request_id", "unknown")
        session_id = request_data.get("session_id", "default")

        if self.debug:
            self.get_logger().info(f"[CloudLLMNode] 收到请求: request_id={request_id}, route_hint={route_hint}")

        if route_hint != "cloud":
            return

        if not self.enabled:
            self.get_logger().warn(f"[CloudLLMNode] 云端 LLM 未启用，忽略 cloud 请求")
            return

        if self.waiting_for_response:
            self.get_logger().warn(f"[CloudLLMNode] 正在等待上一个响应，忽略重复请求")
            return

        self.waiting_for_response = True
        self.current_request_id = request_id
        self.current_session_id = session_id

        try:
            self._process_cloud_request(request_data)
        except Exception as e:
            self.get_logger().error(f"[CloudLLMNode] 处理请求异常: {e}")
            self._publish_fallback_response(request_id, session_id, f"process_error: {e}")
        finally:
            self.waiting_for_response = False
            self.current_request_id = None
            self.current_session_id = None

    def _process_cloud_request(self, request_data: dict) -> None:
        request_id = request_data.get("request_id", "unknown")
        session_id = request_data.get("session_id", "default")
        user_text = request_data.get("user_text", "")

        if self.debug:
            self.get_logger().info(f"[CloudLLMNode] 处理 cloud 请求: user_text={user_text}")

        # 构建消息列表
        messages = self.build_cloud_messages(request_data)
        
        # 打印完整构建的消息（方便调试）
        self.get_logger().info(f"[CloudLLMNode] ===== 构建的完整消息 =====")
        for i, msg in enumerate(messages):
            role = msg.get("role", "unknown")
            content = msg.get("content", "")
            # 打印前2000字符以确保完整
            self.get_logger().info(f"[CloudLLMNode] [{i}] Role: {role}")
            if len(content) > 2000:
                self.get_logger().info(f"[CloudLLMNode] [{i}] Content (前2000字符):\n{content[:2000]}")
                self.get_logger().info(f"[CloudLLMNode] [{i}] Content (后500字符):\n...{content[-500:]}")
            else:
                self.get_logger().info(f"[CloudLLMNode] [{i}] Content:\n{content}")
        self.get_logger().info(f"[CloudLLMNode] ===== 消息构建完成 =====")
        
        success, full_response = self._call_deepseek_api(messages)

        if not success:
            self.get_logger().error(f"[CloudLLMNode] API 调用失败: {full_response}")
            self._publish_fallback_response(request_id, session_id, f"api_error: {full_response}")
            return

        # 打印完整的 LLM 原始响应
        self.get_logger().info(f"[CloudLLMNode] ===== LLM 完整原始响应 (len={len(full_response)}) =====")
        self.get_logger().info(f"[CloudLLMNode] {full_response}")
        self.get_logger().info(f"[CloudLLMNode] ===== 响应打印完毕 =====")

        success, parsed_json, natural_response = self._parse_llm_response(full_response)

        if not success:
            self.get_logger().warn(f"[CloudLLMNode] JSON 解析失败，使用 fallback")
            self._publish_fallback_response(request_id, session_id, "json_parse_failed")
            return

        self._publish_response(request_id, session_id, parsed_json, natural_response)

    def build_cloud_messages(self, request_data: dict) -> List[dict]:
        """
        构建发给云端 LLM 的消息列表。
        
        优先级策略：
        1. 最高优先：request_data 中已有 'messages' 字段 → 直接使用
        2. 其次：request_data 中已有 'system_prompt' 字段 → 作为 system message
        3. 再次：request_data 中已有 'prompt' / 'prompt_text' 字段 → 直接作为 user message
        4. 最低：都不存在 → 使用极薄协议 + 最小字段转发
        
        设计原则：CloudLLMNode 是"模型调用适配层"，不是"业务提示词生成器"
        """
        
        # 优先级 1: 如果 request_data 中已有完整的 messages，直接使用
        if "messages" in request_data and request_data["messages"]:
            messages = request_data["messages"]
            if isinstance(messages, list) and len(messages) > 0:
                self.get_logger().info(f"[CloudLLMNode] 使用 request_data 中已有的 messages (共 {len(messages)} 条)")
                return messages
        
        # 优先级 2: 如果有完整的 prompt 文本
        prompt_text = None
        if "prompt" in request_data and request_data["prompt"]:
            prompt_text = request_data["prompt"]
        elif "prompt_text" in request_data and request_data["prompt_text"]:
            prompt_text = request_data["prompt_text"]
        
        if prompt_text:
            self.get_logger().info(f"[CloudLLMNode] 使用 request_data 中已有的 prompt (len={len(prompt_text)})")
            return [
                {"role": "system", "content": self.system_prompt},
                {"role": "user", "content": prompt_text}
            ]
        
        # 优先级 3: 如果有 system_prompt 字段
        custom_system_prompt = request_data.get("system_prompt", "")
        if custom_system_prompt:
            self.get_logger().info(f"[CloudLLMNode] 使用 request_data 中的 system_prompt")
            system_content = custom_system_prompt
        else:
            system_content = self.system_prompt
        
        # 优先级 4: 最小字段转发模式（不做业务加工）
        user_prompt = self._build_minimal_user_prompt(request_data)
        
        return [
            {"role": "system", "content": system_content},
            {"role": "user", "content": user_prompt}
        ]

    def _build_minimal_user_prompt(self, request_data: dict) -> str:
        """
        最小字段转发 - 只做原样转发，不做业务加工。
        
        格式简单清晰，不包含任何机器人业务提示词。
        """
        parts = []
        
        # 用户输入
        user_text = request_data.get("user_text", "")
        if user_text:
            parts.append(f"用户输入: {user_text}")
        
        # 记忆上下文
        memory_text = request_data.get("memory_text", "")
        if memory_text:
            parts.append(f"\n记忆上下文: {memory_text}")
        
        # 世界状态
        world_snapshot = request_data.get("world_snapshot", {})
        if world_snapshot:
            if isinstance(world_snapshot, str):
                parts.append(f"\n世界状态: {world_snapshot}")
            else:
                parts.append(f"\n世界状态: {json.dumps(world_snapshot, ensure_ascii=False)}")
        
        # 意图信息
        intent_info = request_data.get("intent_info", {})
        if intent_info:
            intent_str = json.dumps(intent_info, ensure_ascii=False)
            parts.append(f"\n意图信息: {intent_str}")
        
        # 老师记忆
        teacher_memory_hint = request_data.get("teacher_memory_hint", "")
        if teacher_memory_hint:
            if isinstance(teacher_memory_hint, dict):
                parts.append(f"\n老师记忆: {json.dumps(teacher_memory_hint, ensure_ascii=False)}")
            else:
                parts.append(f"\n老师记忆: {teacher_memory_hint}")
        
        # 可用工具
        available_tools = request_data.get("available_tools", [])
        if available_tools:
            if isinstance(available_tools, list):
                tools_str = ", ".join(str(t) for t in available_tools)
            else:
                tools_str = str(available_tools)
            parts.append(f"\n可用工具: {tools_str}")
        
        # 工具描述
        tool_descriptions = request_data.get("tool_descriptions", [])
        if tool_descriptions:
            if isinstance(tool_descriptions, list):
                desc_parts = []
                for t in tool_descriptions:
                    if isinstance(t, dict):
                        name = t.get("name", "")
                        desc = t.get("description", "")
                        desc_parts.append(f"- {name}: {desc}")
                    else:
                        desc_parts.append(f"- {t}")
                parts.append(f"\n工具描述:\n" + "\n".join(desc_parts))
            else:
                parts.append(f"\n工具描述: {tool_descriptions}")
        
        # 领域规则
        domain_rules = request_data.get("domain_rules", {})
        if domain_rules:
            if isinstance(domain_rules, dict):
                parts.append(f"\n领域规则: {json.dumps(domain_rules, ensure_ascii=False)}")
            else:
                parts.append(f"\n领域规则: {domain_rules}")
        
        # 规划模式
        planner_mode = request_data.get("planner_mode", "")
        if planner_mode:
            parts.append(f"\n规划模式: {planner_mode}")
        
        # 当前状态
        current_state = request_data.get("current_state", {})
        if current_state:
            if isinstance(current_state, dict):
                parts.append(f"\n当前状态: {json.dumps(current_state, ensure_ascii=False)}")
            else:
                parts.append(f"\n当前状态: {current_state}")
        
        # 输出要求
        parts.append("\n\n请严格基于以上内容回答，并严格按协议输出。")
        
        return "\n".join(parts)

    def _call_deepseek_api(self, messages: list) -> Tuple[bool, str]:
        try:
            import openai
            
            # 打印完整的请求信息
            self.get_logger().info(f"[CloudLLMNode] ===== 发送请求到云端 LLM =====")
            self.get_logger().info(f"[CloudLLMNode] API: {self.base_url}/chat/completions")
            self.get_logger().info(f"[CloudLLMNode] Model: {self.model}")
            self.get_logger().info(f"[CloudLLMNode] Temperature: 0.7, Max Tokens: 2048")
            self.get_logger().info(f"[CloudLLMNode] ===== System Prompt =====")
            for msg in messages:
                if msg["role"] == "system":
                    self.get_logger().info(f"[CloudLLMNode] [SYSTEM] {msg['content'][:500]}...")
                elif msg["role"] == "user":
                    self.get_logger().info(f"[CloudLLMNode] [USER] {msg['content'][:1000]}...")
            self.get_logger().info(f"[CloudLLMNode] ===== 请求消息总数: {len(messages)} =====")
            
            client = openai.OpenAI(api_key=self.api_key, base_url=self.base_url, timeout=self.timeout_sec)
            response = client.chat.completions.create(
                model=self.model, messages=messages, temperature=0.7, max_tokens=2048,
            )
            if not response.choices:
                return False, "empty_choices"
            content = response.choices[0].message.content
            if not content:
                return False, "empty_content"
            return True, content
        except ImportError:
            self.get_logger().error("[CloudLLMNode] 未安装 openai 库，请运行: pip install openai")
            return False, "openai_not_installed"
        except Exception as e:
            self.get_logger().error(f"[CloudLLMNode] API 调用异常: {e}")
            return False, str(e)

    def _parse_llm_response(self, full_text: str) -> Tuple[bool, dict, str]:
        if not full_text:
            return False, {}, ""

        natural_response = ""
        json_part = ""
        separator = self.separator

        if separator in full_text:
            parts = full_text.split(separator, 1)
            natural_response = parts[0].strip()
            if len(parts) > 1:
                json_part = parts[1].strip()
        else:
            self.get_logger().warn(f"[CloudLLMNode] 未找到分隔符 {separator}，尝试兜底解析")
            natural_response = full_text.strip()
            json_part = full_text.strip()

        success, json_data = self.extract_json_from_response(json_part)
        if success:
            return True, json_data, natural_response
        else:
            success, json_data = self.extract_json_from_response(full_text)
            if success:
                return True, json_data, natural_response
            return False, {}, natural_response

    def extract_json_from_response(self, text: str) -> Tuple[bool, dict]:
        if not text:
            return False, {}

        text = text.strip()

        # 方法1: 直接尝试解析整个文本
        try:
            json_data = json.loads(text)
            return True, json_data
        except json.JSONDecodeError:
            pass

        # 方法2: 查找最外层 JSON 对象
        json_pattern = r'\{[\s\S]*\}'
        matches = list(re.finditer(json_pattern, text))
        if matches:
            for match in reversed(matches):
                candidate = match.group(0)
                try:
                    json_data = json.loads(candidate)
                    if isinstance(json_data, dict) and "plan" in json_data:
                        return True, json_data
                except json.JSONDecodeError:
                    continue

        return False, {}

    def _publish_response(self, request_id: str, session_id: str, json_data: dict, natural_response: str = "") -> None:
        try:
            if "request_id" not in json_data or not json_data.get("request_id"):
                json_data["request_id"] = request_id
            if "session_id" not in json_data or not json_data.get("session_id"):
                json_data["session_id"] = session_id

            if "plan" not in json_data or not isinstance(json_data.get("plan"), dict):
                json_data["plan"] = {}

            plan = json_data["plan"]

            if "plan_id" not in plan or not plan.get("plan_id"):
                plan["plan_id"] = f"cloud_plan_{uuid.uuid4().hex[:8]}"
            if "goal" not in plan or not plan.get("goal"):
                plan["goal"] = "云端 LLM 生成的任务"
            if "reasoning" not in plan or not plan.get("reasoning"):
                plan["reasoning"] = "由 CloudLLMNode 调用 DeepSeek 生成"
            if "risk_level" not in plan or not plan.get("risk_level"):
                plan["risk_level"] = "low"
            if "requires_confirmation" not in plan:
                plan["requires_confirmation"] = False
            if "confirmation_message" not in plan:
                plan["confirmation_message"] = None
            if "steps" not in plan:
                plan["steps"] = []
            # 将 action_list 转换为 steps（兼容不同格式）
            if "action_list" in plan and plan["action_list"]:
                if not plan["steps"]:
                    for action in plan["action_list"]:
                        step = {
                            "action": action.get("action", ""),
                            "description": action.get("description", ""),
                            "parameters": action.get("parameters", {}),
                            "preconditions": action.get("preconditions", {}),
                            "fallback": action.get("fallback"),
                            "status": "pending"
                        }
                        plan["steps"].append(step)
            if "status" not in plan or not plan.get("status"):
                plan["status"] = "created"
            if "source" not in plan or not plan.get("source"):
                plan["source"] = "cloud_llm"
            if "metadata" not in plan or not isinstance(plan.get("metadata"), dict):
                plan["metadata"] = {}

            for step in plan.get("steps", []):
                if "step_id" not in step:
                    step["step_id"] = 1
                if "action" not in step:
                    step["action"] = "unknown"
                if "tool_call_type" not in step:
                    step["tool_call_type"] = "action"
                if "parameters" not in step:
                    step["parameters"] = {}
                if "preconditions" not in step:
                    step["preconditions"] = {}
                if "fallback" not in step:
                    step["fallback"] = None
                if "status" not in step:
                    step["status"] = "pending"
                if "description" not in step:
                    step["description"] = step.get("action", "")
                if "requires_confirmation" not in step:
                    step["requires_confirmation"] = False
                if "confirmation_message" not in step:
                    step["confirmation_message"] = None
                if "metadata" not in step:
                    step["metadata"] = {}

            response_json = json.dumps(json_data, ensure_ascii=False)
            msg = String()
            msg.data = response_json
            self.pub_response.publish(msg)

            # 同时发布自然语言到 /llm_response (供 TTS 使用)
            if natural_response:
                natural_msg = String()
                natural_msg.data = natural_response
                self.pub_natural_response.publish(natural_msg)

            self.get_logger().info(
                f"[CloudLLMNode] 发布响应: request_id={request_id}, source={plan.get('source')}, steps={len(plan.get('steps', []))}"
            )
        except Exception as e:
            self.get_logger().error(f"[CloudLLMNode] 发布响应异常: {e}")
            self._publish_fallback_response(request_id, session_id, f"publish_error: {e}")

    def _publish_fallback_response(self, request_id: str, session_id: str, reason: str = "fallback") -> None:
        fallback_json = self.build_fallback_json(request_id, session_id, reason)
        fallback_text = "抱歉，我没有完全理解您的请求，请再说一次。"
        try:
            response_json = json.dumps(fallback_json, ensure_ascii=False)
            msg = String()
            msg.data = response_json
            self.pub_response.publish(msg)

            # 同时发布自然语言到 /llm_response (供 TTS 使用)
            natural_msg = String()
            natural_msg.data = fallback_text
            self.pub_natural_response.publish(natural_msg)

            self.get_logger().warn(f"[CloudLLMNode] 发布 fallback 响应: request_id={request_id}, reason={reason}")
        except Exception as e:
            self.get_logger().error(f"[CloudLLMNode] 发布 fallback 失败: {e}")

    def build_fallback_json(self, request_id: str, session_id: str, reason: str = "fallback") -> dict:
        return {
            "request_id": request_id,
            "session_id": session_id,
            "plan": {
                "plan_id": f"cloud_fallback_{uuid.uuid4().hex[:8]}",
                "goal": "无法可靠解析云端模型输出",
                "reasoning": f"Cloud LLM 输出解析失败，返回安全 fallback。原因: {reason}",
                "risk_level": "low",
                "requires_confirmation": False,
                "confirmation_message": None,
                "steps": [
                    {
                        "step_id": 1,
                        "action": "ask_user",
                        "tool_call_type": "ask_user",
                        "parameters": {"question": "抱歉，我没有完全理解您的请求，请再说一次。"},
                        "preconditions": {},
                        "fallback": None,
                        "status": "pending",
                        "description": "fallback: 请求用户重新描述",
                        "requires_confirmation": False,
                        "confirmation_message": None,
                        "metadata": {"fallback_reason": reason, "fallback_source": "cloud_llm"}
                    }
                ],
                "status": "created",
                "source": "cloud_fallback",
                "metadata": {"response_type": "fallback", "fallback": True, "fallback_reason": reason}
            }
        }


def main(args=None):
    rclpy.init(args=args)
    node = CloudLLMNode()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        node.get_logger().info("[CloudLLMNode] 收到 Ctrl+C，正在关闭...")
    finally:
        node.destroy_node()
        rclpy.shutdown()

