#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称：deploy_service.py
模块描述：自动化部署测试模块业务服务层

作者：czj
创建日期：2026-08-12
最后修改：2026-08-12
"""

import asyncio
import logging
import threading
import traceback
from datetime import datetime
from typing import Optional, List, Tuple, Dict, Any

from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, desc, delete

from app.models.deploy_server import DeployServer, encrypt_password, decrypt_password
from app.models.deploy_script import DeployScript
from app.models.deploy_execution import DeployExecution
from app.models.deploy_log import DeployLog
from app.schemas.deploy import (
    DeployServerCreate, DeployServerUpdate,
    DeployScriptCreate, DeployScriptUpdate,
    DeployExecutionCreate,
)
from app.executors.deploy_executor import DeployExecutor
from app.websocket.manager import manager
from app.utils.id_generator import generate_id

logger = logging.getLogger(__name__)


class DeployService:
    """
    自动化部署测试服务

    提供服务器管理、脚本配置管理、执行记录管理、部署执行等核心业务功能。
    CRUD 操作均为异步，部署执行在专用线程中进行以避免阻塞事件循环。
    """

    def __init__(self, db: AsyncSession):
        """
        初始化部署服务

        Args:
            db (AsyncSession): 数据库会话对象
        """
        self.db = db

    # ==================== 服务器管理 ====================

    async def list_servers(
        self,
        skip: int = 0,
        limit: int = 50,
        keyword: Optional[str] = None,
    ) -> Tuple[List[DeployServer], int]:
        """
        获取服务器列表

        Args:
            skip: 跳过记录数
            limit: 返回记录数上限
            keyword: 搜索关键词（名称/主机地址）

        Returns:
            Tuple[List[DeployServer], int]: (服务器列表, 总数)
        """
        conditions = []
        if keyword:
            conditions.append(
                DeployServer.name.contains(keyword) |
                DeployServer.host.contains(keyword)
            )

        # 查询总数
        count_query = select(func.count(DeployServer.id))
        if conditions:
            count_query = count_query.where(*conditions)
        total_result = await self.db.execute(count_query)
        total = total_result.scalar() or 0

        # 查询列表
        query = (
            select(DeployServer)
            .order_by(desc(DeployServer.updated_at))
            .offset(skip)
            .limit(limit)
        )
        if conditions:
            query = query.where(*conditions)

        result = await self.db.execute(query)
        servers = list(result.scalars().all())

        # 返回时脱敏
        return servers, total

    async def get_server(self, server_id: str) -> Optional[DeployServer]:
        """
        获取服务器详情

        Args:
            server_id: 服务器 ID

        Returns:
            Optional[DeployServer]: 服务器对象
        """
        query = select(DeployServer).where(DeployServer.id == server_id)
        result = await self.db.execute(query)
        return result.scalar_one_or_none()

    async def create_server(self, data: DeployServerCreate) -> DeployServer:
        """
        创建服务器

        Args:
            data: 创建服务器请求数据

        Returns:
            DeployServer: 创建的服务器对象
        """
        server = DeployServer(
            id=generate_id("srv"),
            name=data.name,
            host=data.host,
            port=data.port,
            username=data.username,
            auth_type=data.auth_type or "password",
            description=data.description,
        )

        # 加密保存密码
        if data.password:
            server.password_encrypted = encrypt_password(data.password)

        # 保存私钥
        if data.private_key:
            server.private_key = data.private_key

        self.db.add(server)
        await self.db.flush()
        await self.db.refresh(server)
        logger.info(f"[DeployService] 创建服务器: {server.name} ({server.host})")
        return server

    async def update_server(
        self, server_id: str, data: DeployServerUpdate
    ) -> Optional[DeployServer]:
        """
        更新服务器信息

        Args:
            server_id: 服务器 ID
            data: 更新数据（所有字段可选）

        Returns:
            Optional[DeployServer]: 更新后的服务器对象
        """
        server = await self.get_server(server_id)
        if not server:
            return None

        update_data = data.model_dump(exclude_unset=True)

        # 密码更新需要加密
        if "password" in update_data:
            if update_data["password"]:
                server.password_encrypted = encrypt_password(
                    update_data["password"]
                )
            else:
                server.password_encrypted = None
            del update_data["password"]

        # 其他字段直接更新
        for field, value in update_data.items():
            if field == "private_key":
                server.private_key = value
            else:
                setattr(server, field, value)

        server.updated_at = datetime.utcnow()
        await self.db.flush()
        await self.db.refresh(server)
        logger.info(f"[DeployService] 更新服务器: {server.id}")
        return server

    async def delete_server(self, server_id: str) -> bool:
        """
        删除服务器

        Args:
            server_id: 服务器 ID

        Returns:
            bool: 是否删除成功
        """
        server = await self.get_server(server_id)
        if not server:
            return False

        await self.db.delete(server)
        await self.db.flush()
        logger.info(f"[DeployService] 删除服务器: {server.id}")
        return True

    async def test_server_connection(self, server_id: str) -> Dict[str, Any]:
        """
        测试服务器 SSH 连接

        Args:
            server_id: 服务器 ID

        Returns:
            Dict: 连接测试结果 {success, message, server_name, host, port}
        """
        server = await self.get_server(server_id)
        if not server:
            return {
                "success": False,
                "message": "服务器不存在",
                "server_name": "",
                "host": "",
                "port": 22,
            }

        # 解密密码
        password = None
        if server.password_encrypted:
            try:
                password = decrypt_password(server.password_encrypted)
            except Exception:
                pass

        # 创建临时执行器测试连接
        executor = DeployExecutor(
            host=server.host,
            port=server.port,
            username=server.username,
            password=password,
            private_key=server.private_key if server.auth_type == "key" else None,
            timeout=30,
        )

        try:
            executor._connect()
            server.status = "online"
            result = {
                "success": True,
                "message": "连接成功",
                "server_name": server.name,
                "host": server.host,
                "port": server.port,
            }
        except Exception as e:
            server.status = "offline"
            result = {
                "success": False,
                "message": f"连接失败: {str(e)}",
                "server_name": server.name,
                "host": server.host,
                "port": server.port,
            }
        finally:
            executor.close()
            server.updated_at = datetime.utcnow()
            await self.db.flush()

        return result

    # ==================== 脚本管理 ====================

    async def list_scripts(
        self,
        skip: int = 0,
        limit: int = 50,
        server_id: Optional[str] = None,
        keyword: Optional[str] = None,
    ) -> Tuple[List[DeployScript], int]:
        """
        获取脚本配置列表

        Args:
            skip: 跳过记录数
            limit: 返回记录数上限
            server_id: 按服务器筛选
            keyword: 搜索关键词

        Returns:
            Tuple[List[DeployScript], int]: (脚本列表, 总数)
        """
        conditions = []
        if server_id:
            conditions.append(DeployScript.server_id == server_id)
        if keyword:
            conditions.append(DeployScript.name.contains(keyword))

        count_query = select(func.count(DeployScript.id))
        if conditions:
            count_query = count_query.where(*conditions)
        total_result = await self.db.execute(count_query)
        total = total_result.scalar() or 0

        query = (
            select(DeployScript)
            .order_by(desc(DeployScript.updated_at))
            .offset(skip)
            .limit(limit)
        )
        if conditions:
            query = query.where(*conditions)

        result = await self.db.execute(query)
        return list(result.scalars().all()), total

    async def get_script(self, script_id: str) -> Optional[DeployScript]:
        """
        获取脚本配置详情

        Args:
            script_id: 脚本配置 ID

        Returns:
            Optional[DeployScript]: 脚本配置对象
        """
        query = select(DeployScript).where(DeployScript.id == script_id)
        result = await self.db.execute(query)
        return result.scalar_one_or_none()

    async def create_script(self, data: DeployScriptCreate) -> DeployScript:
        """
        创建脚本配置

        Args:
            data: 创建脚本配置请求数据

        Returns:
            DeployScript: 创建的脚本配置对象
        """
        script = DeployScript(
            id=generate_id("script"),
            name=data.name,
            description=data.description,
            server_id=data.server_id,
            remote_command=data.remote_command,
            work_dir=data.work_dir,
            timeout=data.timeout,
            expect_answers=[a.model_dump() for a in data.expect_answers],
        )
        self.db.add(script)
        await self.db.flush()
        await self.db.refresh(script)
        logger.info(f"[DeployService] 创建脚本: {script.name}")
        return script

    async def update_script(
        self, script_id: str, data: DeployScriptUpdate
    ) -> Optional[DeployScript]:
        """
        更新脚本配置

        Args:
            script_id: 脚本配置 ID
            data: 更新数据

        Returns:
            Optional[DeployScript]: 更新后的脚本配置对象
        """
        script = await self.get_script(script_id)
        if not script:
            return None

        update_data = data.model_dump(exclude_unset=True)

        # expect_answers 需要转为 dict 列表
        if "expect_answers" in update_data and update_data["expect_answers"] is not None:
            update_data["expect_answers"] = [
                a.model_dump() for a in update_data["expect_answers"]
            ]

        for field, value in update_data.items():
            if value is not None:
                setattr(script, field, value)

        script.updated_at = datetime.utcnow()
        await self.db.flush()
        await self.db.refresh(script)
        return script

    async def delete_script(self, script_id: str) -> bool:
        """
        删除脚本配置

        Args:
            script_id: 脚本配置 ID

        Returns:
            bool: 是否删除成功
        """
        script = await self.get_script(script_id)
        if not script:
            return False

        await self.db.delete(script)
        await self.db.flush()
        return True

    # ==================== 执行记录管理 ====================

    async def list_executions(
        self,
        skip: int = 0,
        limit: int = 50,
        script_id: Optional[str] = None,
        status: Optional[str] = None,
    ) -> Tuple[List[DeployExecution], int]:
        """
        获取执行记录列表

        Args:
            skip: 跳过记录数
            limit: 返回记录数上限
            script_id: 按脚本筛选
            status: 按状态筛选

        Returns:
            Tuple[List[DeployExecution], int]: (执行记录列表, 总数)
        """
        conditions = []
        if script_id:
            conditions.append(DeployExecution.script_id == script_id)
        if status:
            conditions.append(DeployExecution.status == status)

        count_query = select(func.count(DeployExecution.id))
        if conditions:
            count_query = count_query.where(*conditions)
        total_result = await self.db.execute(count_query)
        total = total_result.scalar() or 0

        query = (
            select(DeployExecution)
            .order_by(desc(DeployExecution.created_at))
            .offset(skip)
            .limit(limit)
        )
        if conditions:
            query = query.where(*conditions)

        result = await self.db.execute(query)
        return list(result.scalars().all()), total

    async def get_execution(self, execution_id: str) -> Optional[DeployExecution]:
        """
        获取执行记录详情

        Args:
            execution_id: 执行记录 ID

        Returns:
            Optional[DeployExecution]: 执行记录对象
        """
        query = select(DeployExecution).where(DeployExecution.id == execution_id)
        result = await self.db.execute(query)
        return result.scalar_one_or_none()

    async def create_execution(self, data: DeployExecutionCreate) -> DeployExecution:
        """
        创建执行记录（还未开始执行）

        Args:
            data: 创建执行记录请求数据

        Returns:
            DeployExecution: 创建的执行记录对象
        """
        # 获取脚本和服务器信息用于快照
        script = await self.get_script(data.script_id)
        if not script:
            raise ValueError(f"脚本配置不存在: {data.script_id}")

        # 获取关联的服务器（通过脚本的 server_id）
        server_query = select(DeployServer).where(DeployServer.id == script.server_id)
        server_result = await self.db.execute(server_query)
        server = server_result.scalar_one_or_none()

        execution = DeployExecution(
            id=generate_id("deploy_exec"),
            script_id=script.id,
            server_id=script.server_id,
            script_name=script.name,
            server_name=server.name if server else "",
            server_host=server.host if server else "",
            remote_command=script.remote_command,
            status="pending",
            trigger_by=data.trigger_by,
        )
        self.db.add(execution)
        await self.db.flush()
        await self.db.refresh(execution)
        logger.info(f"[DeployService] 创建执行记录: {execution.id}")
        return execution

    async def delete_execution(self, execution_id: str) -> bool:
        """
        删除执行记录

        Args:
            execution_id: 执行记录 ID

        Returns:
            bool: 是否删除成功
        """
        execution = await self.get_execution(execution_id)
        if not execution:
            return False

        await self.db.delete(execution)
        await self.db.flush()
        return True

    async def get_execution_logs(
        self,
        execution_id: str,
        skip: int = 0,
        limit: int = 5000,
    ) -> Tuple[List[DeployLog], int]:
        """
        获取执行日志

        Args:
            execution_id: 执行记录 ID
            skip: 跳过记录数
            limit: 返回记录数上限

        Returns:
            Tuple[List[DeployLog], int]: (日志列表, 总数)
        """
        conditions = [DeployLog.execution_id == execution_id]

        count_query = select(func.count(DeployLog.id)).where(*conditions)
        total_result = await self.db.execute(count_query)
        total = total_result.scalar() or 0

        query = (
            select(DeployLog)
            .where(*conditions)
            .order_by(DeployLog.timestamp.asc())
            .offset(skip)
            .limit(limit)
        )
        result = await self.db.execute(query)
        return list(result.scalars().all()), total

    # ==================== 核心执行逻辑 ====================

    # 存放正在运行的执行器，用于取消操作
    _running_executors: Dict[str, DeployExecutor] = {}
    _running_executors_lock = threading.Lock()

    async def run_execution(self, execution_id: str) -> DeployExecution:
        """
        执行部署任务

        在专用线程中执行 SSH 部署，通过 WebSocket 实时推送日志。
        使用 asyncio.run_coroutine_threadsafe 实现跨线程广播。

        Args:
            execution_id: 执行记录 ID

        Returns:
            DeployExecution: 更新后的执行记录

        Raises:
            ValueError: 执行记录不存在或状态异常
        """
        execution = await self.get_execution(execution_id)
        if not execution:
            raise ValueError(f"执行记录不存在: {execution_id}")

        if execution.status not in ("pending", "failed", "cancelled"):
            raise ValueError(
                f"执行记录状态异常，无法执行: {execution.status}"
            )

        # 获取脚本配置
        script = await self.get_script(execution.script_id)
        if not script:
            raise ValueError(f"脚本配置不存在: {execution.script_id}")

        # 获取服务器信息
        server = await self.get_server(execution.server_id)
        if not server:
            raise ValueError(f"目标服务器不存在: {execution.server_id}")

        # 解密密码
        password = None
        if server.password_encrypted:
            try:
                password = decrypt_password(server.password_encrypted)
            except Exception as e:
                raise ValueError(f"密码解密失败: {e}")

        # 更新状态
        execution.status = "running"
        execution.start_time = datetime.utcnow()
        await self.db.flush()

        # 广播执行开始
        await manager.broadcast(execution_id, {
            "type": "deploy_start",
            "data": {
                "execution_id": execution_id,
                "script_name": script.name,
                "server_name": server.name,
                "server_host": server.host,
                "message": "开始执行部署任务",
            },
        })

        # 创建执行器
        executor = DeployExecutor(
            host=server.host,
            port=server.port,
            username=server.username,
            password=password,
            private_key=server.private_key if server.auth_type == "key" else None,
            timeout=script.timeout,
            expect_answers=script.expect_answers or [],
        )

        # 注册到全局运行池
        loop = asyncio.get_event_loop()
        with self._running_executors_lock:
            self._running_executors[execution_id] = executor

        # 保存日志到数据库
        def save_log(level: str, message: str, is_sensitive: bool = False):
            """保存日志到数据库（在线程中调用）"""
            log_entry = DeployLog(
                id=generate_id("deploy_log"),
                execution_id=execution_id,
                level=level,
                message=message,
                is_sensitive=is_sensitive,
            )
            # 使用新的异步会话添加日志
            try:
                asyncio.run_coroutine_threadsafe(
                    self._add_log(log_entry), loop
                )
            except Exception as e:
                logger.error(f"[DeployService] 保存日志失败: {e}")

        # 在专用线程中执行 SSH 部署
        result_holder: Dict[str, Any] = {}

        def _deploy_worker():
            try:
                executor.set_broadcast(loop, manager.broadcast)

                # 保存初始日志
                save_log("INFO", f"准备连接服务器 {server.host}:{server.port}")

                result = executor.execute(
                    execution_id=execution_id,
                    command=script.remote_command,
                    work_dir=script.work_dir,
                )
                result_holder["result"] = result
            except Exception as e:
                logger.error(
                    f"[DeployService] 部署执行异常: {traceback.format_exc()}"
                )
                result_holder["error"] = str(e)
            finally:
                with self._running_executors_lock:
                    self._running_executors.pop(execution_id, None)

        worker_thread = threading.Thread(
            target=_deploy_worker,
            name=f"deploy-exec-{execution_id}",
            daemon=True,
        )
        worker_thread.start()

        # 主协程等待线程结束
        await loop.run_in_executor(None, worker_thread.join)

        # 线程执行完毕，处理结果
        if "error" in result_holder:
            execution.status = "failed"
            execution.error_message = result_holder["error"]
            await manager.broadcast(execution_id, {
                "type": "deploy_error",
                "data": {
                    "execution_id": execution_id,
                    "error": result_holder["error"],
                },
            })
        else:
            result = result_holder.get("result", {})
            exit_code = result.get("exit_code")

            if exit_code == 0:
                execution.status = "completed"
                await manager.broadcast(execution_id, {
                    "type": "deploy_complete",
                    "data": {
                        "execution_id": execution_id,
                        "exit_code": exit_code,
                        "duration": result.get("duration", 0),
                        "message": "部署执行成功",
                    },
                })
            elif exit_code == -1 and result.get("error_message") == "执行已被用户取消":
                execution.status = "cancelled"
                execution.error_message = "执行已被用户取消"
            elif exit_code == -1:
                execution.status = "failed"
                execution.error_message = result.get(
                    "error_message", "SSH 连接失败"
                )
                await manager.broadcast(execution_id, {
                    "type": "deploy_error",
                    "data": {
                        "execution_id": execution_id,
                        "error": result.get("error_message", ""),
                    },
                })
            else:
                execution.status = "failed"
                execution.error_message = result.get(
                    "error_message", f"执行失败，退出码: {exit_code}"
                )
                await manager.broadcast(execution_id, {
                    "type": "deploy_error",
                    "data": {
                        "execution_id": execution_id,
                        "error": result.get("error_message", ""),
                        "exit_code": exit_code,
                    },
                })

            execution.exit_code = exit_code

        execution.end_time = datetime.utcnow()
        execution.duration = result_holder.get("result", {}).get("duration", 0.0)
        await self.db.flush()
        await self.db.refresh(execution)

        return execution

    async def _add_log(self, log_entry: DeployLog) -> None:
        """
        异步添加日志到数据库
        在 run_in_executor 线程中通过 asyncio.run_coroutine_threadsafe 调用

        Args:
            log_entry: 日志条目
        """
        try:
            self.db.add(log_entry)
            await self.db.flush()
        except Exception as e:
            logger.error(f"[DeployService] 保存日志失败: {e}")

    async def cancel_execution(self, execution_id: str) -> bool:
        """
        取消正在执行的部署任务

        Args:
            execution_id: 执行记录 ID

        Returns:
            bool: 是否成功取消
        """
        executor = None
        with self._running_executors_lock:
            executor = self._running_executors.get(execution_id)

        if not executor:
            # 尝试更新状态
            execution = await self.get_execution(execution_id)
            if execution and execution.status == "running":
                execution.status = "cancelled"
                execution.error_message = "执行已被用户取消"
                execution.end_time = datetime.utcnow()
                await self.db.flush()
                return True
            return False

        executor.cancel()
        logger.info(f"[DeployService] 已发送取消信号: {execution_id}")
        return True

    async def get_running_executions(self) -> List[Dict[str, Any]]:
        """
        获取正在执行的部署任务列表

        Returns:
            List[Dict]: 运行中的执行信息列表
        """
        with self._running_executors_lock:
            return [
                {"execution_id": eid, "status": "running"}
                for eid in self._running_executors
            ]