from flask import Flask, request, jsonify, send_file, send_from_directory, Response, after_this_request

import sys, io, argparse, secrets, json, traceback
import logging
import threading
import subprocess
import time
from logging.handlers import RotatingFileHandler
import os
import shutil
import requests
import jwt
import tarfile
from datetime import datetime
import inflection
from functools import wraps
from space_mgmt import space_blueprint

# 创建 Flask 应用实例
app = Flask(__name__)
app.register_blueprint(space_blueprint)

# region 驼峰/蛇形转换工具函数
def dict_to_camel(d):
    """
    递归将字典中的蛇形键转换为驼峰键，支持嵌套结构
    
    Args:
        d: 要转换的字典或列表
        
    Returns:
        转换后的新字典/列表，保持原数据结构
    """
    # 处理字典类型
    if isinstance(d, dict):
        new_dict = {}
        for key, value in d.items():
            # 转换键为驼峰格式
            camel_key = inflection.camelize(key, False)
            # 递归处理值
            new_dict[camel_key] = dict_to_camel(value)
        return new_dict
    
    # 处理列表类型
    elif isinstance(d, list):
        return [dict_to_camel(item) for item in d]
    
    # 其他类型直接返回
    else:
        return d

# 驼峰转蛇形
def dict_to_snake(d):
    """
    递归将字典中的驼峰键转换为蛇形键，支持嵌套结构
    
    Args:
        d: 要转换的字典或列表
        
    Returns:
        转换后的新字典/列表，保持原数据结构
    """
    # 处理字典类型
    if isinstance(d, dict):
        new_dict = {}
        for key, value in d.items():
            # 转换键为蛇形格式
            snake_key = inflection.underscore(key)
            # 递归处理值
            new_dict[snake_key] = dict_to_snake(value)
        return new_dict
    
    # 处理列表类型
    elif isinstance(d, list):
        return [dict_to_snake(item) for item in d]
    
    # 其他类型直接返回
    else:
        return d

# endregion

# 全局变量：用于存储 script_dir 和其他路径
script_dir = None
rms_dir = None
project_dir = None
config_tool_dir = None
# 存储任务的状态
task_status = {}

# 任务状态枚举（统一状态标识）
TASK_STATUS = {
    "PENDING": "pending",       # 任务已创建，等待执行
    "DOWNLOADING": "downloading",  # 下载中（pull_from_cloud执行中）
    "EXTRACTING": "extracting",    # 解压中（extract_modules执行中）
    "RUNNING": "running",       # 任务执行中
    "UPDATE_SYB_DIR_SCRIPT_RUNNING": "update_syb_dir_script_running",  # 覆盖sysb文件夹脚本执行中
    "SUCCESS": "success",       # 任务执行成功
    "FAILED": "failed",         # 任务执行失败
    "TIMEOUT": "timeout",       # 任务超时
    "CANCELLED": "cancelled"    # 任务被取消（可选）
}

MODULE_NAME_MAPPING = {
    # 显示名称 → metadata下的实际目录名（关键映射）
    "Console": "robot_console",
    "System B": "sysb_core",
    "DWdeploy": "dwdeploy_lite"
}

# 全局任务状态存储（key: task_id, value: 任务状态字典）
# 生产环境建议用Redis等持久化存储，避免进程重启后状态丢失
TASK_STORAGE = {}

# 全局任务存储（key: taskId, value: 任务信息）
UPDATE_TASK_STORAGE = {}

# 生成唯一任务ID（简单实现，生产环境可改用uuid）
def generate_task_id() -> str:
    return f"task_{int(time.time() * 1000)}"  # 时间戳毫秒级，避免重复

# 获取打包后的临时目录路径
def get_static_folder():
    if getattr(sys, 'frozen', False):
        # 如果是打包后的可执行文件，使用 sys._MEIPASS
        base_path = sys._MEIPASS
    else:
        # 如果是开发环境，使用当前工作目录
        base_path = os.path.dirname(os.path.abspath(__file__))
    
    return os.path.join(base_path, 'static')

# 设置路径并导入模块
def set_paths(script_dir_value):
    global script_dir, rms_dir, project_dir, config_tool_dir
    
    # 规范化并设置脚本目录
    script_dir = os.path.normpath(os.path.abspath(script_dir_value))
    rms_dir = os.path.dirname(script_dir)
    project_dir = os.path.join(rms_dir, "project")
    config_tool_dir = os.path.join(rms_dir, "modules", "robot_console", "latest", "robot_console")
    
    os.makedirs(project_dir, exist_ok=True)
    
    # 导入模块
    global list_project, create_project, edit_project, delete_project, rc_basic_utils
    try:
        # 从scripts目录导入
        sys.path.append(script_dir)
        import list_project, create_project, edit_project, delete_project
        
        # 动态导入rc_basic_utils（从外部文件加载）
        import importlib.util
        
        # 构建rc_basic_utils.py的绝对路径
        rc_basic_utils_path = os.path.join(config_tool_dir, "rc_basic_utils.py")
        
        # 检查文件是否存在
        if not os.path.exists(rc_basic_utils_path):
            print(f"Error: {rc_basic_utils_path} does not exist!")
            sys.exit(1)
        
        # 动态导入模块
        spec = importlib.util.spec_from_file_location("rc_basic_utils", rc_basic_utils_path)
        rc_basic_utils = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(rc_basic_utils)
        
        print(f"Successfully imported rc_basic_utils from {rc_basic_utils_path}")
        
    except Exception as e:
        print(f"Error importing modules: {e}")
        import traceback
        traceback.print_exc()
        sys.exit(1)

# 配置日志
def set_logger():
    global logger
    # 配置日志输出格式
    formatter = logging.Formatter(
        '%(asctime)s.%(msecs)03d %(process)d %(levelname)05s %(module)s():%(lineno)d %(message)s',
        datefmt='%Y-%m-%d %H:%M:%S')
    logs_dir = os.path.join(config_tool_dir, "logs")
    os.makedirs(logs_dir, exist_ok=True)
    log_file_name = os.path.join(logs_dir, "info.log")
    fh = RotatingFileHandler(log_file_name, maxBytes=100 * 1024 * 1024, backupCount=3)
    fh.setLevel(logging.INFO)
    fh.setFormatter(formatter)

    logger = logging.getLogger('config_tool_logger')
    if not any(isinstance(h, RotatingFileHandler) for h in logger.handlers):
        logger.setLevel(logging.INFO)
        logger.addHandler(fh)

# region 权限控制装饰器

# 首先添加角色映射，因为配置文件中没有角色信息
# 我们通过用户名直接映射角色
USER_ROLES = {
    "system": "system",   # system用户拥有最高权限
    "admin": "admin",     # admin用户拥有管理员权限
    "user": "user"        # user用户拥有普通用户权限
}

# 定义权限等级，system > admin > user
PERMISSION_LEVELS = {
    "system": 3,
    "admin": 2,
    "user": 1
}

def require_permission(min_role):
    """
    权限控制装饰器，要求用户拥有至少min_role级别的权限
    :param min_role: 允许访问的最低角色(system, admin, user)
    """
    def decorator(f):
        @wraps(f)
        def wrapped(*args, **kwargs):
            # 获取当前用户
            current_user = getattr(request, 'current_user', None)
            if not current_user:
                return jsonify({
                    'code': 401, 
                    'message': '未认证，请先登录'
                }), 401
            
            # 获取当前用户角色
            current_role = USER_ROLES.get(current_user)
            if not current_role:
                return jsonify({
                    'code': 403, 
                    'message': '用户角色未定义'
                }), 403
            
            # 检查权限是否足够
            if PERMISSION_LEVELS[current_role] < PERMISSION_LEVELS[min_role]:
                return jsonify({
                    'code': 403, 
                    'message': f'权限不足，需要{min_role}权限'
                }), 403
            
            return f(*args, **kwargs)
        return wrapped
    return decorator
# endregion

# region JWT相关函数

# 配置JWT密钥（应从环境变量获取，这里示例使用固定值）
JWT_SECRET_KEY = secrets.token_hex(32)
# JWT_SECRET_KEY = "your-secret-key-should-be-more-complex"
JWT_EXPIRATION_DELTA = 24 * 60 * 60  # Token有效期：24小时

def generate_jwt_token(username):
    """生成JWT Token，包含用户名信息"""
    payload = {
        'username': username,
        'exp': int(time.time()) + JWT_EXPIRATION_DELTA,
        'iat': int(time.time())
    }
    return jwt.encode(payload, JWT_SECRET_KEY, algorithm='HS256')

def verify_jwt_token(token):
    """验证JWT Token"""
    try:
        payload = jwt.decode(token, JWT_SECRET_KEY, algorithms=['HS256'])
        return payload
    except jwt.ExpiredSignatureError:
        return None
    except jwt.InvalidTokenError:
        return None


@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def catch_all(path):
    static_folder = get_static_folder()
    file_path = os.path.join(static_folder, path)
    
    # 检查文件是否存在
    if os.path.isfile(file_path):
        return send_from_directory(static_folder, path)
    else:
        # 对于单页应用，所有未匹配的路由都返回index.html
        return send_from_directory(static_folder, 'index.html')

@app.before_request
def before_request():
    # 不需要Token的接口（白名单）
    public_routes = [
        'user_login_api',
        'devicemanager_ping',
        'catch_all',  # 更新白名单以包含新的通配符路由
        'user_register_api'
    ]
    
    if request.endpoint in public_routes:
        return
    
    # 检查请求头中是否有Token
    if 'Authorization' not in request.headers:
        return jsonify({'code': 401, 'message': '缺少Token，未授权访问'}), 401
    
    auth_header = request.headers['Authorization']
    if not auth_header.startswith('Bearer '):
        return jsonify({'code': 401, 'message': 'Token格式错误，应为Bearer <token>'}), 401
    
    token = auth_header.split(' ')[1]
    payload = verify_jwt_token(token)
    if not payload:
        return jsonify({'code': 401, 'message': '无效的Token或Token已过期'}), 401
    
    request.current_user = payload['username']

@app.errorhandler(jwt.exceptions.PyJWTError)
def handle_jwt_error(e):
    return jsonify({'code': 401, 'message': f'JWT验证错误: {str(e)}'}), 401

# endregion

# region Exception Handling Module
REQUEST_FAIL = 500

class GenericException(Exception):
    def __init__(self, code, message) -> None:
        self.code = code
        self.message = message

@app.errorhandler(GenericException)
def handle_exception(e:GenericException):
    return {'code': e.code, 'message': e.message}

@app.errorhandler(Exception)
def handle_general_exception(e):
    # 捕获所有其他未处理的异常
    response = jsonify({'code': -1, 'message': str(e)})
    response.status_code = 500
    return response

# endregion

# region 项目管理模块
# 列出所有项目
@app.route('/api/v1/robotconsole/project/list', methods=['GET'])
def list_project_api():
    logger.info("list_project_api")
    data = list_project.get_project_list()
    logger.info("get_project_list {}".format(data))
    return {'code': 200, 'message': 'ok', 'data': data}

# 新建一个项目
@app.route('/api/v1/robotconsole/project/add', methods=['POST'])
def add_project_api():
    try:
        # 1. 获取并校验请求参数
        param = request.get_json()
        if not param:
            logger.error("新建项目请求参数为空")
            return jsonify({'code': 400, 'message': '请求参数不能为空', 'data': {}})
        
        logger.info("新建一个项目的原始参数: {}".format(param))
        snake_param = dict_to_snake(param)
        logger.info("转换后的snake参数: {}".format(snake_param))

        # 2. 校验必填参数
        required_params = ['project_name', 'dw_ip', 'pi_ip']
        missing_params = [p for p in required_params if p not in snake_param or not snake_param[p]]
        if missing_params:
            logger.error(f"新建项目缺少必填参数: {missing_params}")
            return jsonify({
                'code': 400, 
                'message': f'缺少必填参数: {", ".join(missing_params)}', 
                'data': {}
            })

        # 3. 构造subprocess调用参数
        script_path = os.path.join(script_dir, 'create_project.py')
        cmd = [
            'python3',
            script_path,
            '--project_name', snake_param['project_name'],
            '--dw_ip', snake_param['dw_ip'],
            '--pi_ip', snake_param['pi_ip']
        ]

        # 可选参数：rms_version
        if 'rms_version' in snake_param and snake_param['rms_version']:
            cmd.extend(['--rms_version', snake_param['rms_version']])
        
        # 新增：处理capabilities参数
        if 'capabilities' in snake_param and snake_param['capabilities']:
            # 支持列表或字符串格式：
            # - 列表：["base", "nav"] → 转为 "base,nav"
            # - 字符串："base,nav" → 直接使用
            if isinstance(snake_param['capabilities'], list):
                caps_str = ','.join([str(cap).strip() for cap in snake_param['capabilities'] if cap])
            else:
                caps_str = str(snake_param['capabilities']).strip()
            
            if caps_str:  # 确保非空
                cmd.extend(['--capabilities', caps_str])
                logger.info(f"传递自定义能力列表: {caps_str}")

        logger.info(f"执行脚本命令: {' '.join(cmd)}")

        # 4. 执行子进程调用
        result = subprocess.run(
            cmd,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            encoding='utf-8',
            timeout=300
        )

        # 5. 处理执行结果
        if result.returncode == 0:
            logger.info(f"项目创建成功，脚本输出: {result.stdout}")
            return jsonify({'code': 200, 'message': 'ok', 'data': {}})
        else:
            error_msg = f"项目创建失败，脚本错误输出: {result.stderr}"
            logger.error(error_msg)
            return jsonify({
                'code': 500, 
                'message': f'执行脚本失败: {error_msg}', 
                'data': {}
            })

    except subprocess.TimeoutExpired:
        logger.error(f"执行脚本超时，命令: {' '.join(cmd)}")
        return jsonify({
            'code': 504, 
            'message': '执行脚本超时', 
            'data': {}
        })
    except Exception as e:
        logger.error(f"新建项目接口异常: {str(e)}", exc_info=True)
        return jsonify({
            'code': 500, 
            'message': f'服务器内部错误: {str(e)}', 
            'data': {}
        })

# 修改选中的项目
@app.route('/api/v1/robotconsole/project/update', methods=['POST'])
def edit_project_api():
    param = request.get_json()
    logger.info("修改选中项目的参数 {}".format(param))
    snake_param = dict_to_snake(param)
    logger.info("snake_param {}".format(snake_param))
    edit_project.edit_project(param_dict=snake_param)
    return {'code': 200, 'message': 'ok', 'data': {}}

# 删除选中的项目
@app.route('/api/v1/robotconsole/project/delete', methods=['POST'])
def delete_project_api():
    param = request.get_json()
    logger.info("删除项目的参数 {}".format(param))
    snake_param = dict_to_snake(param)
    logger.info("snake_param {}".format(snake_param))
    # password = app.config.get('ARGS').password
    delete_project.delete_project(snake_param)
    return {'code': 200, 'message': 'ok', 'data': {}}

def run_project_py(script_dir, project_name, rms_version, dw_or_pi): 
    # 构建脚本的完整路径 
    script_path = os.path.join(script_dir, 'run_project.py')
    logger.info("script_path {}".format(script_path))
    # 创建命令列表，包含Python解释器、脚本路径和附加参数 
    command = ['python3', script_path, 
               '--project', project_name+"_"+rms_version,
               '--backend', dw_or_pi.upper()]
    print(command)
    logger.info("command {}".format(command))
    try:
        result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, check=True)
        print('Return code:', result.returncode)
        logger.info('Return code: %s', result.returncode)
        if result.returncode == 0:
            print('Output:', result.stdout)
            logger.info('Return message: %s', result.stdout)
            return {'code': 200, 'message': "项目启动成功", 'data': {}}
        else:
            logger.info('Return message: %s', result.stderr)
            return {'code': REQUEST_FAIL, 'message': result.stderr, 'data': {}, 'errorCode': 10700003}, 500
    except subprocess.CalledProcessError as e:
        # 如果命令执行失败，捕获异常并返回错误信息
        print(f"Error executing script: {e.stderr}")
        logger.error(f"Error executing script: {e.stderr}")
        return {'code': REQUEST_FAIL, 'message': e.stderr, 'errorCode': 10700003}, 500

# 启动项目
@app.route('/api/v1/robotconsole/project/run', methods=['POST'])
def run_project_api():
    param = request.get_json()
    logger.info("启动项目的参数 {}".format(param))
    
    return run_project_py(script_dir, 
                   param["projectName"], 
                   param["rmsVersion"], 
                   param["dwOrPi"])

def stop_project_py(script_dir): 
    # 构建脚本的完整路径 
    script_path = os.path.join(script_dir, 'stop_project.py')
    logger.info("script_path {}".format(script_path))
    # 创建命令列表，包含Python解释器、脚本路径和附加参数 
    command = ['python3', script_path, 
               '--stop-all']
    print(command)
    logger.info("command {}".format(command))
    # 执行命令并捕获输出
    try:
        result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, check=True)
        print('Return code:', result.returncode)
        logger.info('Return code: %s', result.returncode)
        if result.returncode == 0:
            print('Output:', result.stdout)
            logger.info('Return message: %s', result.stdout)
            return {'code': 200, 'message': "项目停止成功", 'data': {}}
        else:
            logger.info('Return message: %s', result.stderr)
            return {'code': REQUEST_FAIL, 'message': result.stderr, 'data': {}}, 500
    except subprocess.CalledProcessError as e:
        # 如果命令执行失败，捕获异常并返回错误信息
        print(f"Error executing script: {e.stderr}")
        logger.error(f"Error executing script: {e.stderr}")
        return {'code': REQUEST_FAIL, 'message': e.stderr}, 500

# 停止项目
@app.route('/api/v1/robotconsole/project/stop', methods=['POST'])
def stop_project_api():
    return stop_project_py(script_dir)

# 查询正在运行的项目
@app.route('/api/v1/robotconsole/project/running', methods=['GET'])
def get_running_project_api():
    project_name = list_project.get_run_project()
    return {'code': 200, 'message': 'ok', 'data': project_name}

# 查询项目是否重名
@app.route('/api/v1/robotconsole/project/name-check', methods=['GET'])
def check_project_name_api():
    # 获取查询参数中的 project_name
    project_name = request.args.get('projectName')
    if project_name is None:
        # 如果 project_name 参数不存在，返回错误信息
        return jsonify({'code': 400, 'message': 'Missing project_name parameter', 'data': None}), 400
    
    logger.info("查询项目是否重名的参数 {}".format(project_name))
    is_exist = rc_basic_utils.project_exist(project_name)  # 假设这是检查项目是否存在的函数
    return jsonify({'code': 200, 'message': 'ok', 'data': is_exist})

# 查询所有RMS版本
@app.route('/api/v1/robotconsole/rms/list', methods=['GET'])
def get_rms_versions_api():
    # password = app.config.get('ARGS').password
    version = rc_basic_utils.get_sysb_version()
    return {'code': 200, 'message': 'ok', 'version': version}

# 查询当前机器人可激活功能列表
@app.route('/api/v1/robotconsole/factory-set/capabilities', methods=['GET'])
def get_available_capabilities_api():
    capabilities = rc_basic_utils.get_factory_set_formatted_capabilities()
    return {'code': 200, 'message': 'ok', 'data': capabilities}

# 查询当前项目激活功能列表
@app.route('/api/v1/robotconsole/project/capabilities', methods=['GET'])
def get_project_capabilities_api():
    project_full_name = request.args.get('projectFullName')
    capabilities = rc_basic_utils.get_project_capabilities(project_full_name)
    return {'code': 200, 'message': 'ok', 'data': capabilities}

def update_project_py(project_param): 
    # 构建脚本的完整路径 
    script_path = os.path.join(script_dir, 'update_project.py')
    logger.info("script_path {}".format(script_path))
    cmd = ["python3", script_path, f"--project={project_param}"]
    logger.info(f"构建update_project.py执行命令：{' '.join(cmd)}")

    try:
        result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, check=True)
        print('Return code:', result.returncode)
        logger.info('Return code: %s', result.returncode)
        if result.returncode == 0:
            print('Output:', result.stdout)
            logger.info('Return message: %s', result.stdout)
            return {'code': 200, 'message': "项目更新为新版本成功", 'data': {}}
        else:
            logger.info('Return message: %s', result.stderr)
            return {'code': REQUEST_FAIL, 'message': result.stderr, 'data': {}}, 500
    except subprocess.CalledProcessError as e:
        # 如果命令执行失败，捕获异常并返回错误信息
        print(f"Error executing script: {e.stderr}")
        logger.error(f"Error executing script: {e.stderr}")
        return {'code': REQUEST_FAIL, 'message': e.stderr}, 500

# 更新升级项目（sysb更新后，可以调用这个接口针对某个项目进行更新）
@app.route('/api/v1/robotconsole/project/upgrade', methods=['POST'])
def upgrade_project():
    param = request.get_json()
    logger.info("升级项目的参数 {}".format(param))
    project_param = param.get('projectName', '')+"_"+param.get('rmsVersion')
    return update_project_py(project_param)

# endregion

# region 登录模块
@app.route('/api/v1/robotconsole/user/register', methods=['POST'])
def user_register_api():
    param = request.get_json()
    logger.info("登录的参数 {}".format(param))
    snake_param = dict_to_snake(param)
    logger.info("snake_param {}".format(snake_param))
    rc_basic_utils.set_user_password(snake_param)
    return {'code': 200, 'message': 'ok', 'data': {}}, 200

@app.route('/api/v1/robotconsole/user/login', methods=['POST'])
def user_login_api():
    param = request.get_json()
    logger.info("登录的参数 {}".format(param))
    snake_param = dict_to_snake(param)
    logger.info("snake_param {}".format(snake_param))
    is_logged_in = rc_basic_utils.login_check(snake_param)
    if is_logged_in:
        # 密码正确，生成Token
        token = generate_jwt_token(snake_param.get('username', 'user'))

        # 确保Token是字符串类型
        if isinstance(token, bytes):
            token = token.decode('utf-8')
            
        # 读取install_status文件
        install_status = rc_basic_utils.read_install_status()
        logger.info(f"安装状态: {install_status}")
        
        return {'code': 200, 'message': 'ok', 'data': {'token': token, 'install_status': install_status}}, 200
    else:
        return {'code': 401, 'message': '密码错误', 'data': {}, 'errorCode': 10700004}, 401

@app.route('/api/v1/robotconsole/user/update', methods=['POST'])
def user_update_api():
    param = request.get_json()
    logger.info("user/update的参数 {}".format(param))
    snake_param = dict_to_snake(param)
    logger.info("snake_param {}".format(snake_param))
    is_logged_in = rc_basic_utils.login_update(snake_param)
    if is_logged_in:
        return {'code': 200, 'message': 'ok', 'data': {}}
    else:
        return {'code': 401, 'message': '密码错误', 'data': {}}, 401

#endregion

# region 状态显示模块
#获取机器人名，机器人类型，机器人IP，相机（IP，user，password）
@app.route('/api/v1/robotconsole/info/basic', methods=['GET'])
def get_basic_info_api():
    logger.info("获取基础状态")
    basic_info = rc_basic_utils.get_basic_info()
    logger.info("basic_info {}".format(basic_info))
    return {'code': 200, 'message': 'ok', 'data': basic_info}

#获取机器人本体状态
@app.route('/api/v1/robotconsole/info/robot', methods=['GET'])
def get_robot_info_api():
    robot_id, _ = rc_basic_utils.get_robot_name_type()
    logger.info(f"获取机器人id: {robot_id}")
    try:
        # 获取配置中的ROS Flask服务端口
        ros_flask_ip = app.config.get('ARGS').ros_flask_ip
        ros_flask_port = app.config.get('ARGS').ros_flask_port
        # 添加 robot_id 作为查询参数
        params = {'robot_id': robot_id}
        
        # 构建目标服务URL
        # 假设两个容器在同一网络中，可通过服务名或IP访问
        target_url = f"http://{ros_flask_ip}:{ros_flask_port}/api/v1/robotconsole/ros/info/robot"
        
        # 发送请求到另一个Flask服务
        response = requests.get(target_url, params=params, timeout=10)
        response.raise_for_status()

        response_data = response.json()
        logger.info(f"response.json(): {response_data}")
        
        # 现在alter_lidar_states_in_response可以直接返回处理后的字典
        processed_data = rc_basic_utils.alter_lidar_states_in_response(response_data)
        logger.info(f"processed_data: {processed_data}")
        
        camel_response = dict_to_camel(processed_data)
        logger.info(f"camel_response: {camel_response}")

        # 返回获取的数据
        return jsonify(camel_response), 200
        
    except requests.exceptions.RequestException as e:
        logger.error(f"请求ROS服务失败: {str(e)}")
        return jsonify({"error": "无运行中的项目，请创建或启动项目"}), 503
    except Exception as e:
        logger.exception("获取机器人信息时发生错误")
        return jsonify({"error": "获取机器人信息失败"}), 500 
    
#获取通信状态（当前连接的服务器IP，服务器ftp连通状态）
@app.route('/api/v1/robotconsole/info/comm', methods=['GET'])
def get_comm_info_api():
    logger.info("获取通信状态")
    comm_info = rc_basic_utils.get_comm_info()
    logger.info("comm_info {}".format(comm_info))
    return {'code': 200, 'message': 'ok', 'data': comm_info}

@app.route('/api/v1/robotconsole/info/comm-state', methods=['GET'])
def get_comm_state_api():
    logger.info("获取ros通信状态")
    robot_id, _ = rc_basic_utils.get_robot_name_type()
    logger.info(f"获取机器人id: {robot_id}")
    # 获取配置中的ROS Flask服务端口
    ros_flask_ip = app.config.get('ARGS').ros_flask_ip
    ros_flask_port = app.config.get('ARGS').ros_flask_port
    return rc_basic_utils.get_comm_state(ros_flask_ip, ros_flask_port, robot_id)

#获取项目状态（ 项目名，项目rms版本，项目IP （DW/PI) ）
@app.route('/api/v1/robotconsole/info/project', methods=['GET'])
def get_project_info_api():
    logger.info("获取项目状态")
    project_info = rc_basic_utils.get_project_info()
    logger.info("project_status {}".format(project_info))
    return {'code': 200, 'message': 'ok', 'data': project_info}

# endregion

# region 配置中心
#本体配置模块
#修改机器人名，机器人类型，机器人IP，相机（IP，user，password）
@app.route('/api/v1/robotconsole/info/basic/update', methods=['POST'])
def update_basic_info_api():
    param = request.get_json()
    logger.info("本体配置的参数 {}".format(param))
    snake_param = dict_to_snake(param)
    logger.info("snake_param {}".format(snake_param))
    is_updated = rc_basic_utils.update_basic_info(snake_param)
    if is_updated:
        return {'code': 200, 'message': 'ok', 'data': {}}
    else:
        return {'code': 500, 'message': '修改配置失败', 'data': {}}, 500

#能力模块配置
@app.route('/api/v1/robotconsole/info/ability', methods=['GET'])
def get_ability_info_api():
    logger.info("获取能力模块配置")
    project_full_name = request.args.get('projectFullName')
    logger.info("project_full_name {}".format(project_full_name))
    
    ability_info = rc_basic_utils.get_ability_info(project_full_name)
    return {'code': 200, 'message': 'ok', 'data': dict_to_camel(ability_info)}

@app.route('/api/v1/robotconsole/info/ability/update', methods=['POST'])
def update_ability_info_api():
    param = request.get_json()
    logger.info("更新能力模块配置的参数 {}".format(param))
    snake_param = dict_to_snake(param)
    logger.info("snake_param {}".format(snake_param))
    result = rc_basic_utils.update_ability_info(snake_param)
    project_full_name = snake_param['project_full_name']
    project_name_without_version = project_full_name.split('_')[0]
    project_version = project_full_name.split('_')[1]
    logger.info("更新能力模块配置的result {}".format(result))

    if result['update_status'] == 'success':
        logger.info("更新能力配置 success，开始重启项目")
        return run_project_py(script_dir, 
                   project_name_without_version, 
                   project_version, 
                   'dw')
    else:
        return {'code': 500, 'message': '修改配置失败', 'data': dict_to_camel(result)}, 500    

#配置出厂功能（功能激活）
@app.route('/api/v1/robotconsole/factory-set', methods=['GET'])
@require_permission('system')  # 仅system可访问
def get_factory_set_api():
    """获取出厂配置（返回 capabilities 列表及 enabled 状态）"""
    logger.info("收到获取出厂配置请求")
    return rc_basic_utils.get_factory_set()

@app.route('/api/v1/robotconsole/factory-set/update', methods=['POST'])
@require_permission('system')  # 仅system可访问
def update_factory_set_api():
    """更新出厂配置（修改 capabilities 的 enabled 状态）"""
    logger.info("收到更新出厂配置请求")
    try:
        # 1. 解析前台 JSON 参数
        if not request.is_json:
            logger.error("请求格式错误，需为 JSON")
            return {"status": "error", "message": "请求必须为 JSON 格式"}, 400
        
        param = request.get_json()
        logger.info(f"原始请求参数: {param}")
        
        # 2. 转换参数格式（驼峰式 → 蛇形，适配 Python 命名规范）
        snake_param = dict_to_snake(param)
        logger.info(f"转换后参数: {snake_param}")
        
        # 3. 调用更新逻辑
        return rc_basic_utils.update_factory_set(snake_param)
    
    except Exception as e:
        logger.error(f"处理更新请求失败: {str(e)}", exc_info=True)
        return {"status": "error", "message": f"请求处理失败: {str(e)}"}, 400

# region 配置中心-三方应用管理
#获取三方应用列表
@app.route('/api/v1/robotconsole/apps/list', methods=['GET'])
def get_app_list_api():
    """ 获取三方应用列表， 返回是否已安装、运行状态 """
    logger.info("收到获取三方应用列表")
    try:
        apps = rc_basic_utils.get_app_list()
        return jsonify({
            "code": 200,
            "message": "success",
            "data": {
                "apps": apps,
                "total": len(apps)
            }
        }), 200
    except Exception as e:
        logger.error(f"获取应用列表失败: {str(e)}")
        return jsonify({
            "code": 500,
            "message": f"服务器内部错误: {str(e)}",
            "data": None
        }), 500

# 安装第三方应用
@app.route("/api/v1/robotconsole/apps/install", methods=["POST"])
def start_install():
    """
    启动应用安装任务
    请求参数：
    {
        "app_name": "dwlite"  # 必选，应用名称
    }
    """
    logger.info(f"收到安装请求: {request.json}")

    network_check_result = rc_basic_utils.check_update_network_requriements()
    if network_check_result:
    # 如果网络检查返回了响应对象，直接返回该响应
        return network_check_result
    logger.info("check_update_network_requriements returned success")

    try:
        data = request.get_json() or {}
        app_name = data.get("app_name")

        if not app_name:
            logger.warning("app_name 不能为空")
            return jsonify({
                "code": 400,
                "msg": "app_name 不能为空",
                "data": None
            }), 400

        # 检查应用是否存在（通过 metadata 目录）
        app_metadata_path = os.path.join(os.path.dirname(rc_basic_utils.PULL_SYSB_SCRIPT_PATH), "..", "metadata", "third_party_app", app_name)
        logger.info(f"检查应用元数据目录: {app_metadata_path}")

        if not os.path.exists(app_metadata_path):
            logger.error(f"应用 {app_name} 的元数据目录不存在: {app_metadata_path}")
            return jsonify({
                "code": 404,
                "msg": f"应用 {app_name} 的元数据目录不存在: {app_metadata_path}",
                "data": None
            }), 404

        logger.info(f"应用 {app_name} 元数据目录存在")
        # 创建任务
        task_id = rc_basic_utils.create_task(app_name)

        # 异步执行拉取和安装脚本
        import threading
        logger.info(f"启动异步线程执行安装任务: {task_id}")
        thread = threading.Thread(
            target=rc_basic_utils.execute_pull_script,
            args=(app_name, task_id),
            daemon=True
        )
        thread.start()
        logger.info(f"异步线程已启动，线程ID: {thread.ident}")

        return jsonify({
            "code": 200,
            "msg": "安装任务已启动",
            "data": {
                "task_id": task_id,
                "app_name": app_name,
                "tips": "请通过 /api/v1/robotconsole/apps/install/progress 接口轮询进度"
            }
        }), 200

    except Exception as e:
        logger.error(f"启动安装任务失败: {e}", exc_info=True)
        return jsonify({
            "code": 500,
            "msg": f"启动安装任务失败: {str(e)}",
            "data": None
        }), 500

# 查询安装进度
@app.route("/api/v1/robotconsole/apps/install/progress", methods=["GET"])
def get_install_progress():
    """
    查询安装进度
    请求参数：
    ?task_id=install_dwlite_12345678
    """
    task_id = request.args.get("task_id")
    logger.info(f"收到进度查询请求: task_id={task_id}")

    try:
        if not task_id:
            logger.warning("task_id 不能为空")
            return jsonify({
                "code": 400,
                "msg": "task_id 不能为空",
                "data": None
            }), 400

        task = rc_basic_utils.get_task(task_id)
        if not task:
            logger.warning(f"任务 {task_id} 不存在")
            return jsonify({
                "code": 404,
                "msg": f"任务 {task_id} 不存在",
                "data": None
            }), 404

        logger.info(f"返回任务进度: task_id={task_id}, status={task['status']}, progress={task['progress']}")
        return jsonify({
            "code": 200,
            "msg": "success",
            "data": {
                "task_id": task["task_id"],
                "app_name": task["app_name"],
                "status": task["status"],
                "progress": task["progress"],  # 整体进度（0-100）
                "step_progress": task["step_progress"],  # 当前步骤内进度（0-100）
                "step": task["step"],  # 当前步骤（pulling/installing/completed）
                "message": task["message"],  # 进度描述
                "create_time": task["create_time"],
                "update_time": task["update_time"],
                "error": task["error"]
            }
        }), 200

    except Exception as e:
        logger.error(f"查询安装进度失败: {e}", exc_info=True)
        return jsonify({
            "code": 500,
            "msg": f"查询安装进度失败: {str(e)}",
            "data": None
        }), 500

# 启动第三方应用
@app.route("/api/v1/robotconsole/apps/start", methods=["POST"])
def start_app():
    """启动指定第三方应用"""
    data = request.get_json() or {}
    app_name = data.get("app_name")

    if not app_name:
        logger.warning("app_name 不能为空")
        return jsonify({
            "code": 400,
            "msg": "app_name 不能为空",
            "data": None
        }), 400

    try:
        result = rc_basic_utils.execute_operation_script(app_name, "start.sh")
        return jsonify({
            "code": 200 if result["success"] else 400,
            "message": result["message"],
            "data": {
                "app_name": app_name,
                "operation": "start",
                "success": result["success"],
                "script_output": result["output"]
            }
        }), 200 if result["success"] else 400
    except Exception as e:
        logger.error(f"启动应用 {app_name} 失败: {str(e)}")
        return jsonify({
            "code": 500,
            "message": f"启动应用失败: {str(e)}",
            "data": None
        }), 500

@app.route("/api/v1/robotconsole/apps/stop", methods=["POST"])
def stop_app():
    """停止指定第三方应用"""
    data = request.get_json() or {}
    app_name = data.get("app_name")

    if not app_name:
        logger.warning("app_name 不能为空")
        return jsonify({
            "code": 400,
            "msg": "app_name 不能为空",
            "data": None
        }), 400

    try:
        result = rc_basic_utils.execute_operation_script(app_name, "stop.sh")
        return jsonify({
            "code": 200 if result["success"] else 400,
            "message": result["message"],
            "data": {
                "app_name": app_name,
                "operation": "stop",
                "success": result["success"],
                "script_output": result["output"]
            }
        }), 200 if result["success"] else 400
    except Exception as e:
        logger.error(f"停止应用 {app_name} 失败: {str(e)}")
        return jsonify({
            "code": 500,
            "message": f"停止应用失败: {str(e)}",
            "data": None
        }), 500


@app.route("/api/v1/robotconsole/apps/uninstall", methods=["POST"])
def uninstall_app():
    """卸载指定第三方应用"""
    data = request.get_json() or {}
    app_name = data.get("app_name")

    if not app_name:
        logger.warning("app_name 不能为空")
        return jsonify({
            "code": 400,
            "msg": "app_name 不能为空",
            "data": None
        }), 400

    try:
        result = rc_basic_utils.execute_operation_script(app_name, "uninstall.sh")
        return jsonify({
            "code": 200 if result["success"] else 400,
            "message": result["message"],
            "data": {
                "app_name": app_name,
                "operation": "uninstall",
                "success": result["success"],
                "script_output": result["output"]
            }
        }), 200 if result["success"] else 400
    except Exception as e:
        logger.error(f"卸载应用 {app_name} 失败: {str(e)}")
        return jsonify({
            "code": 500,
            "message": f"卸载应用失败: {str(e)}",
            "data": None
        }), 500

# endregion 配置中心-三方应用管理

#endregion

# region 告警模块
@app.route('/api/v1/robotconsole/alarms/list', methods=['GET'])
def get_alarms_api():
    logger.info("获取告警")
    ros_flask_ip = app.config.get('ARGS').ros_flask_ip
    ros_flask_port = app.config.get('ARGS').ros_flask_port
    return rc_basic_utils.get_alarms(ros_flask_ip, ros_flask_port)

@app.route('/api/v1/robotconsole/alarms/respond', methods=['POST'])
def respond_alarm_api():
    logger.info("respond_alarm_api")
    param = request.get_json()
    logger.info("respond_alarm_api param {}".format(param))
    snake_param = dict_to_snake(param)
    logger.info("snake_param {}".format(snake_param))
    ros_flask_ip = app.config.get('ARGS').ros_flask_ip
    ros_flask_port = app.config.get('ARGS').ros_flask_port
    return rc_basic_utils.respond_alarm(snake_param, ros_flask_ip, ros_flask_port)

@app.route('/api/v1/robotconsole/alarms/clear-all', methods=['POST'])
def clear_all_alarms_api():
    logger.info("clear_all_alarms_api")
    ros_flask_ip = app.config.get('ARGS').ros_flask_ip
    ros_flask_port = app.config.get('ARGS').ros_flask_port
    return rc_basic_utils.clear_all_alarms(ros_flask_ip, ros_flask_port)

@app.route('/api/v1/robotconsole/alarms/read', methods=['POST'])
def read_one_alarm():
    logger.info("read_one_alarm_api")
    param = request.get_json()
    logger.info("read_one_alarm_api param {}".format(param))
    snake_param = dict_to_snake(param)
    logger.info("snake_param {}".format(snake_param))
    ros_flask_ip = app.config.get('ARGS').ros_flask_ip
    ros_flask_port = app.config.get('ARGS').ros_flask_port
    return rc_basic_utils.read_one_alarm(snake_param, ros_flask_ip, ros_flask_port)

@app.route('/api/v1/robotconsole/alarms/read-all', methods=['POST'])
def read_all_alarm():
    logger.info("read_all_alarms_api")
    ros_flask_ip = app.config.get('ARGS').ros_flask_ip
    ros_flask_port = app.config.get('ARGS').ros_flask_port
    return rc_basic_utils.read_all_alarm(ros_flask_ip, ros_flask_port)

# endregion

# region 相机转流模块
@app.route('/api/v1/robotconsole/system/camera-transfer/ni-list', methods=['GET'])
def get_network_interface_list_api():
    logger.info("获取网卡列表")
    try:
        interfaces = rc_basic_utils.get_network_interface_list()
        print("可用的网络接口:")
        for idx, iface in enumerate(interfaces, 1):
            print(f"{idx}. {iface}")
    except Exception as e:
        print(f"错误: {e}")
    
    logger.info("network_interface_list {}".format(interfaces))
    return {'code': 200, 'message': 'ok', 'data': interfaces}

@app.route('/api/v1/robotconsole/system/camera-transfer', methods=['POST'])
def camera_transfer_api():
    logger.info("camera_transfer_api")
    param = request.get_json()
    logger.info("camera_transfer_api param {}".format(param))
    snake_param = dict_to_snake(param)
    logger.info("snake_param {}".format(snake_param))
    return rc_basic_utils.camera_transfer(param_dict=snake_param)

# endregion

# region 日志打包模块
def get_tar_file_path(param_dict):
    start_date = param_dict.get('start_date')
    end_date = param_dict.get('end_date')
    module = param_dict.get('module')
    project_full_name = param_dict.get('project_full_name')
    # 创建临时打包目录
    temp_tar_dir = os.path.join(config_tool_dir, "temp_logs")
    os.makedirs(temp_tar_dir, exist_ok=True)
    tar_file_name = f"{project_full_name}_{module}_{start_date}_{end_date}.tar.gz"
    tar_file_path = os.path.join(temp_tar_dir, tar_file_name)
    return tar_file_path, tar_file_name, temp_tar_dir

def package_log_check(param_dict, temp_tar_dir):

    start_date = param_dict.get('start_date')
    end_date = param_dict.get('end_date')
    module = param_dict.get('module')
    project_full_name = param_dict.get('project_full_name')
    
    # 验证参数完整性
    if not all([start_date, end_date, module, project_full_name]):
        return Response(json.dumps({'code': 400, 'message': '缺少必要参数'}), 
                        status=400, mimetype='application/json')
    
    module_folder_name = module
    if module=='door':
        module_folder_name = 'door_control'
    # 构建文件路径
    project_folder = os.path.join(project_dir, project_full_name)
    log_folder = os.path.join(project_folder, 'logs')
    if module == "door" or module == "camera" or module == "unimotion":
        module_folder = os.path.join(log_folder, "hardware", module_folder_name)
    else:
        module_folder = os.path.join(log_folder, module_folder_name)
    
    # 验证文件夹存在
    if not os.path.exists(module_folder):
        return Response(json.dumps({'code': 404, 'message': f"模块日志文件夹不存在: {module_folder}"}), 
                        status=404, mimetype='application/json')
    
    # 解析日期
    try:
        start = datetime.strptime(start_date, "%Y%m%d")
        end = datetime.strptime(end_date, "%Y%m%d")
    except ValueError as e:
        return Response(json.dumps({'code': 400, 'message': f"日期格式错误，应为YYYYMMDD: {str(e)}"}), 
                        status=400, mimetype='application/json')
    
    # 确保开始日期不晚于结束日期
    if start > end:
        return Response(json.dumps({'code': 400, 'message': "开始日期不能晚于结束日期"}), 
                        status=400, mimetype='application/json')
    
    # 筛选符合条件的日志文件
    matched_files = []
    for filename in os.listdir(module_folder):
        if not filename.endswith(".log"):
            continue
        
        # 从文件名中提取日期部分
        date_str = None
        for i in range(len(filename)):
            if filename[i:i+8].isdigit():
                date_str = filename[i:i+8]
                break
        
        if not date_str:
            continue
        
        # 检查日期是否在范围内
        try:
            file_date = datetime.strptime(date_str, "%Y%m%d")
            if start <= file_date <= end:
                file_path = os.path.join(module_folder, filename)
                shutil.copy2(file_path, temp_tar_dir)
                logger.info(f"已拷贝文件：{filename} 从{file_path}到{temp_tar_dir}")
                matched_files.append(filename)
        except ValueError:
            continue
    
    # 检查是否找到匹配的文件
    if not matched_files:
        shutil.rmtree(temp_tar_dir, ignore_errors=True)
        return Response(json.dumps({'code': 404, 'message': f"未找到{start_date}至{end_date}范围内的日志文件"}), 
                        status=404, mimetype='application/json')

def package_log(task_id, tar_file_path, temp_tar_dir):
    """
    打包指定日期范围内的日志文件并返回文件流
    """
    try:
        # 使用tarfile模块创建.tar.gz文件
        with tarfile.open(tar_file_path, 'w:gz') as tar:
            for file in os.listdir(temp_tar_dir):
                logger.info(f"打包日志：Adding file: {file}")
                file_path = os.path.join(temp_tar_dir, file)
                tar.add(file_path, arcname=file)
                logger.info(f"打包日志：Added file: {file}")
        # 更新任务状态
        logger.info("打包日志：completed: %s", task_id)
        print(f"打包日志：completed: {task_id}")
        task_status[task_id]['status'] = 'completed'

    except Exception as e:
        task_status[task_id] = {'status': 'failed', 'error': str(e)}
        logger.error("Error running script: %s", str(e))
        print(f"Error running script: {str(e)}")
    
@app.route('/api/v1/robotconsole/system/package-log-async', methods=['POST'])
def package_log_api():
    logger.info("camera_transfer_api")
    param = request.get_json()
    logger.info("camera_transfer_api param {}".format(param))
    snake_param = dict_to_snake(param)
    logger.info("snake_param {}".format(snake_param))
    tar_file_path, tar_file_name, temp_tar_dir = get_tar_file_path(snake_param)
    logger.info(f"tar_file_path: {tar_file_path}, tar_file_name: {tar_file_name}, temp_tar_dir: {temp_tar_dir}")

    # 检查日志文件并复制到临时目录
    check_result = package_log_check(snake_param, temp_tar_dir)
    
    # 如果检查返回了错误响应，直接返回该响应
    if check_result:
        return check_result
    
    # 生成唯一的任务 ID
    task_id = str(int(time.time()))
    logger.info("task_id: %s", task_id)
    print(f"task_id: {task_id}")
    
    # 初始化任务状态
    task_status[task_id] = {'status': 'running', 
                            'tar_file_path': f'{tar_file_path}',
                            'tar_file_name': f'{tar_file_name}' }
    
    # 启动新线程运行脚本
    thread = threading.Thread(target=package_log, args=(task_id, tar_file_path, temp_tar_dir))
    thread.start()
    
    return jsonify({'taskId': task_id}), 200

@app.route('/api/v1/robotconsole/system/package-log-async/status', methods=['GET'])
def check_log_status():
    task_id = request.args.get('taskId')
    logger.info("查询task_id: %s", task_id)
    print(f"查询task_id: {task_id}")

    status = task_status.get(task_id)
    print(f"status: {status}")
    logger.info(f"status: {status}")
    if not status:
        return jsonify({'error': '异步导出log任务不存在，导出失败'}), 200

    return dict_to_camel(jsonify(status))

@app.route('/api/v1/robotconsole/system/package-log-async/file', methods=['GET'])
def get_log_file():
    task_id = request.args.get('taskId')
    logger.info("查询task_id: %s", task_id)
    
    status = task_status.get(task_id)
    if not status:
        return jsonify({'error': '异步导出log任务不存在，导出失败'}), 404
    
    current_status = status['status']
    if current_status == 'completed': 
        tar_file_path = status['tar_file_path']
        tar_file_name = status['tar_file_name']
        
        # 注册仅对当前请求有效的清理函数
        @after_this_request
        def cleanup(response):
            try:
                temp_tar_dir = os.path.dirname(tar_file_path)
                shutil.rmtree(temp_tar_dir, ignore_errors=True)
                logger.info(f"已成功删除临时目录: {temp_tar_dir}")
            except Exception as e:
                logger.error(f"删除临时目录失败: {e}")
            return response
        
        # 发送文件作为响应
        return send_file(
            tar_file_path,
            as_attachment=True,
            download_name=tar_file_name,
            mimetype='application/gzip'
        )
    
    return jsonify(status)

# endregion

# region 准备更新模块
#获取当前版本信息（版本类型+版本号）
@app.route('/api/v1/robotconsole/system/version', methods=['GET'])
def get_current_system_version_api():
    """获取当前版本信息"""
    current_system_version_yaml_path = os.path.join(rms_dir, "version.yaml")
    current_system_version = rc_basic_utils.get_system_version(current_system_version_yaml_path)
    return {"code": 200, "data": current_system_version, "message": "success"}

#检查版本更新
@app.route('/api/v1/robotconsole/system/check-update', methods=['GET'])
def check_system_update_api():
    logger.info("check_update_api")
    # 调用网络检查函数并处理返回值
    network_check_result = rc_basic_utils.check_update_network_requriements()
    if network_check_result:
        # 如果网络检查返回了响应对象，直接返回该响应
        return network_check_result
    logger.info("check_update_network_requriements returned success")
    version_comparison_list = rc_basic_utils.check_system_update()
    return {"code": 200, "data": dict_to_camel(version_comparison_list), "message": "success"}

# 检查升级锁是否存在，不存在则创建，存在则返回500
@app.route('/api/v1/robotconsole/system/check-update-lock', methods=['GET'])
def check_system_update_lock_api():
    logger.info("check_system_update_lock_api")
    success_or_not, message = rc_basic_utils.create_update_lock()
    if success_or_not:
        return {"code": 200, "data": {"okToContinue": success_or_not, "message": message}, "message": "success"}
    else:
        return {"code": 500, "data": {"okToContinue": success_or_not, "message": message}, "message": "fail"}, 500

@app.route('/api/v1/robotconsole/system/overall-update-status', methods=['GET'])
def get_system_update_overall_update_status_api():
    # 读取install_status文件
    install_status = rc_basic_utils.read_install_status()
    logger.info(f"安装状态: {install_status}")
    
    return {'code': 200, 'message': 'ok', 'data': {'install_status': install_status}}, 200    

# endregion

# region 下载更新

@app.route('/api/v1/robotconsole/system/check-download-modules', methods=['GET'])
def get_modules_need_download_api():
    result = rc_basic_utils.get_modules_need_download()
    return {"code": 200, "data": dict_to_camel(result), "message": "success"}


@app.route('/api/v1/robotconsole/system/download-update', methods=['POST'])
def download_system_update_api():
    logger.info("Enter download_system_update_api")
    try:
        # 1. 解析入参
        param = request.get_json()
        if not param:
            logger.error("Invalid request: no JSON body")
            # 写入失败状态（taskId为空，标记为接口级异常）
            rc_basic_utils.update_install_status(
                stage="download",
                sub_stage=TASK_STATUS["FAILED"],
                module_name="",
                progress=0,
                task_id="",  # 无任务ID时传空
                detail="请求参数为空",
                error_msg="请求参数为空"
            )
            return jsonify({
                "code": 400,
                "message": "请求参数为空",
                "data": None
            })
        logger.info(f"Received request param: {param}")
        
        # 2. 解析关键参数
        system_type = param.get("systemType")
        module = param.get("module", {})
        module_id = module.get("id")
        module_name = module.get("name", "")
        
        # 3. 入参校验
        if not system_type or not isinstance(module_id, int) or not isinstance(module_name, str):
            error_msg = f"参数格式错误：systemType={system_type}, module={module}"
            logger.error(error_msg)
            # 写入失败状态（无任务ID）
            rc_basic_utils.update_install_status(
                system_type = system_type,
                stage="download",
                sub_stage=TASK_STATUS["FAILED"],
                module_name=module_name,
                progress=0,
                task_id="",
                detail="参数校验失败",
                error_msg=error_msg
            )
            return jsonify({
                "code": 400,
                "message": "参数格式错误：systemType（字符串）、module.id（整数）、module.name（字符串）为必填项",
                "data": None
            })
        
        # 4. 标记是否为System Dependencies模块
        is_system_deps = (module_id == 0) and (module_name == "System Dependencies")
        
        # 5. 生成任务ID，初始化任务状态
        task_id = generate_task_id()
        TASK_STORAGE[task_id] = {
            "task_id": task_id,
            "system_type": system_type,
            "module_id": module_id,
            "module_name": module_name,
            "is_system_deps": is_system_deps,
            "status": TASK_STATUS["PENDING"],
            "progress": 0,
            "result": None,
            "error_msg": "",
            "start_time": None,
            "download_end_time": None,
            "extract_end_time": None,
            "end_time": None
        }
        
        # ========== 初始化下载状态（传入taskId） ==========
        rc_basic_utils.update_install_status(
            system_type = system_type,
            stage="download",
            sub_stage=TASK_STATUS["PENDING"],
            module_name=module_name,
            progress=0,
            task_id=task_id,  # 核心：关联任务ID
            detail=f"准备下载模块：{module_name}"
        )
        
        # 6. 启动后台线程执行下载+解压任务
        def async_download_extract():
            task_info = TASK_STORAGE[task_id]
            # ========== 更新为下载中状态（传入taskId） ==========
            rc_basic_utils.update_install_status(
                system_type = system_type,
                stage="download",
                sub_stage=TASK_STATUS["DOWNLOADING"],
                module_name=module_name,
                progress=0,
                task_id=task_id,
                detail=f"开始下载模块：{module_name}"
            )
            
            task_info["status"] = TASK_STATUS["DOWNLOADING"]
            task_info["start_time"] = time.strftime("%Y-%m-%d %H:%M:%S")
            logger.info(f"Task {task_id} started: download {module_name} (id={module_id}, is_system_deps={is_system_deps})")
            
            progress_thread = None
            if not is_system_deps:
                progress_thread = threading.Thread(target=lambda: _sync_progress(task_id))
                progress_thread.daemon = True
                progress_thread.start()

            try:
                # 第一步：执行下载
                if is_system_deps:
                    # System Dependencies：首个模块，会触发版本部署
                    success = rc_basic_utils.download_system_update_package(module_id, module_name, system_type)
                    # ========== 更新进度为100%（传入taskId） ==========
                    rc_basic_utils.update_install_status(
                        system_type = system_type,
                        stage="download",
                        sub_stage=TASK_STATUS["SUCCESS"],
                        module_name=module_name,
                        progress=100,
                        task_id=task_id,
                        detail=f"System Dependencies模块无需下载，直接完成"
                    )
                else:
                    success = rc_basic_utils.download_system_update_package(module_id, module_name, system_type)
                    task_info["download_end_time"] = time.strftime("%Y-%m-%d %H:%M:%S")
                    logger.info(f"Task {task_id} download completed")
                    
                    if success:
                        # ========== 更新为解压中状态（传入taskId） ==========
                        rc_basic_utils.update_install_status(
                            system_type = system_type,
                            stage="download",
                            sub_stage=TASK_STATUS["EXTRACTING"],
                            module_name=module_name,
                            progress=50,
                            task_id=task_id,
                            detail=f"模块{module_name}下载完成，开始解压"
                        )
                        
                        task_info["status"] = TASK_STATUS["EXTRACTING"]
                        task_info["progress"] = 50
                        
                        # 第二步：执行解压
                        extract_success = rc_basic_utils.call_extract_script(module_name)
                        task_info["extract_end_time"] = time.strftime("%Y-%m-%d %H:%M:%S")
                        
                        if extract_success:
                            task_info["progress"] = 100
                            # ========== 更新为下载成功状态（传入taskId） ==========
                            rc_basic_utils.update_install_status(
                                system_type = system_type,
                                stage="download",
                                sub_stage=TASK_STATUS["SUCCESS"],
                                module_name=module_name,
                                progress=100,
                                task_id=task_id,
                                detail=f"模块{module_name}下载并解压完成"
                            )
                        else:
                            raise RuntimeError(f"模块{module_name}解压失败")
                    else:
                        raise RuntimeError(f"模块{module_name}下载失败")
                
                # 更新任务状态（成功）
                task_info["status"] = TASK_STATUS["SUCCESS"]
                task_info["result"] = True
                task_info["progress"] = 100
                logger.info(f"Task {task_id} completed successfully")
            except Exception as e:
                # 更新任务状态（失败）
                error_msg = str(e)
                task_info["status"] = TASK_STATUS["FAILED"]
                task_info["result"] = False
                task_info["error_msg"] = error_msg
                logger.error(f"Task {task_id} failed: {error_msg}")
                # ========== 更新为下载失败状态（传入taskId） ==========
                rc_basic_utils.update_install_status(
                    system_type = system_type,
                    stage="download",
                    sub_stage=TASK_STATUS["FAILED"],
                    module_name=module_name,
                    progress=task_info["progress"],
                    task_id=task_id,
                    detail=f"模块{module_name}下载/解压失败",
                    error_msg=error_msg
                )
            finally:
                task_info["end_time"] = time.strftime("%Y-%m-%d %H:%M:%S")
                if progress_thread and progress_thread.is_alive():
                    progress_thread.join(timeout=2)
        
        def _sync_progress(task_id: str):
            """同步进度：实时更新到install_status（传入taskId）"""
            task_info = TASK_STORAGE.get(task_id)
            if not task_info or task_info["is_system_deps"]:
                logger.warning(f"Task {task_id} 是System Dependencies，无需同步进度")
                return
            
            progress_file = "/opt/sandbox/rms_pull_progress.json"
            while task_info["status"] in [TASK_STATUS["DOWNLOADING"], TASK_STATUS["EXTRACTING"]]:
                if task_info["status"] == TASK_STATUS["DOWNLOADING"] and os.path.exists(progress_file):
                    try:
                        with open(progress_file, "r", encoding="utf-8") as f:
                            progress_data = json.load(f)
                            download_percent = progress_data.get("step_percentage", 0)
                            download_percent = max(0.0, min(100.0, download_percent))
                            task_info["progress"] = round(download_percent * 0.5, 2)
                            # ========== 实时更新下载进度（传入taskId） ==========
                            rc_basic_utils.update_install_status(
                                system_type = system_type,
                                stage="download",
                                sub_stage=TASK_STATUS["DOWNLOADING"],
                                module_name=task_info["module_name"],
                                progress=task_info["progress"],
                                task_id=task_id,
                                detail=f"模块{task_info['module_name']}下载中：{download_percent}%"
                            )
                            logger.info(f"Task {task_id} 下载进度：{download_percent}% → 整体进度：{task_info['progress']}%")
                    except Exception as e:
                        logger.warning(f"Task {task_id} 读取进度文件失败：{e}")
                elif task_info["status"] == TASK_STATUS["EXTRACTING"]:
                    # ========== 更新解压中进度（传入taskId） ==========
                    rc_basic_utils.update_install_status(
                        system_type = system_type,
                        stage="download",
                        sub_stage=TASK_STATUS["EXTRACTING"],
                        module_name=task_info["module_name"],
                        progress=50,
                        task_id=task_id,
                        detail=f"模块{task_info['module_name']}解压中"
                    )
                
                time.sleep(5)

        # 启动下载+解压线程
        thread = threading.Thread(target=async_download_extract, daemon=True)
        thread.start()
        
        # 7. 返回任务ID给前端
        return jsonify({
            "code": 200,
            "message": "下载任务已启动",
            "data": {"taskId": task_id}
        })
    
    except Exception as e:
        error_msg = str(e)
        logger.error(f"download_system_update_api error: {error_msg}", exc_info=True)
        # ========== 接口异常时写入状态（无taskId） ==========
        rc_basic_utils.update_install_status(
            system_type = system_type,
            stage="download",
            sub_stage=TASK_STATUS["FAILED"],
            module_name="",
            progress=0,
            task_id="",
            detail="下载接口处理异常",
            error_msg=error_msg
        )
        return jsonify({
            "code": 500,
            "message": f"接口处理异常：{error_msg}",
            "data": None
        })
    
@app.route('/api/v1/robotconsole/system/download-update/status', methods=['GET'])
def download_system_update_status_api():
    logger.info("Enter download_system_update_status_api")
    try:
        # 1. 获取查询参数taskId（与返回字段一致，使用驼峰命名）
        task_id = request.args.get("taskId")
        if not task_id:
            logger.error("Missing required parameter: taskId")
            return jsonify({
                "code": 400,
                "message": "缺少必填参数：taskId",
                "data": None
            })
        
        # 2. 查询任务状态
        task_info = TASK_STORAGE.get(task_id)
        if not task_info:
            logger.error(f"Task {task_id} not found")
            return jsonify({
                "code": 404,
                "message": f"任务不存在：{task_id}",
                "data": None
            })
        
        # 3. 返回任务状态详情（新增解压相关字段，适配前端）
        return jsonify({
            "code": 200,
            "message": "success",
            "data": {
                "taskId": task_info["task_id"],
                "systemType": task_info["system_type"],
                "module": {
                    "id": task_info["module_id"],
                    "name": task_info["module_name"]
                },
                "status": task_info["status"],  # 可能值：pending/downloading/extracting/success/failed
                "progress": task_info["progress"],  # 0-100：下载0-50，解压50，完成100
                "result": task_info["result"],
                "errorMsg": task_info["error_msg"],
                "startTime": task_info["start_time"],
                "downloadEndTime": task_info["download_end_time"],
                "extractEndTime": task_info["extract_end_time"],
                "endTime": task_info["end_time"]
            }
        })
    
    except Exception as e:
        logger.error(f"download_system_update_status_api error: {str(e)}")
        return jsonify({
            "code": 500,
            "message": f"接口处理异常：{str(e)}",
            "data": None
        })

# endregion

# region 应用更新（运行update.sh脚本）

@app.route('/api/v1/robotconsole/system/check-install-modules', methods=['GET'])
def get_modules_need_updatesh_api():
    result = rc_basic_utils.get_modules_need_updatesh()
    return {"code": 200, "data": dict_to_camel(result), "message": "success"}

@app.route('/api/v1/robotconsole/system/run-update-sh', methods=['POST'])
def run_update_sh_api():
    logger.info("Enter run_update_sh_api")
    try:
        # 1. 解析入参
        param = request.get_json()
        if not param:
            logger.error("Invalid request: no JSON body")
            # 写入安装失败状态（无taskId）
            rc_basic_utils.update_install_status(
                stage="update",
                sub_stage=TASK_STATUS["FAILED"],
                module_name="",
                progress=0,
                task_id="",
                detail="请求参数为空",
                error_msg="请求参数为空"
            )
            return jsonify({
                "code": 400,
                "message": "请求参数为空",
                "data": None
            })
        
        module_name = param.get("name", "").strip()
        if not module_name:
            logger.error("Missing required parameter: name")
            # 写入安装失败状态（无taskId）
            rc_basic_utils.update_install_status(
                stage="update",
                sub_stage=TASK_STATUS["FAILED"],
                module_name="",
                progress=0,
                task_id="",
                detail="缺少模块名称参数",
                error_msg="缺少必填参数：name（模块名称）"
            )
            return jsonify({
                "code": 400,
                "message": "缺少必填参数：name（模块名称）",
                "data": None
            })
        
        logger.info(f"Received update request for module: {module_name}")
        
        # 2. 模块支持检查
        if module_name in ["Console", "System B"] and module_name not in MODULE_NAME_MAPPING:
            error_msg = f"不支持的系统模块：{module_name}，支持的系统模块：{[k for k in MODULE_NAME_MAPPING.keys() if k in ['Console', 'System B']]}"
            logger.error(error_msg)
            # 写入安装失败状态（无taskId）
            rc_basic_utils.update_install_status(
                stage="update",
                sub_stage=TASK_STATUS["FAILED"],
                module_name=module_name,
                progress=0,
                task_id="",
                detail="模块支持检查失败",
                error_msg=error_msg
            )
            return jsonify({
                "code": 400,
                "message": error_msg,
                "data": None
            })
        
        # 3. 生成任务ID并初始化任务状态
        task_id = generate_task_id()
        task_info = {
            "taskId": task_id,
            "moduleName": module_name,
            "status": TASK_STATUS["PENDING"],
            "progress": 0,
            "errorMsg": "",
            "startTime": None,
            "endTime": None,
            "scriptPath": "",
            "statusDesc": "任务待执行"
        }
        UPDATE_TASK_STORAGE[task_id] = task_info
        
        # ========== 初始化安装状态（传入taskId） ==========
        rc_basic_utils.update_install_status(
            stage="update",
            sub_stage=TASK_STATUS["PENDING"],
            module_name=module_name,
            progress=0,
            task_id=task_id,
            detail=f"准备安装模块：{module_name}"
        )
        
        # 4. 异步执行更新任务
        def async_run_update():
            task = UPDATE_TASK_STORAGE[task_id]
            # ========== 更新为安装中状态（传入taskId） ==========
            rc_basic_utils.update_install_status(
                stage="update",
                sub_stage=TASK_STATUS["RUNNING"],
                module_name=module_name,
                progress=0,
                task_id=task_id,
                detail=f"开始安装模块：{module_name}"
            )
            
            task["status"] = TASK_STATUS["RUNNING"]
            task["startTime"] = time.strftime("%Y-%m-%d %H:%M:%S")
            task["statusDesc"] = "更新中"
            
            try:
                # 第一步：获取脚本路径
                main_script_path = rc_basic_utils.get_update_script_path(module_name)
                task["scriptPath"] = main_script_path
                
                # 第二步：执行主更新脚本
                # ========== 更新为主脚本执行中（传入taskId） ==========
                rc_basic_utils.update_install_status(
                    stage="update",
                    sub_stage=TASK_STATUS["RUNNING"],
                    module_name=module_name,
                    progress=80,
                    task_id=task_id,
                    detail=f"模块{module_name}主更新脚本执行中"
                )
                
                task["status"] = TASK_STATUS["RUNNING"]
                task["statusDesc"] = "主更新脚本执行中"
                task["progress"] = 80
                rc_basic_utils.execute_script(main_script_path, script_type="main")
                
                is_final_module = rc_basic_utils.check_is_final_module(module_name)

                UPDATE_SYSB_DIR_SCRIPT = "/opt/sandbox/sysb-deployment/scripts/UPDATE-SYSB-DIR.sh"  # 覆盖sysb deployment文件夹脚本
                UPDATE_PROJECT_PY = "/opt/sysb/scripts/update_project.py"  # 更新sysb项目脚本
                # 若本次调用为需要安装的列表中最后一个模块，则在执行完主更新脚本后，执行覆盖文件夹路径的脚本
                if is_final_module:
                    # ========== 更新为覆盖脚本执行中（传入taskId） ==========
                    rc_basic_utils.update_install_status(
                        stage="update",
                        sub_stage=TASK_STATUS["UPDATE_SYB_DIR_SCRIPT_RUNNING"],
                        module_name=module_name,
                        progress=90,
                        task_id=task_id,
                        detail=f"模块{module_name}覆盖目录脚本执行中"
                    )
                    
                    task["status"] = TASK_STATUS["UPDATE_SYB_DIR_SCRIPT_RUNNING"]
                    task["statusDesc"] = "覆盖目录脚本执行中"
                    task["progress"] = 90
                    task["scriptPath"] = UPDATE_SYSB_DIR_SCRIPT
                    if not os.path.exists(UPDATE_SYSB_DIR_SCRIPT):
                        raise FileNotFoundError(f"覆盖目录脚本不存在：{UPDATE_SYSB_DIR_SCRIPT}")
                    rc_basic_utils.execute_script(UPDATE_SYSB_DIR_SCRIPT, script_type="update_sysb_dir")
                    # ========== 更新为update_project执行中（传入taskId） ==========
                    rc_basic_utils.update_install_status(
                        stage="update",
                        sub_stage=TASK_STATUS["UPDATE_SYB_DIR_SCRIPT_RUNNING"],
                        module_name=module_name,
                        progress=95,
                        task_id=task_id,
                        detail=f"模块{module_name}更新sysb项目执行中"
                    )
                    
                    task["status"] = TASK_STATUS["UPDATE_SYB_DIR_SCRIPT_RUNNING"]
                    task["statusDesc"] = "更新sysb项目执行中"
                    task["progress"] = 95
                    task["scriptPath"] = UPDATE_PROJECT_PY
                    if not os.path.exists(UPDATE_PROJECT_PY):
                        raise FileNotFoundError(f"更新sysb项目脚本不存在：{UPDATE_PROJECT_PY}")
                    rc_basic_utils.execute_script(UPDATE_PROJECT_PY, script_type="update_sysb_dir")

                # 第四步：任务成功
                task["status"] = TASK_STATUS["SUCCESS"]
                task["statusDesc"] = "更新成功"
                task["progress"] = 100
                # ========== 更新为安装成功状态（传入taskId） ==========
                rc_basic_utils.update_install_status(
                    stage="update",
                    sub_stage=TASK_STATUS["SUCCESS"],
                    module_name=module_name,
                    progress=100,
                    task_id=task_id,
                    detail=f"模块{module_name}安装完成"
                )
                
                logger.info(f"Task {task_id} (module: {module_name}) update completed successfully")
            
            except Exception as e:
                # 任务失败
                error_msg = str(e)
                task["status"] = TASK_STATUS["FAILED"]
                task["statusDesc"] = f"更新失败：{error_msg[:50]}..."
                task["errorMsg"] = error_msg
                logger.error(f"Task {task_id} (module: {module_name}) update failed: {error_msg}", exc_info=True)
                # ========== 更新为安装失败状态（传入taskId） ==========
                rc_basic_utils.update_install_status(
                    stage="update",
                    sub_stage=TASK_STATUS["FAILED"],
                    module_name=module_name,
                    progress=task["progress"],
                    task_id=task_id,
                    detail=f"模块{module_name}安装失败",
                    error_msg=error_msg
                )
            
            finally:
                task["endTime"] = time.strftime("%Y-%m-%d %H:%M:%S")
        
        # 启动异步线程
        thread = threading.Thread(target=async_run_update, daemon=True)
        thread.start()
        
        # 5. 返回响应
        return jsonify({
            "code": 200,
            "message": f"模块 {module_name} 更新任务已启动",
            "data": {
                "taskId": task_id,
                "moduleName": module_name,
                "initialStatus": task_info["status"],
                "statusDesc": task_info["statusDesc"]
            }
        })
    
    except Exception as e:
        error_msg = str(e)
        logger.error(f"run_update_sh_api error: {error_msg}", exc_info=True)
        # ========== 接口异常时写入状态（无taskId） ==========
        rc_basic_utils.update_install_status(
            stage="update",
            sub_stage=TASK_STATUS["FAILED"],
            module_name=module_name if 'module_name' in locals() else "",
            progress=0,
            task_id="",
            detail="安装接口处理异常",
            error_msg=error_msg
        )
        return jsonify({
            "code": 500,
            "message": f"接口处理异常：{error_msg}",
            "data": None
        })
    
@app.route('/api/v1/robotconsole/system/run-update-sh/status', methods=['GET'])
def run_update_sh_status_api():
    logger.info("Enter run_update_sh_status_api")
    try:
        # 1. 获取查询参数taskId（必填）
        task_id = request.args.get("taskId", "").strip()
        if not task_id:
            logger.error("Missing required parameter: taskId")
            return jsonify({
                "code": 400,
                "message": "缺少必填参数：taskId",
                "data": None
            })
        
        # 2. 查询任务状态（不存在返回404）
        task = UPDATE_TASK_STORAGE.get(task_id)
        if not task:
            logger.error(f"Task not found: {task_id}")
            return jsonify({
                "code": 404,
                "message": f"任务不存在：{task_id}",
                "data": None
            })
        
        # 3. 构造响应数据（保持字段驼峰命名，适配前端）
        response_data = {
            "taskId": task["taskId"],
            "moduleName": task["moduleName"],
            "status": task["status"],
            "statusDesc": task["statusDesc"],
            "progress": task["progress"],
            "errorMsg": task["errorMsg"],
            "startTime": task["startTime"],
            "endTime": task["endTime"],
            "scriptPath": task["scriptPath"]
        }
        
        logger.info(f"Query task {task_id} status: {response_data}")
        return jsonify({
            "code": 200,
            "message": "success",
            "data": response_data
        })
    
    except Exception as e:
        logger.error(f"run_update_sh_status_api error: {str(e)}", exc_info=True)
        return jsonify({
            "code": 500,
            "message": f"接口处理异常：{str(e)}",
            "data": None
        })

# endregion 应用更新（运行update.sh脚本）

# region 系统更新老逻辑-已废弃

#立即更新
@app.route('/api/v1/robotconsole/system/update', methods=['POST'])
def system_update_api():
    logger.info("system_update_api")
    param = request.get_json()
    logger.info("respond_alarm_api param {}".format(param))
    snake_param = dict_to_snake(param)
    rc_basic_utils.system_update(snake_param)
    return {"code": 200, "message": "success", "data": {}}

#获取RMS版本列表（包含字段needUpdate)
@app.route('/api/v1/robotconsole/system/rms-update-info/list', methods=['GET'])
def rms_update_info_list_api():
    logger.info("rms_update_info_list_api")
    rms_need_update_list = list_project.get_version_list_with_need_update_property()
    return {"code": 200, "message": "success", "data": rms_need_update_list}

def rms_update(task_id, snake_param):
    rms_version_list = snake_param['rms_version_list']
    # 构建脚本的完整路径 
    script_path = os.path.join(script_dir, 'pull_from_cloud.py')
    logger.info("script_path {}".format(script_path))
    # 创建命令列表
    command = ['python3', script_path, 
            '--rms_version_list']

    # 添加所有版本作为单独的参数
    command.extend(rms_version_list)
    print(command)
    logger.info("command {}".format(command))
    try:
        result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, check=True)
        print('Return code:', result.returncode)
        logger.info('Return code: %s', result.returncode)
        if result.returncode == 0:
            print('Output:', result.stdout)
            logger.info('Return message: %s', result.stdout)
            task_status[task_id]['status'] = 'completed'
        else:
            logger.info('Return message: %s', result.stderr)
            task_status[task_id] = {'status': 'failed', 'error': str(result.stderr)}
    except subprocess.CalledProcessError as e:
        # 如果命令执行失败，捕获异常并返回错误信息
        task_status[task_id] = {'status': 'failed', 'error': str(e)}
        print(f"Error executing script: {e.stderr}")
        logger.error(f"Error executing script: {e.stderr}")
        return {'code': REQUEST_FAIL, 'message': e.stderr}, 500


@app.route('/api/v1/robotconsole/system/rms-update-async', methods=['POST'])
def rms_update_async_api():
    logger.info("rms_update_async_api")
    param = request.get_json()
    logger.info("respond_alarm_api param {}".format(param))
    snake_param = dict_to_snake(param)
    logger.info("snake_param {}".format(snake_param))
    # 生成唯一的任务 ID
    task_id = str(int(time.time()))
    logger.info("task_id: %s", task_id)
    print(f"task_id: {task_id}")
    
    # 初始化任务状态
    task_status[task_id] = {'status': 'running'}
    
    # 启动新线程运行脚本
    thread = threading.Thread(target=rms_update, args=(task_id, snake_param))
    thread.start()
    
    return jsonify({'taskId': task_id}), 200

@app.route('/api/v1/robotconsole/system/rms-update-async/status', methods=['GET'])
def rms_update_async_status_api():
    logger.info("rms_update_async_status_api")
    task_id = request.args.get('taskId')
    logger.info("查询task_id: %s", task_id)
    print(f"查询task_id: {task_id}")

    status = dict_to_camel(task_status.get(task_id))
    print(f"status: {status}")
    logger.info(f"status: {status}")
    if not status:
        return jsonify({'error': '异步拉取RMS镜像任务不存在'}), 200
    
    progress = rc_basic_utils.get_rms_pull_progress()
    result = {
        'code': 200,
        'message': 'success',
        'data': {
            'status': status,
            'progress': progress
        }
    }
    return result

# endregion

# region 重启Config_tool服务模块
def restart_service(task_id, new_executable_path):
    """
    使用 nohup 创建一个独立的后台进程来执行重启，以防止当前服务被终止后重启中断。
    """
    try:
        # ========== 更新为重启中状态 ==========
        rc_basic_utils.update_install_status(
            stage="restart",
            sub_stage=TASK_STATUS["RUNNING"],
            module_name="Console",
            progress=50,
            task_id=task_id,
            detail="开始重启robot_console.service服务"
        )

        start_sh_script_path = os.path.join(config_tool_dir, "start.sh")
        
        # 创建一个临时的帮助脚本来执行重启
        helper_script_path = os.path.join(config_tool_dir, "restart_helper.sh")
        
        # 获取当前可执行文件的路径，作为 start.sh 的第一个参数
        # sys.executable 在打包后会指向可执行文件本身
        current_executable_path = sys.executable

        # 帮助脚本内容
        script_content = f"""#!/bin/bash
                            # 等待一段时间，确保API响应已经发出
                            sleep 3

                            # 调用 start.sh，传入当前路径和新路径
                            # 1. 第一个参数是当前可执行文件的路径（用于清理服务）
                            # 2. 第二个参数是新的可执行文件的路径（用于启动新服务）
                            # 3. 后续是其他配置参数
                            /bin/bash "{start_sh_script_path}" \\
                                "{current_executable_path}" \\
                                "{new_executable_path}"

                            # 自我删除
                            rm -- "$0"
                            """
        
        with open(helper_script_path, "w") as f:
            f.write(script_content)
        
        # 赋予帮助脚本执行权限
        os.chmod(helper_script_path, 0o755)

        # 使用 nohup 在后台执行帮助脚本，使其脱离当前进程
        # 这可以确保即使当前服务停止，重启脚本也能继续执行
        subprocess.Popen(["nohup", "/bin/bash", helper_script_path, "&"])

        # 注意：这里的成功/失败状态更新将无法执行，因为服务会立即被新进程终止。
        # 状态的最终确认应该由客户端在重启后查询新服务的状态来完成。
        # 但我们仍然可以记录一个“重启指令已发出”的状态。
        logger.info("重启指令已通过 nohup 发出。")

    except Exception as e:
        error_msg = str(e)
        logger.error(f"准备重启脚本时失败：{error_msg}", exc_info=True)
        # ========== 更新为重启失败状态 ==========
        rc_basic_utils.update_install_status(
            stage="restart",
            sub_stage=TASK_STATUS["FAILED"],
            module_name="Console",
            progress=50,
            task_id=task_id,
            detail="准备重启robot_console.service服务失败",
            error_msg=error_msg
        )


@app.route('/api/v1/robotconsole/system/reboot', methods=['POST'])
def system_reboot_api():
    logger.info("重启config_tool系统服务请求")
    
    new_executable_path = os.path.join(config_tool_dir, "robot_console")
    if not os.path.exists(new_executable_path):
        return jsonify({
            "status": "error", 
            "message": f"指定的路径不存在: {new_executable_path}"
        }), 400

    # 生成重启专属taskId
    task_id = generate_task_id()
    # ========== 初始化重启状态 ==========
    rc_basic_utils.update_install_status(
        stage="restart",
        sub_stage=TASK_STATUS["PENDING"],
        module_name="Console",
        progress=0,
        task_id=task_id,
        detail="准备重启robot_console.service服务"
    )
    # 启动新线程执行重启操作
    threading.Thread(target=restart_service, args=(task_id, new_executable_path), daemon=True).start()
    # 立即返回响应
    return jsonify({
        "status": "success", 
        "message": "服务正在重启，请稍后...",
        "data": {"taskId": task_id}
    })

# endregion

# region 更新后清理环境
@app.route('/api/v1/robotconsole/system/update-cleanup', methods=['POST'])
def update_cleanup_api():
    """
    SYSB升级环境清理接口（添加taskId）
    """
    # 生成清理专属taskId
    task_id = generate_task_id()
    # 1. 日志记录请求
    logger.info(f"收到升级环境清理接口请求 - /api/v1/robotconsole/system/update-cleanup (taskId: {task_id})")
    # ========== 初始化清理状态（传入taskId） ==========
    rc_basic_utils.update_install_status(
        stage="cleanup",
        sub_stage=TASK_STATUS["PENDING"],
        module_name="",
        progress=0,
        task_id=task_id,
        detail="准备清理升级环境"
    )
    
    try:
        # ========== 更新为清理中状态（传入taskId） ==========
        rc_basic_utils.update_install_status(
            stage="cleanup",
            sub_stage=TASK_STATUS["RUNNING"],
            module_name="",
            progress=50,
            task_id=task_id,
            detail="开始清理升级环境（sandbox/临时目录/锁文件等）"
        )
        
        # 2. 执行清理逻辑
        rc_basic_utils.cleanup_sysb_environment()

        # ========== 更新为清理成功状态（传入taskId） ==========
        rc_basic_utils.update_install_status(
            stage="cleanup",
            sub_stage=TASK_STATUS["SUCCESS"],
            module_name="",
            progress=100,
            task_id=task_id,
            detail="升级环境清理完成（sandbox/临时目录/锁文件/维护文件已清理）"
        )
        
        # 3. 返回成功响应（包含清理任务ID）
        response = {
            "code": 200,
            "message": "升级环境清理完成",
            "data": {
                "taskId": task_id,  # 返回清理任务ID
                "cleanup_items": [
                    "/opt/sandbox 目录内容",
                    "/opt/sysb.back-* 临时目录",
                    "/opt/sysb.new* 临时目录",
                    "/var/lock/sysb_update.lock 升级锁",
                    "维护模式运行项目文件",
                    "维护模式标记文件"
                ]
            }
        }
        logger.info(f"升级环境清理接口执行成功 (taskId: {task_id})")
        return jsonify(response), 200

    except PermissionError as e:
        # 权限错误处理
        error_msg = f"清理失败：无操作权限 - {str(e)}"
        logger.error(f"清理失败 (taskId: {task_id}): {error_msg}", exc_info=True)
        # ========== 更新为清理失败状态（传入taskId） ==========
        rc_basic_utils.update_install_status(
            stage="cleanup",
            sub_stage=TASK_STATUS["FAILED"],
            module_name="",
            progress=50,
            task_id=task_id,
            detail="清理升级环境时权限不足",
            error_msg=error_msg
        )
        return jsonify({
            "code": 403,
            "message": error_msg,
            "data": {"taskId": task_id}
        }), 403

    except Exception as e:
        # 通用异常处理
        error_msg = f"清理失败：系统异常 - {str(e)}"
        logger.error(f"清理失败 (taskId: {task_id}): {error_msg}", exc_info=True)
        # ========== 更新为清理失败状态（传入taskId） ==========
        rc_basic_utils.update_install_status(
            stage="cleanup",
            sub_stage=TASK_STATUS["FAILED"],
            module_name="",
            progress=50,
            task_id=task_id,
            detail="清理升级环境时系统异常",
            error_msg=error_msg
        )
        return jsonify({
            "code": 500,
            "message": error_msg,
            "data": {
                "taskId": task_id,
                "traceback": traceback.format_exc() if app.config.get('DEBUG') else None
            }
        }), 500

# endregion

# region 离线更新模块
def get_safe_path(user_path):
    BASE_DIR = os.path.dirname(os.path.dirname(script_dir))
    """确保用户访问的路径在BASE_DIR范围内，防止路径遍历攻击"""
    # 拼接基础目录和用户请求的路径
    combined_path = os.path.join(BASE_DIR, user_path)
    # 规范化路径
    normalized_path = os.path.abspath(combined_path)
    
    # 检查是否在允许的目录范围内
    if not normalized_path.startswith(os.path.abspath(BASE_DIR)):
        return None
    
    return normalized_path

def get_parent_path(current_path):
    """获取当前路径的父路径"""
    if not current_path:  # 如果是根目录，没有父路径
        return None
    
    # 分割路径
    path_parts = current_path.split('/')
    # 移除最后一部分
    parent_parts = path_parts[:-1]
    # 重新组合
    return '/'.join(parent_parts) if parent_parts else ''

@app.route('/api/v1/robotconsole/system/files')
def get_files():
    try:
        BASE_DIR = os.path.dirname(os.path.dirname(script_dir))
        DEFAULT_PATH = os.path.join(BASE_DIR, '')  # 根目录
        # 获取请求的路径，默认为DEFAULT_PATH
        user_path = request.args.get('path', DEFAULT_PATH)
        
        # 安全处理路径
        safe_path = get_safe_path(user_path)
        if not safe_path:
            return jsonify({
                'success': False,
                'error': '访问被拒绝：路径超出允许范围'
            })
        
        # 检查路径是否存在且是目录
        if not os.path.exists(safe_path) or not os.path.isdir(safe_path):
            return jsonify({
                'success': False,
                'error': '目录不存在'
            })
        
        # 获取目录内容
        contents = []
        for item in os.listdir(safe_path):
            item_path = os.path.join(safe_path, item)
            relative_path = os.path.join(user_path, item) if user_path else item
            
            # 获取项目信息
            is_directory = os.path.isdir(item_path)
            size = os.path.getsize(item_path) if not is_directory else None
            
            contents.append({
                'name': item,
                'path': relative_path,
                'is_directory': is_directory,
                'size': size
            })
        
        # 排序：目录在前，文件在后，按名称排序
        contents.sort(key=lambda x: (not x['is_directory'], x['name'].lower()))
        
        # 获取父路径
        parent_path = get_parent_path(user_path)
        
        return jsonify({
            'success': True,
            'current_path': user_path,
            'parent_path': parent_path,
            'contents': contents
        })
    
    except Exception as e:
        return jsonify({
            'success': False,
            'error': str(e)
        })

@app.route('/api/v1/robotconsole/system/files/select', methods=['POST'])
def select_file():
    try:
        data = request.get_json()
        file_path = data.get('path', '')
        
        # 安全处理路径
        safe_path = get_safe_path(file_path)
        if not safe_path:
            return jsonify({
                'success': False,
                'error': '访问被拒绝：路径超出允许范围'
            })
        
        # 检查文件是否存在
        if not os.path.exists(safe_path):
            return jsonify({
                'success': False,
                'error': '文件不存在'
            })
        
        # 这里可以添加处理选中文件的逻辑
        # 例如记录日志、读取文件内容等
        
        return jsonify({
            'success': True,
            'message': '文件已选择',
            'path': file_path,
            'full_path': safe_path
        })
    
    except Exception as e:
        return jsonify({
            'success': False,
            'error': str(e)
        })
# endregion 离线更新模块

# region 以下为devicemanager调用的接口
@app.route('/api/v1/robotconsole/devicemanager/user/update', methods=['POST'])
def devicemanager_user_update_api():
    param = request.get_json()
    logger.info("user/update的参数 {}".format(param))
    print("user/update的参数 {}".format(param))
    snake_param = dict_to_snake(param)
    logger.info("snake_param {}".format(snake_param))
    print("snake_param {}".format(snake_param))
    is_logged_in = rc_basic_utils.login_update(snake_param)
    if is_logged_in:
        return {'code': 200, 'message': 'ok', 'data': {}}
    else:
        return {'code': 401, 'message': '密码错误', 'data': {}}, 401

#获取机器人名，机器人类型，机器人IP，相机（IP，user，password）
@app.route('/api/v1/robotconsole/devicemanager/robot-info', methods=['GET'])
def devicemanager_get_basic_info_api():
    logger.info("获取基础状态")
    basic_info = rc_basic_utils.get_basic_info()
    basic_info = rc_basic_utils.build_basic_info_with_rtsp_url(basic_info)
    del basic_info['robotIp']
    logger.info("basic_info {}".format(basic_info))
    return {'code': 200, 'message': 'ok', 'data': basic_info}

#机器人发现
@app.route('/api/v1/robotconsole/devicemanager/ping', methods=['GET'])
def devicemanager_ping():
    logger.info("获取基础状态")
    robot_name, robot_type = rc_basic_utils.get_robot_name_type()
    logger.info("机器人名称 {}".format(robot_name))
    logger.info("机器人类型 {}".format(robot_type))

    project_info = rc_basic_utils.get_project_info()
    current_project = project_info["projectName"]
    isRunningProject = 0
    currentServer = ""
    if current_project != "":
        isRunningProject = 1
        currentServer = project_info["serverType"]
    return {'code': 200, 'message': 'ok', 'data': {'robotName': robot_name, 'robotType': robot_type, 'isRunningProject': isRunningProject, 'currentServer': currentServer}}

def switch_backend_py(script_dir, project_name, rms_version, dw_or_pi): 
    # 构建脚本的完整路径 
    script_path = os.path.join(script_dir, 'switch_backend.py')
    logger.info("script_path {}".format(script_path))
    # 创建命令列表，包含Python解释器、脚本路径和附加参数 
    command = ['python3', script_path, 
               '--project', project_name+"_"+rms_version,
               '--backend', dw_or_pi.upper()]
    print(command)
    logger.info("command {}".format(command))
    try:
        result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, check=True)
        print('Return code:', result.returncode)
        logger.info('Return code: %s', result.returncode)
        if result.returncode == 0:
            print('Output:', result.stdout)
            logger.info('Return message: %s', result.stdout)
            return {'code': 200, 'message': "项目启动成功", 'data': {}}
        else:
            logger.info('Return message: %s', result.stderr)
            return {'code': REQUEST_FAIL, 'message': result.stderr, 'data': {}, 'errorCode': 10700003}, 500
    except subprocess.CalledProcessError as e:
        # 如果命令执行失败，捕获异常并返回错误信息
        print(f"Error executing script: {e.stderr}")
        logger.error(f"Error executing script: {e.stderr}")
        return {'code': REQUEST_FAIL, 'message': e.stderr, 'errorCode': 10700003}, 500


#机器人注册
@app.route('/api/v1/robotconsole/devicemanager/register', methods=['POST'])
def devicemanager_register():
    logger.info("注册机器人")
    project_name = list_project.get_run_project()
    if project_name == "":
        return {'code': 400, 'message': '请先启动项目', 'errorCode': 10700001}, 400

    param = request.get_json()
    logger.info(f"注册参数: {param}")
    snake_param = dict_to_snake(param)
    logger.info(f"snake_param: {snake_param}")
    try:
        rc_basic_utils.edit_project_ip_env(snake_param)
    except Exception as e:
        logger.error(f"更新项目信息失败: {e}")
        return {'code': 400, 'message': '更新项目信息失败', 'errorCode': 10700002}, 400

    project_name_without_version = project_name.split("_")[0]
    project_version = project_name.split("_")[1]

    return switch_backend_py(script_dir, 
                   project_name_without_version, 
                   project_version, 
                   snake_param['server_type'])

@app.route('/api/v1/robotconsole/devicemanager/project-info', methods=['GET'])
def devicemanager_get_project_info_api():
    logger.info("获取项目状态")
    project_info = rc_basic_utils.get_project_info()
    logger.info("project_info {}".format(project_info))
    return {'code': 200, 'message': 'ok', 'data': project_info}

# endregion

def main():
    # 创建 ArgumentParser 对象
    parser = argparse.ArgumentParser(description="RMS Client配置工具")
    
    # 添加命令行参数
    parser.add_argument('--ip', type=str, default='0.0.0.0', help='配置工具 IP address')
    parser.add_argument('--port', type=int, default=5000, help='配置工具端口号port')
    parser.add_argument('--ros_flask_port', type=int, default=5050, help='ros_flask端口号port')
    parser.add_argument('--ros_flask_ip', type=str, default='127.0.0.1', help='ros_flask_ip')
    # parser.add_argument('--password', type=str, required=True, help='当前用户密码')
    parser.add_argument('--script_dir', type=str, required=True, help='RMS scripts文件夹绝对路径')

    # 解析命令行参数
    args = parser.parse_args()

    
    app.config['ARGS'] = args

    # 设置路径和导入模块
    set_paths(args.script_dir)
    set_logger()
    
    # 打印接收到的参数
    print(f"parsed args: {args}")
    logger.info(f"parsed args: {args}")
    # 启动 Flask 应用
    # app.run(host=args.ip, port=args.port)
    # app.run(debug=True, host="0.0.0.0", port=5000, use_reloader=False)
    app.run(debug=True, host=args.ip, port=args.port, use_reloader=False)


# 如果直接运行此脚本，则启动应用
if __name__ == '__main__':
    main()
