# -*- coding: utf-8 -*-
"""
executor.py — 检测执行器

职责：
  - 在目标服务器上布置 assets/ 为 <workdir>/lib/ 结构
  - 注入 LIB_DIR 并执行指定模块
  - 收集原始 stdout 返回

执行器类型：
  - LocalExecutor：本机直接 subprocess（模块文件已在 assets/）
  - SSHExecutor：远程 paramiko（SFTP 上传 → SSH 执行）
"""

from __future__ import annotations

import abc
import logging
import os
import subprocess
import tempfile
import time
import shutil
from pathlib import Path
from typing import Dict, List, Optional

from .paths import (
    ASSETS_DIR, SYSTEM_ASSETS_DIR, SERVICE_ASSETS_DIR,
    COMMON_SH, CONFIG_TEMPLATE,
)
from .check_modules import CheckModule

logger = logging.getLogger("service_monitor.executor")

# 目标机上的工作目录前缀（后端根据 run_id 创建子目录）
_REMOTE_BASE = "/tmp/check_modules"


class BaseExecutor(abc.ABC):
    """执行器抽象基类。"""

    def __init__(self, run_id: str):
        self.run_id = run_id
        self._workdir: Optional[str] = None   # 目标机上的工作目录

    @property
    @abc.abstractmethod
    def is_local(self) -> bool:
        """是否为本地执行器。"""

    @abc.abstractmethod
    def _exec(self, command: str, timeout: int) -> str:
        """在目标机上执行命令，返回 stdout。"""

    @abc.abstractmethod
    def _upload_file(self, local_path: Path, remote_path: str):
        """上传单个文件到目标机。"""

    def _remote_workdir(self) -> str:
        """远程工作目录路径。"""
        return f"{_REMOTE_BASE}_{self.run_id}"

    def setup(self) -> str:
        """在目标机上布置工作目录并渲染配置，返回 workdir 路径。"""
        wd = self._remote_workdir()
        self._workdir = wd
        self._exec(f"mkdir -p {wd}/lib/system {wd}/lib/service", timeout=10)
        return wd

    def upload_assets(self) -> None:
        """上传所有资产文件到目标机 <workdir>/lib/。"""
        wd = self._workdir
        if not wd:
            raise RuntimeError("setup() 未调用，workdir 为空")

        # 按 lib/ 结构上传
        self._upload_file(COMMON_SH, f"{wd}/lib/common.sh")
        self._upload_file(CONFIG_TEMPLATE, f"{wd}/lib/config.sh")

        for f in SYSTEM_ASSETS_DIR.iterdir():
            if f.suffix == ".sh":
                self._upload_file(f, f"{wd}/lib/system/{f.name}")

        for f in SERVICE_ASSETS_DIR.iterdir():
            if f.suffix == ".sh":
                self._upload_file(f, f"{wd}/lib/service/{f.name}")

    def run_module(self, module: CheckModule, config_text: str, timeout: int = 90) -> str:
        """执行单个检测模块，返回原始 stdout。

        步骤：
          1. 覆盖写入渲染后的 config.sh
          2. 设 LIB_DIR=<workdir> 执行 bash 模块
        """
        wd = self._workdir
        if not wd:
            raise RuntimeError("setup() 未调用")

        # 写入渲染后的 config.sh
        self._write_config(wd, config_text)

        # 构造执行命令
        cmd = (
            f"export LIB_DIR={wd} && "
            f"cd {wd} && "
            f"chmod +x lib/{module.relpath} && "
            f"bash lib/{module.relpath}"
        )
        logger.info("执行模块: %s (%s)", module.name, module.id)
        t0 = time.time()
        result = self._exec(cmd, timeout=timeout)
        elapsed = time.time() - t0
        logger.info("模块 %s 完成 (%.1fs, %d 行)", module.id, elapsed, len(result.splitlines()))
        return result

    @abc.abstractmethod
    def _write_config(self, wd: str, config_text: str) -> None:
        """将渲染后的 config.sh 写入目标机。"""

    def cleanup(self) -> None:
        """清理目标机上的工作目录。"""
        wd = self._workdir
        if wd:
            try:
                self._exec(f"rm -rf {wd}", timeout=10)
                logger.info("清理工作目录: %s", wd)
            except Exception as e:
                logger.warning("清理工作目录失败: %s", e)
            self._workdir = None

    def test_connection(self) -> bool:
        """测试连接。"""
        try:
            result = self._exec("echo OK", timeout=10)
            return "OK" in result
        except Exception as e:
            logger.warning("连接测试失败: %s", e)
            return False


# ============================================================
# 本地执行器
# ============================================================

class LocalExecutor(BaseExecutor):
    """本机执行器：直接 subprocess.run。

    资产文件在 ASSETS_DIR 中已存在，setup/upload_assets 只建目录结构，
    _write_config 写渲染后的 config.sh，run_module 用 subprocess 调 bash。
    """

    def __init__(self, run_id: str):
        super().__init__(run_id)
        self._local_workdir: Optional[Path] = None

    @property
    def is_local(self) -> bool:
        return True

    def setup(self) -> str:
        # 本地用 tempfile 创建临时工作目录
        self._local_workdir = Path(tempfile.mkdtemp(prefix=f"mon_{self.run_id}_"))
        # 建 lib 子目录
        lib_dir = self._local_workdir / "lib"
        (lib_dir / "system").mkdir(parents=True, exist_ok=True)
        (lib_dir / "service").mkdir(parents=True, exist_ok=True)
        # 复制资产文件
        shutil.copy2(COMMON_SH, lib_dir / "common.sh")
        for f in SYSTEM_ASSETS_DIR.iterdir():
            if f.suffix == ".sh":
                shutil.copy2(f, lib_dir / "system" / f.name)
        for f in SERVICE_ASSETS_DIR.iterdir():
            if f.suffix == ".sh":
                shutil.copy2(f, lib_dir / "service" / f.name)
        self._workdir = str(self._local_workdir)
        return self._workdir

    def upload_assets(self) -> None:
        # setup 时已完成复制，无需额外操作
        pass

    def _write_config(self, wd: str, config_text: str) -> None:
        config_path = Path(wd) / "lib" / "config.sh"
        config_path.write_text(config_text, encoding="utf-8")

    def _exec(self, command: str, timeout: int) -> str:
        try:
            r = subprocess.run(
                ["bash", "-c", command],
                capture_output=True, text=True, timeout=timeout,
                encoding="utf-8", errors="replace",
            )
            output = r.stdout
            if r.stderr:
                # 将 stderr 中非 command not found 的合并到 stdout（模块会输出 ERROR: 前缀）
                for line in r.stderr.splitlines():
                    if "command not found" in line.lower():
                        continue
                    output += line + "\n"
            return output
        except subprocess.TimeoutExpired:
            logger.warning("命令执行超时 (%ds): %s", timeout, command[:80])
            return ""
        except FileNotFoundError as e:
            logger.error("bash 不可用: %s", e)
            return ""

    def _upload_file(self, local_path: Path, remote_path: str):
        # 本地模式，文件已在 setup 时复制
        pass

    def cleanup(self) -> None:
        if self._local_workdir and self._local_workdir.exists():
            shutil.rmtree(self._local_workdir, ignore_errors=True)
            logger.info("清理本地工作目录: %s", self._local_workdir)
        self._workdir = None
        self._local_workdir = None


# ============================================================
# SSH 远程执行器
# ============================================================

class SSHExecutor(BaseExecutor):
    """远程执行器：paramiko SSH + SFTP。

    需要：paramiko，PyPI 包。
    密码从 target 解密后传入，不在初始化参数中硬编码。
    """

    def __init__(self, run_id: str, host: str, port: int,
                 username: str, password: str, connect_timeout: int = 15):
        super().__init__(run_id)
        self.host = host
        self.port = port
        self.username = username
        self.password = password
        self.connect_timeout = connect_timeout
        self._ssh_client = None
        self._sftp_client = None

    @property
    def is_local(self) -> bool:
        return False

    def _get_ssh(self):
        """懒加载 SSH 连接。"""
        if self._ssh_client is None:
            import paramiko
            self._ssh_client = paramiko.SSHClient()
            self._ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
            try:
                self._ssh_client.connect(
                    hostname=self.host, port=self.port,
                    username=self.username, password=self.password,
                    timeout=self.connect_timeout,
                    allow_agent=False, look_for_keys=False,
                )
                logger.info("SSH 连接成功: %s@%s:%d", self.username, self.host, self.port)
            except Exception as e:
                self._ssh_client.close()
                self._ssh_client = None
                raise ConnectionError(f"SSH 连接失败: {e}") from e
        return self._ssh_client

    def _get_sftp(self):
        if self._sftp_client is None:
            ssh = self._get_ssh()
            self._sftp_client = ssh.open_sftp()
        return self._sftp_client

    def _exec(self, command: str, timeout: int) -> str:
        ssh = self._get_ssh()
        try:
            _, stdout, stderr = ssh.exec_command(command, timeout=timeout)
            exit_code = stdout.channel.recv_exit_status()
            out = stdout.read().decode("utf-8", errors="replace")
            err = stderr.read().decode("utf-8", errors="replace")
            if err:
                for line in err.splitlines():
                    if "command not found" in line.lower():
                        continue
                    out += line + "\n"
            return out
        except Exception as e:
            logger.warning("SSH 执行失败: %s (cmd: %s)", e, command[:60])
            return ""

    def _upload_file(self, local_path: Path, remote_path: str):
        sftp = self._get_sftp()
        try:
            sftp.put(str(local_path), remote_path)
            logger.debug("上传: %s → %s", local_path.name, remote_path)
        except Exception as e:
            raise IOError(f"上传文件失败 {local_path.name}: {e}") from e

    def _write_config(self, wd: str, config_text: str) -> None:
        # SSH 模式：通过 SFTP 写入渲染后的 config.sh
        sftp = self._get_sftp()
        remote_config = f"{wd}/lib/config.sh"
        with sftp.open(remote_config, "w") as f:
            f.write(config_text)

    def cleanup(self) -> None:
        super().cleanup()
        try:
            if self._sftp_client:
                self._sftp_client.close()
        except Exception:
            pass
        try:
            if self._ssh_client:
                self._ssh_client.close()
        except Exception:
            pass
        self._sftp_client = None
        self._ssh_client = None
