# -*- coding: utf-8 -*-
"""
target_service.py — 监测目标管理（CRUD + 连通性测试）

数据存储：service_monitor/data/targets.json
凭据：SSH 密码用 Fernet 加密后存，运行时解密注入
"""

from __future__ import annotations

import json
import logging
import re
import uuid
from datetime import datetime
from typing import Optional

from ..utils.paths import TARGETS_FILE, ensure_dirs
from ..utils.crypto import encrypt_password, decrypt_password, is_encrypted
from ..utils.executor import LocalExecutor, SSHExecutor, SSHConnectionError, DependencyError

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

# 本地目标内置 ID
LOCAL_TARGET_ID = "local"

# 校验白名单
_HOST_RE = re.compile(r"^[A-Za-z0-9.\-]{1,255}$")
_USER_RE = re.compile(r"^[A-Za-z0-9._\-]{1,64}$")


# ============================================================
# 数据读写
# ============================================================

def _load_targets() -> list:
    """加载目标列表。"""
    if not TARGETS_FILE.exists():
        return [_local_target()]
    try:
        data = json.loads(TARGETS_FILE.read_text(encoding="utf-8"))
        if not isinstance(data, list):
            return [_local_target()]
        # 确保本地目标存在
        if not any(t.get("id") == LOCAL_TARGET_ID for t in data):
            data.insert(0, _local_target())
        return data
    except (json.JSONDecodeError, OSError) as e:
        logger.error("加载 targets.json 失败: %s", e)
        return [_local_target()]


def _save_targets(targets: list) -> None:
    """保存目标列表。"""
    ensure_dirs()
    TARGETS_FILE.write_text(
        json.dumps(targets, ensure_ascii=False, indent=2),
        encoding="utf-8",
    )


def _local_target() -> dict:
    """内置本地目标。"""
    return {
        "id": LOCAL_TARGET_ID,
        "name": "本机（当前服务器）",
        "type": "local",
        "host": "",
        "port": 22,
        "username": "",
        "password_enc": "",
        "container_overrides": {},
        "credential_overrides": {},
        "thresholds": {},
        "built_in": True,
        "created_at": "2026-07-16T00:00:00",
        "updated_at": "2026-07-16T00:00:00",
    }


def _now() -> str:
    return datetime.now().strftime("%Y-%m-%dT%H:%M:%S")


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

def list_targets(role: str) -> list:
    """列出目标。

    普通用户：只返回本地目标元信息（不含敏感凭据）
    管理员：返回全部（密码字段脱敏为布尔标记，不返回密文）
    """
    targets = _load_targets()
    result = []
    for t in targets:
        item = {
            "id": t["id"],
            "name": t["name"],
            "type": t["type"],
            "host": t.get("host", ""),
            "port": t.get("port", 22),
            "username": t.get("username", ""),
            "has_password": bool(t.get("password_enc")),
            "built_in": t.get("built_in", False),
            "container_overrides": t.get("container_overrides", {}),
            "thresholds": t.get("thresholds", {}),
            "created_at": t.get("created_at"),
            "updated_at": t.get("updated_at"),
        }
        # 普通用户只看本地目标
        if role != "admin" and t["type"] != "local":
            continue
        result.append(item)
    return result


def get_target(target_id: str) -> Optional[dict]:
    """获取单个目标原始记录（含密文，内部用）。"""
    for t in _load_targets():
        if t["id"] == target_id:
            return t
    return None


def get_target_view(target_id: str, role: str) -> Optional[dict]:
    """获取单个目标的对外视图（脱敏）。"""
    t = get_target(target_id)
    if not t:
        return None
    # 普通用户只能看本地目标
    if role != "admin" and t["type"] != "local":
        return None
    return _build_view(t)


def _build_view(t: dict) -> dict:
    return {
        "id": t["id"],
        "name": t["name"],
        "type": t["type"],
        "host": t.get("host", ""),
        "port": t.get("port", 22),
        "username": t.get("username", ""),
        "has_password": bool(t.get("password_enc")),
        "built_in": t.get("built_in", False),
        "container_overrides": t.get("container_overrides", {}),
        "thresholds": t.get("thresholds", {}),
    }


def create_target(data: dict) -> dict:
    """新增远程目标（管理员）。"""
    name = (data.get("name") or "").strip()
    host = (data.get("host") or "").strip()
    port = data.get("port", 22)
    username = (data.get("username") or "").strip()
    password = data.get("password") or ""

    if not name:
        raise ValueError("目标名称不能为空")
    if not host or not _HOST_RE.match(host):
        raise ValueError("主机地址非法")
    if not username or not _USER_RE.match(username):
        raise ValueError("用户名非法")
    if not password:
        raise ValueError("密码不能为空")

    try:
        port = int(port)
    except (TypeError, ValueError):
        raise ValueError("端口必须为数字")

    now = _now()
    target = {
        "id": str(uuid.uuid4())[:12],
        "name": name,
        "type": "remote",
        "host": host,
        "port": port,
        "username": username,
        "password_enc": encrypt_password(password),
        "container_overrides": _sanitize_overrides(data.get("container_overrides")),
        "credential_overrides": _encrypt_creds(data.get("credential_overrides")),
        "thresholds": _sanitize_thresholds(data.get("thresholds")),
        "built_in": False,
        "created_at": now,
        "updated_at": now,
    }

    targets = _load_targets()
    targets.append(target)
    _save_targets(targets)
    logger.info("新增监测目标: %s (%s@%s)", name, username, host)
    return _build_view(target)


def update_target(target_id: str, data: dict) -> dict:
    """更新目标（管理员）。本地目标只允许改 container_overrides/thresholds。"""
    targets = _load_targets()
    target = next((t for t in targets if t["id"] == target_id), None)
    if not target:
        raise ValueError("目标不存在")

    if "name" in data and data["name"]:
        target["name"] = str(data["name"]).strip()

    if target["type"] == "remote":
        if "host" in data and data["host"]:
            if not _HOST_RE.match(str(data["host"])):
                raise ValueError("主机地址非法")
            target["host"] = str(data["host"]).strip()
        if "port" in data:
            try:
                target["port"] = int(data["port"])
            except (TypeError, ValueError):
                raise ValueError("端口必须为数字")
        if "username" in data and data["username"]:
            if not _USER_RE.match(str(data["username"])):
                raise ValueError("用户名非法")
            target["username"] = str(data["username"]).strip()
        # 密码可选更新（不传则不改）
        if data.get("password"):
            target["password_enc"] = encrypt_password(data["password"])
        if "credential_overrides" in data:
            target["credential_overrides"] = _encrypt_creds(data["credential_overrides"])

    # 通用可改字段
    if "container_overrides" in data:
        target["container_overrides"] = _sanitize_overrides(data["container_overrides"])
    if "thresholds" in data:
        target["thresholds"] = _sanitize_thresholds(data["thresholds"])

    target["updated_at"] = _now()
    _save_targets(targets)
    logger.info("更新监测目标: %s", target_id)
    return _build_view(target)


def delete_target(target_id: str) -> bool:
    """删除目标（管理员）。本地目标不可删。"""
    if target_id == LOCAL_TARGET_ID:
        raise ValueError("内置本地目标不可删除")
    targets = _load_targets()
    new_list = [t for t in targets if t["id"] != target_id]
    if len(new_list) == len(targets):
        return False
    _save_targets(new_list)
    logger.info("删除监测目标: %s", target_id)
    return True


# ============================================================
# 连通性测试（不保存）
# ============================================================

def test_connection(data: dict) -> dict:
    """测试远程目标 SSH 连通性（不落库）。

    Returns: {
        "success": bool,
        "message": str,
        "detail": str | None,    # 详细错误信息
        "error_code": str | None # 错误码
    }
    """
    host = (data.get("host") or "").strip()
    port = data.get("port", 22)
    username = (data.get("username") or "").strip()
    password = data.get("password") or ""

    if not _HOST_RE.match(host):
        return {"success": False, "message": "主机地址非法", "detail": None, "error_code": "INVALID_HOST"}
    if not _USER_RE.match(username):
        return {"success": False, "message": "用户名非法", "detail": None, "error_code": "INVALID_USER"}
    if not password:
        return {"success": False, "message": "密码不能为空", "detail": None, "error_code": "PASSWORD_REQUIRED"}
    try:
        port = int(port)
    except (TypeError, ValueError):
        return {"success": False, "message": "端口必须为数字", "detail": None, "error_code": "INVALID_PORT"}

    try:
        exe = SSHExecutor(run_id="test", host=host, port=port, username=username, password=password)
    except DependencyError as e:
        return {
            "success": False,
            "message": str(e),
            "detail": str(e),
            "error_code": "DEPENDENCY_MISSING",
        }

    try:
        ok = exe.test_connection()
        return {
            "success": ok,
            "message": "连接成功" if ok else "SSH 连接失败",
            "detail": None,
            "error_code": None,
        }
    except SSHConnectionError as e:
        return {
            "success": False,
            "message": str(e),
            "detail": e.detail,
            "error_code": e.error_code,
        }
    except Exception as e:
        logger.exception("SSH 连接测试异常: %s", e)
        return {
            "success": False,
            "message": f"SSH 连接失败：{e}",
            "detail": str(e),
            "error_code": "UNKNOWN",
        }
    finally:
        try:
            exe.cleanup()
        except Exception:
            pass


# ============================================================
# 执行器工厂
# ============================================================

def make_executor(target: dict, run_id: str):
    """根据目标类型构造执行器（密码在此解密）。"""
    if target["type"] == "local":
        return LocalExecutor(run_id)

    # 远程：解密密码
    password_enc = target.get("password_enc", "")
    password = decrypt_password(password_enc) if is_encrypted(password_enc) else password_enc

    # 合并目标级凭据覆盖（如 mysql/redis 密码）
    exe = SSHExecutor(
        run_id=run_id,
        host=target["host"],
        port=target.get("port", 22),
        username=target["username"],
        password=password,
    )
    return exe


def resolve_credentials(target: dict) -> dict:
    """解析目标的所有凭据（解密），供 config 渲染用。

    返回 credential_overrides（明文），含 mysql_password / redis_password。
    """
    overrides = target.get("credential_overrides", {})
    plain = {}
    for key, enc in overrides.items():
        if is_encrypted(enc):
            plain[key] = decrypt_password(enc)
        else:
            plain[key] = enc
    return plain


# ============================================================
# 输入净化
# ============================================================

def _sanitize_overrides(overrides) -> dict:
    """净化容器模式覆盖：键只允许字母/数字/下划线，值只允许安全字符。"""
    if not isinstance(overrides, dict):
        return {}
    safe = {}
    pat = re.compile(r"^[A-Za-z0-9_|\-]{1,64}$")
    for k, v in overrides.items():
        if not re.match(r"^[A-Za-z0-9_]{1,32}$", str(k)):
            continue
        if isinstance(v, str) and pat.match(v):
            safe[str(k)] = v
    return safe


def _encrypt_creds(creds) -> dict:
    """凭据覆盖：明文 → 加密存储。"""
    if not isinstance(creds, dict):
        return {}
    safe = {}
    for k, v in creds.items():
        if not re.match(r"^[A-Za-z0-9_]{1,32}$", str(k)):
            continue
        if isinstance(v, str) and v:
            # 已加密则原样保留，否则加密
            safe[str(k)] = v if is_encrypted(v) else encrypt_password(v)
    return safe


def _sanitize_thresholds(thresholds) -> dict:
    """阈值覆盖：键白名单，值数字。"""
    if not isinstance(thresholds, dict):
        return {}
    allowed = {
        "cpu_warning", "cpu_critical",
        "memory_warning", "memory_critical",
        "disk_warning", "disk_critical",
        "thread_warning", "thread_critical",
        "java_log_path", "python_log_path", "nginx_log_path", "nacos_log_path",
    }
    safe = {}
    for k, v in thresholds.items():
        if k.lower() in allowed:
            try:
                safe[k.lower()] = int(v)
            except (TypeError, ValueError):
                # 字符串型阈值（路径）
                safe[k.lower()] = str(v)[:200]
    return safe
