#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称：target_resource_monitor.py
模块描述：目标机资源监控器 —— 通过 SSH 采集被测系统（目标机）的 CPU/内存/负载指标，
         并采集指定 Java 服务的进程级 CPU/内存占用（按关键字匹配进程，如 ubains-meeting-*）。
         用于判断压测结果是否受目标机资源瓶颈限制。
         基于 paramiko 实现，每次 sample() 通过单条 SSH 命令采集。

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

import logging
import re
import time
from typing import List, Optional, Tuple

import paramiko

logger = logging.getLogger(__name__)

# SSH 单条采集命令（一次往返获取全部指标）
# /proc/stat 首行 → CPU 总时间 / 空闲时间
# /proc/meminfo → MemTotal / MemAvailable
# /proc/loadavg → 1/5/15 分钟平均负载
# ps 采集指定 Java 进程的 CPU/内存/RSS（%cpu 为进程平均占用，100% = 单核）
#   -o %cpu=,%mem=,rss=,pid=,comm= 固定列宽输出，避免出现 '100' 与 '10' 混淆
#   pgrep -f 用法：pgrep -f ubains-meeting 会误匹配到包含该关键字的主机进程，
#   改用 `ps -eo %cpu,%mem,rss,pid,comm,args | grep` 并精确过滤 java 进程。
_SSH_CMD = (
    "cat /proc/stat | head -1; "
    "echo '---MEM---'; "
    "cat /proc/meminfo | grep -E 'MemTotal|MemAvailable'; "
    "echo '---LOAD---'; "
    "cat /proc/loadavg; "
    "echo '---JAVA---'; "
    "ps -eo %cpu=,%mem=,rss=,pid=,comm=,args= | grep -E 'java|jdk' | grep -v grep"
)

# 匹配的 Java 进程关键字（命中任一即纳入监控；覆盖会议相关服务）
_JAVA_PROCESS_KEYWORDS = [
    "ubains-meeting-inner-api",
    "ubains-meeting-api",
    "ubains-meeting-quartz",
    "ubains-meeting",
]


def _match_keyword(args: str) -> Optional[str]:
    """
    从进程命令行中匹配 Java 服务关键字

    Args:
        args: 进程 args（-jar 后面的 jar 文件名或启动命令）

    Returns:
        Optional[str]: 命中的关键字（用于展示进程名），未命中返回 None
    """
    for kw in _JAVA_PROCESS_KEYWORDS:
        if kw in args:
            return kw
    return None


class TargetResourceMonitor:
    """
    目标机资源监控器（SSH 方式）

    每个采样周期调用一次 sample()，返回 CPU/内存/负载/Java 进程指标字典。
    系统 CPU 使用率通过相邻两次 /proc/stat 的 idle/total 差值计算（首次采样返回 0.0）。
    Java 进程指标通过 ps 命令采集（%cpu 为进程自启动以来的平均占用，
    100% = 单核满载，多核进程可超过 100%；%mem 相对系统总内存的百分比；
    rss 为常驻内存 KB，已换算为 MB）。
    连接断开时自动重连，连续失败超过阈值则标记为不可用。

    Attributes:
        host (str): 目标机 SSH 主机
        port (int): 目标机 SSH 端口
        username (str): SSH 用户名
        password (str): SSH 密码
    """

    def __init__(
        self,
        host: str = "192.168.5.44",
        port: int = 22,
        username: str = "root",
        password: str = "Ubains@123",
        sampling_interval: float = 1.0,
        max_retries: int = 5,
        connect_timeout: int = 10,
    ):
        """
        初始化目标机资源监控器

        Args:
            host: SSH 主机地址
            port: SSH 端口
            username: SSH 用户名
            password: SSH 密码
            sampling_interval: 采样间隔（秒）
            max_retries: 最大连续失败重连次数（超过后标记为不可用）
            connect_timeout: SSH 连接超时（秒）
        """
        self.host = host
        self.port = port
        self.username = username
        self.password = password
        self._interval = sampling_interval
        self._max_retries = max_retries
        self._connect_timeout = connect_timeout

        self._client: Optional[paramiko.SSHClient] = None
        self._connected = False
        self._consecutive_failures = 0
        self._disabled = False  # 连续失败超过阈值后标记为不可用

        # CPU 差值基准（首次采样建基线，第二次才有差值）
        self._cpu_last: Optional[Tuple[float, float]] = None  # (idle, total)

    # ==================== 对外接口 ====================

    def sample(self) -> dict:
        """
        采集一次目标机资源指标

        Returns:
            dict: {
                "cpuPercent": float,       # 系统 CPU 使用率 %
                "memPercent": float,       # 系统内存使用率 %
                "memUsedMb": float,        # 已用内存 MB
                "memTotalMb": float,       # 总内存 MB
                "loadAvg1": float,         # 1 分钟平均负载
                "loadAvg5": float,         # 5 分钟平均负载
                "loadAvg15": float,        # 15 分钟平均负载
                "javaProcesses": [         # 匹配的 Java 进程 CPU/内存占用
                    {
                        "processName": str,  # 命中的服务关键字（如 ubains-meeting-inner-api）
                        "pid": int,          # 进程 PID
                        "cpuPercent": float, # 进程 CPU 占用 %（100% = 单核）
                        "memPercent": float, # 进程内存占用 %（相对系统总内存）
                        "rssMb": float,      # 进程常驻内存 MB
                    }, ...
                ],
                "timestamp": float,        # 采集时间戳
                "available": bool,         # 监控器是否可用
            }
            采集失败时各值为 0.0，available=False，不抛异常。
        """
        result = {
            "cpuPercent": 0.0,
            "memPercent": 0.0,
            "memUsedMb": 0.0,
            "memTotalMb": 0.0,
            "loadAvg1": 0.0,
            "loadAvg5": 0.0,
            "loadAvg15": 0.0,
            "javaProcesses": [],
            "timestamp": time.time(),
            "available": not self._disabled,
        }

        if self._disabled:
            return result

        try:
            output = self._execute()
            if output is None:
                self._consecutive_failures += 1
                if self._consecutive_failures >= self._max_retries:
                    self._disabled = True
                    logger.warning(
                        f"目标机 {self.host} 连续 {self._max_retries} 次采集失败，已禁用监控"
                    )
                return result

            # 重置连续失败计数
            self._consecutive_failures = 0

            # 解析输出
            cpu = self._parse_cpu(output)
            mem = self._parse_mem(output)
            load = self._parse_load(output)
            java_processes = self._parse_java_processes(output)

            result["cpuPercent"] = cpu
            result["memPercent"] = mem[0]
            result["memUsedMb"] = mem[1]
            result["memTotalMb"] = mem[2]
            result["loadAvg1"] = load[0]
            result["loadAvg5"] = load[1]
            result["loadAvg15"] = load[2]
            result["javaProcesses"] = java_processes
            result["available"] = True

        except Exception as e:
            logger.debug(f"目标机资源采样异常: {e}")
            self._consecutive_failures += 1
            if self._consecutive_failures >= self._max_retries:
                self._disabled = True
                logger.warning(
                    f"目标机 {self.host} 连续 {self._max_retries} 次采集失败，已禁用监控"
                )

        return result

    def dispose(self) -> None:
        """关闭 SSH 连接，释放资源"""
        self._disabled = True
        if self._client:
            try:
                self._client.close()
            except Exception:
                pass
            self._client = None
        self._connected = False
        self._cpu_last = None

    # ==================== SSH 连接管理 ====================

    def _connect(self) -> bool:
        """
        建立 SSH 连接（如果尚未连接或已断开）

        Returns:
            bool: 是否连接成功
        """
        if self._connected and self._client:
            try:
                transport = self._client.get_transport()
                if transport and transport.is_active():
                    return True
            except Exception:
                pass

        try:
            if self._client:
                try:
                    self._client.close()
                except Exception:
                    pass

            client = paramiko.SSHClient()
            client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
            client.connect(
                hostname=self.host,
                port=self.port,
                username=self.username,
                password=self.password,
                timeout=self._connect_timeout,
                banner_timeout=self._connect_timeout,
                allow_agent=False,
                look_for_keys=False,
            )
            self._client = client
            self._connected = True
            return True
        except Exception as e:
            logger.debug(f"SSH 连接失败 {self.host}:{self.port}: {e}")
            self._connected = False
            return False

    def _execute(self) -> Optional[str]:
        """
        执行 SSH 采集命令

        Returns:
            Optional[str]: 命令输出文本，失败返回 None
        """
        if not self._connect():
            return None

        try:
            _, stdout, stderr = self._client.exec_command(_SSH_CMD, timeout=15)  # type: ignore
            output = stdout.read().decode("utf-8", errors="replace")
            err = stderr.read().decode("utf-8", errors="replace").strip()
            if err:
                logger.debug(f"SSH 命令 stderr: {err}")
            return output
        except Exception as e:
            logger.debug(f"SSH 命令执行失败: {e}")
            self._connected = False
            return None

    # ==================== 输出解析 ====================

    def _parse_cpu(self, output: str) -> float:
        """
        从 SSH 输出中解析系统 CPU 使用率

        从 /proc/stat 首行提取 user/nice/system/idle/iowait/... 等字段，
        通过相邻两次采样的 idle/total 差值计算 CPU 使用率。

        Args:
            output: SSH 命令完整输出

        Returns:
            float: CPU 使用率百分数，首次采样返回 0.0
        """
        for line in output.splitlines():
            if line.startswith("cpu ") or line.startswith("cpu\t"):
                fields = line.split()
                if len(fields) < 5:
                    return 0.0
                values = [float(x) for x in fields[1:]]
                idle = values[3] + (values[4] if len(values) > 4 else 0.0)  # idle + iowait
                total = sum(values)
                if self._cpu_last is None:
                    self._cpu_last = (idle, total)
                    return 0.0
                last_idle, last_total = self._cpu_last
                self._cpu_last = (idle, total)
                d_total = total - last_total
                d_idle = idle - last_idle
                if d_total <= 0:
                    return 0.0
                return round(max(0.0, min(100.0, (1.0 - d_idle / d_total) * 100.0)), 1)
        return 0.0

    def _parse_mem(self, output: str) -> Tuple[float, float, float]:
        """
        从 SSH 输出中解析系统内存使用率

        从 /proc/meminfo 提取 MemTotal 和 MemAvailable，
        内存使用率 = (1 - MemAvailable / MemTotal) * 100

        Args:
            output: SSH 命令完整输出

        Returns:
            Tuple[float, float, float]: (memPercent, memUsedMb, memTotalMb)
        """
        total_kb = 0.0
        available_kb = 0.0
        in_mem = False

        for line in output.splitlines():
            if line.strip() == "---MEM---":
                in_mem = True
                continue
            if not in_mem or ":" not in line:
                continue
            parts = line.split(":", 1)
            if len(parts) != 2:
                continue
            key = parts[0].strip()
            val = parts[1].strip().split()[0]
            try:
                if key == "MemTotal":
                    total_kb = float(val)
                elif key == "MemAvailable":
                    available_kb = float(val)
            except ValueError:
                continue

        if total_kb <= 0:
            return 0.0, 0.0, 0.0

        total_mb = round(total_kb / 1024.0, 1)
        avail_mb = round(available_kb / 1024.0, 1) if available_kb > 0 else 0.0
        used_mb = round(total_mb - avail_mb, 1) if avail_mb > 0 else 0.0
        percent = round((total_kb - available_kb) / total_kb * 100.0, 1) if available_kb > 0 else 0.0

        return percent, used_mb, total_mb

    def _parse_load(self, output: str) -> Tuple[float, float, float]:
        """
        从 SSH 输出中解析系统平均负载

        /proc/loadavg 格式: "1.23 0.89 0.67 3/456 12345"

        Args:
            output: SSH 命令完整输出

        Returns:
            Tuple[float, float, float]: (loadAvg1, loadAvg5, loadAvg15)
        """
        in_load = False
        for line in output.splitlines():
            if line.strip() == "---LOAD---":
                in_load = True
                continue
            if in_load and line.strip():
                parts = line.strip().split()
                if len(parts) >= 3:
                    try:
                        return (
                            float(parts[0]),
                            float(parts[1]),
                            float(parts[2]),
                        )
                    except ValueError:
                        pass
        return 0.0, 0.0, 0.0

    def _parse_java_processes(self, output: str) -> List[dict]:
        """
        从 SSH 输出中解析匹配关键字 Java 进程的 CPU/内存占用

        ps 列格式（固定列宽，--no-headers）：
            %cpu  %mem  rss(KB)  pid  comm(命令名)  args(完整命令行)

        仅保留匹配 _JAVA_PROCESS_KEYWORDS 的进程；若多个 pid 命中同一关键字
        （如 spring cloud 多实例），各自独立返回一行。

        Args:
            output: SSH 命令完整输出

        Returns:
            List[dict]: [
                {
                    "processName": str,  # 命中的服务关键字
                    "pid": int,
                    "cpuPercent": float, # 100% = 单核满载
                    "memPercent": float, # 相对系统总内存 %
                    "rssMb": float,
                }, ...
            ]
        """
        in_java = False
        processes: List[dict] = []

        for line in output.splitlines():
            if line.strip() == "---JAVA---":
                in_java = True
                continue
            if not in_java or not line.strip():
                continue

            parts = line.split(None, 5)
            if len(parts) < 6:
                continue
            try:
                cpu_str, mem_str, rss_str, pid_str = parts[0], parts[1], parts[2], parts[3]
                comm = parts[4]
                args = parts[5]
                cpu_val = float(cpu_str.strip())
                mem_val = float(mem_str.strip())
                rss_kb = float(rss_str.strip())
                pid = int(pid_str.strip())
            except (ValueError, IndexError):
                continue

            # 仅保留 java 相关进程
            if "java" not in comm.lower() and "jdk" not in comm.lower():
                continue
            # 跳过 grep 自身与临时进程
            if "grep" in args:
                continue

            keyword = _match_keyword(args)
            if not keyword:
                continue

            processes.append({
                "processName": keyword,
                "pid": pid,
                "cpuPercent": round(max(0.0, cpu_val), 1),
                "memPercent": round(max(0.0, mem_val), 1),
                "rssMb": round(rss_kb / 1024.0, 1),
            })

        return processes


# ==================== 模块自测 ====================

if __name__ == "__main__":
    # 手动验证：python -m app.executors.target_resource_monitor
    monitor = TargetResourceMonitor(
        host="192.168.5.44",
        username="root",
        password="Ubains@123",
        sampling_interval=1.0,
    )
    print(f"目标机: {monitor.host}:{monitor.port} ({monitor.username})")
    for i in range(5):
        import time as _time
        _time.sleep(1.0)
        data = monitor.sample()
        procs = data.get("javaProcesses", [])
        print(
            f"[{i + 1}] cpu={data['cpuPercent']}% mem={data['memPercent']}% "
            f"({data['memUsedMb']}/{data['memTotalMb']}MB) "
            f"load={data['loadAvg1']}/{data['loadAvg5']}/{data['loadAvg15']}"
        )
        for p in procs:
            print(
                f"    java: {p['processName']} pid={p['pid']} "
                f"cpu={p['cpuPercent']}% mem={p['memPercent']}% rss={p['rssMb']}MB"
            )
        if not procs:
            print("    (未匹配到 Java 进程)")
    monitor.dispose()