# -*- coding: utf-8 -*-
"""
service_manage.py — 服务管理模块业务层

功能：
  1. 目标配置：管理员维护对接的目标服务器（如 5.44 统一管理平台）
     凭据：密码 Fernet 加密存储（复用 service_monitor.utils.crypto）
  2. 项目信息：保存/读取授权项目信息
  3. 授权文件：上传/下载/状态查询（代理 5.44 或本地占位）
  4. 5.44 代理模式：配置了目标后，下载/上传/任务状态/项目信息全部转发
     到目标服务器（阶段二），无目标时降级为本地占位（阶段一，仅开发用）

与 service_monitor 的目标完全独立（两套数据、两套存储），互不干扰。
"""

from __future__ import annotations

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

from utils.paths import (
    SERVICE_MANAGE_DATA_DIR,
    MANAGE_TARGETS_FILE,
    MANAGE_PROJECTS_FILE,
    MANAGE_UPLOADS_DIR,
)

from services.five44_client import Five44Client, Five44Error

logger = logging.getLogger("service_manage")

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


def _validate_port(port) -> int:
    """校验端口为 1-65535 的整数，非法抛 ValueError。"""
    try:
        port = int(port)
    except (TypeError, ValueError):
        raise ValueError("端口必须为 1-65535 的数字") from None
    if port < 1 or port > 65535:
        raise ValueError("端口必须为 1-65535 的数字")
    return port


# ============================================================
# 密码加密工具（复用 service_monitor 的 crypto）
# ============================================================

try:
    from service_monitor.utils.crypto import encrypt_password, decrypt_password, is_encrypted
except ImportError:
    logger.warning("无法导入 crypto 模块，将使用明文存储（仅开发环境）")

    def encrypt_password(plain: str) -> str:
        return plain

    def decrypt_password(token: str) -> str:
        return token

    def is_encrypted(value: str) -> bool:
        return False


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

def _ensure_dirs():
    """确保数据目录存在。"""
    SERVICE_MANAGE_DATA_DIR.mkdir(parents=True, exist_ok=True)
    MANAGE_UPLOADS_DIR.mkdir(parents=True, exist_ok=True)


def _load_targets() -> list:
    """加载目标列表。"""
    _ensure_dirs()
    if not MANAGE_TARGETS_FILE.exists():
        return []
    try:
        data = json.loads(MANAGE_TARGETS_FILE.read_text(encoding="utf-8"))
        return data if isinstance(data, list) else []
    except (json.JSONDecodeError, OSError) as e:
        logger.error("加载 targets.json 失败: %s", e)
        return []


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


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


# ============================================================
# 目标配置 CRUD
# ============================================================

def list_targets() -> list:
    """列出全部目标（脱敏：密码→has_password 布尔）。"""
    targets = _load_targets()
    result = []
    for t in targets:
        result.append({
            "id": t["id"],
            "name": t["name"],
            "host": t["host"],
            "port": t.get("port", 443),
            "protocol": t.get("protocol", "https"),
            "auth_type": t.get("auth_type", "password"),
            "username": t.get("username", ""),
            "has_password": bool(t.get("password_enc")),
            "connected": t.get("connected", False),
            "created_at": t.get("created_at"),
            "updated_at": t.get("updated_at"),
        })
    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) -> Optional[dict]:
    """获取单个目标对外视图（脱敏）。"""
    t = get_target(target_id)
    if not t:
        return None
    return {
        "id": t["id"],
        "name": t["name"],
        "host": t["host"],
        "port": t.get("port", 443),
        "protocol": t.get("protocol", "https"),
        "auth_type": t.get("auth_type", "password"),
        "username": t.get("username", ""),
        "has_password": bool(t.get("password_enc")),
        "connected": t.get("connected", False),
        "created_at": t.get("created_at"),
        "updated_at": t.get("updated_at"),
    }


def create_target(data: dict) -> dict:
    """新增目标。

    字段：name, host, port, protocol, auth_type, username, password
    """
    name = (data.get("name") or "").strip()
    host = (data.get("host") or "").strip()
    port = data.get("port", 443)
    protocol = data.get("protocol", "https")
    auth_type = data.get("auth_type", "password")
    username = (data.get("username") or "").strip()
    password = data.get("password") or ""

    if not name or not _NAME_RE.match(name) or not host or not _HOST_RE.match(host):
        raise ValueError("目标名称/主机地址非法")
    if protocol not in ("http", "https"):
        raise ValueError("协议必须为 http 或 https")
    if auth_type not in ("password", "token", "none"):
        raise ValueError("认证方式非法")
    if auth_type == "password" and not username:
        raise ValueError("用户名不能为空")
    if auth_type == "password" and not password:
        raise ValueError("密码不能为空")

    _validate_port(port)

    now = _now()
    target = {
        "id": str(uuid.uuid4())[:12],
        "name": name,
        "host": host,
        "port": port,
        "protocol": protocol,
        "auth_type": auth_type,
        "username": username if auth_type == "password" else "",
        "password_enc": encrypt_password(password) if password else "",
        "connected": False,
        "created_at": now,
        "updated_at": now,
    }

    targets = _load_targets()
    targets.append(target)
    _save_targets(targets)
    logger.info("新增管理目标: %s (%s)", name, host)
    return get_target_view(target["id"])


def update_target(target_id: str, data: dict) -> dict:
    """更新目标。"""
    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"]:
        name = str(data["name"]).strip()
        if not _NAME_RE.match(name):
            raise ValueError("目标名称非法")
        target["name"] = name
    if "host" in data and data["host"]:
        host = str(data["host"]).strip()
        if not _HOST_RE.match(host):
            raise ValueError("主机地址非法")
        target["host"] = host
    if "port" in data:
        _validate_port(data["port"])
    if "protocol" in data:
        if data["protocol"] not in ("http", "https"):
            raise ValueError("协议必须为 http 或 https")
        target["protocol"] = data["protocol"]
    if "auth_type" in data:
        if data["auth_type"] not in ("password", "token", "none"):
            raise ValueError("认证方式非法")
        target["auth_type"] = data["auth_type"]
    if "username" in data:
        target["username"] = str(data["username"]).strip()
    if data.get("password"):
        target["password_enc"] = encrypt_password(data["password"])

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


def delete_target(target_id: str) -> bool:
    """删除目标。"""
    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:
    """测试 HTTP(S) 连通性。

    发送 GET 请求到目标服务器根路径，检查响应状态码。
    Returns: {"success": bool, "message": str}
    """
    host = (data.get("host") or "").strip()
    port = data.get("port", 443)
    protocol = data.get("protocol", "https")
    username = (data.get("username") or "").strip()
    password = data.get("password") or ""

    if not host or not _HOST_RE.match(host):
        return {"success": False, "message": "主机地址非法"}
    try:
        port = int(port)
    except (TypeError, ValueError):
        return {"success": False, "message": "端口必须为数字"}

    url = f"{protocol}://{host}:{port}/"
    try:
        import urllib.request
        req = urllib.request.Request(url, method="GET")
        # 设置超时 10 秒
        with urllib.request.urlopen(req, timeout=10) as resp:
            status = resp.getcode()
            if 200 <= status < 400:
                return {"success": True, "message": f"连接成功（HTTP {status}）"}
            else:
                return {"success": False, "message": f"服务器返回异常状态码: {status}"}
    except Exception as e:
        return {"success": False, "message": f"连接失败: {str(e)}"}


# ============================================================
# 项目信息
# ============================================================

def save_project(data: dict) -> dict:
    """保存项目信息（密码脱敏后存储）。"""
    _ensure_dirs()
    project = {
        "salesPeople": (data.get("salesPeople") or "").strip(),
        "deployer": (data.get("deployer") or "").strip(),
        "orderTime": (data.get("orderTime") or "").strip(),
        "serverPassword": encrypt_password(data.get("serverPassword") or ""),
        "projectOverview": (data.get("projectOverview") or "").strip(),
        "updated_at": _now(),
    }
    MANAGE_PROJECTS_FILE.write_text(
        json.dumps(project, ensure_ascii=False, indent=2),
        encoding="utf-8",
    )
    logger.info("保存项目信息")
    return {"success": True, "message": "项目信息保存成功"}


def get_project() -> Optional[dict]:
    """读取项目信息（脱敏：密码→has_password 布尔）。"""
    _ensure_dirs()
    if not MANAGE_PROJECTS_FILE.exists():
        return None
    try:
        data = json.loads(MANAGE_PROJECTS_FILE.read_text(encoding="utf-8"))
        data["has_password"] = bool(data.get("serverPassword"))
        data.pop("serverPassword", None)
        return data
    except (json.JSONDecodeError, OSError):
        return None


# ============================================================
# 5.44 代理客户端
# ============================================================

def _get_proxy_target() -> Optional[dict]:
    """获取用于授权代理的目标服务器（第一个配置的目标，无则 None）。"""
    targets = _load_targets()
    if not targets:
        return None
    t = targets[0]
    if not t.get("host") or not t.get("username") or not t.get("password_enc"):
        return None
    return t


def get_five44_client() -> Optional[Five44Client]:
    """构建 5.44 客户端；未配置目标返回 None（降级本地占位）。"""
    t = _get_proxy_target()
    if not t:
        return None
    try:
        password = decrypt_password(t["password_enc"])
    except Exception as e:
        logger.error("解密目标服务器密码失败: %s", e)
        return None
    return Five44Client(
        base_url=f"{t.get('protocol', 'https')}://{t['host']}:{t.get('port', 443)}",
        username=t["username"],
        password=password,
        verify_ssl=False,
    )


def _download_remote(client: Five44Client, getter) -> tuple:
    """调用 5.44 下载接口并落盘到 uploads 目录。

    Returns: (本地文件路径, 原始文件名)
    """
    content, filename = getter()
    _ensure_dirs()
    filepath = MANAGE_UPLOADS_DIR / filename
    filepath.write_bytes(content)
    logger.info("从 5.44 下载文件: %s (%d bytes)", filename, len(content))
    return filepath, filename


# ============================================================
# 授权文件操作（代理 5.44 / 本地占位）
# ============================================================

def _make_placeholder_file(filename: str, content: str = "占位文件") -> Path:
    """生成占位文件（阶段一降级：未配置目标服务器时使用）。"""
    _ensure_dirs()
    filepath = MANAGE_UPLOADS_DIR / filename
    filepath.write_text(content, encoding="utf-8")
    return filepath


def get_activation_file() -> Path:
    """获取激活文件（代理 5.44；无目标时本地生成占位文件）。"""
    client = get_five44_client()
    if client is not None:
        filepath, _ = _download_remote(client, client.download_activation)
        return filepath
    logger.warning("未配置目标服务器，生成占位激活文件")
    return _make_placeholder_file("licences.zip", "激活文件占位内容")


def get_deploy_view_file() -> Path:
    """获取部署视图文件（代理 5.44；无目标时本地生成占位文件）。"""
    client = get_five44_client()
    if client is not None:
        filepath, _ = _download_remote(client, client.download_deploy_view)
        return filepath
    logger.warning("未配置目标服务器，生成占位部署视图")
    ts = datetime.now().strftime("%Y%m%d%H%M%S")
    return _make_placeholder_file(
        f"deploymentView{ts}.xlsx",
        "部署视图占位内容",
    )


def get_self_check_file() -> Path:
    """获取自检文档文件（代理 5.44；无目标时本地生成占位文件）。"""
    client = get_five44_client()
    if client is not None:
        filepath, _ = _download_remote(client, client.download_self_check)
        return filepath
    logger.warning("未配置目标服务器，生成占位自检文档")
    ts = datetime.now().strftime("%Y%m%d%H%M%S")
    return _make_placeholder_file(
        f"checkList{ts}.xlsx",
        "自检文档占位内容",
    )


def upload_license_file(file_storage) -> dict:
    """上传授权文件到 5.44（无目标时降级本地暂存）。

    Returns: {"success", "taskId", "status", "message"}
    """
    client = get_five44_client()
    if client is None:
        result = save_uploaded_file(file_storage, subdir="license")
        logger.warning("未配置目标服务器，授权文件本地暂存")
        # 与前端轮询约定兼容：本地模式状态立即 completed
        return {
            "success": True,
            "taskId": f"local-{result['filename']}",
            "status": "completed",
            "message": "授权文件已暂存（未对接目标服务器）",
        }
    filename = file_storage.filename or f"upload_{uuid.uuid4().hex[:8]}"
    file_bytes = file_storage.read()
    data = client.upload_license(file_bytes, filename)
    task_id = (
        data.get("taskId")
        or data.get("task_id")
        or (data.get("data") or {}).get("taskId")
        or ""
    )
    return {
        "success": True,
        "taskId": task_id or f"remote-{filename}",
        "status": "pending",
        "message": data.get("msg") or data.get("message") or "授权文件已上传，处理中",
    }


def check_license_task(task_id: str) -> dict:
    """查询授权任务状态（代理 5.44；本地 taskId 直接返回完成）。

    Returns: {"success", "status", "message"}
    """
    if not task_id or task_id.startswith("local-"):
        return {"success": True, "status": "completed", "message": "授权处理完成"}
    client = get_five44_client()
    if client is None:
        return {"success": True, "status": "completed", "message": "授权处理完成"}
    _task_id = task_id.removeprefix("remote-")
    data = client.check_task_status(_task_id) if _task_id else client.check_task_status(task_id)
    # 服务端状态字段可能为：status / taskStatus / state
    status = (
        data.get("status")
        or data.get("taskStatus")
        or (data.get("data") or {}).get("status")
        or "pending"
    )
    return {
        "success": True,
        "status": str(status).lower(),
        "message": data.get("msg") or data.get("message") or "",
    }


def save_project(data: dict) -> dict:
    """保存项目信息（代理 5.44；成功/失败均返回）。

    5.44 保存成功后仍写本地 projects.json（供页面回填 + 脱敏读取），
    密码字段已加密存储。若 5.44 不可达则抛 Five44Error 由路由层返回 502。
    """
    client = get_five44_client()
    if client is not None:
        # 代理到 5.44（密码字段也随请求发送，5.44 侧自行处理）
        client.save_project(dict(data))

    _ensure_dirs()
    project = {
        "salesPeople": (data.get("salesPeople") or "").strip(),
        "deployer": (data.get("deployer") or "").strip(),
        "orderTime": (data.get("orderTime") or "").strip(),
        "serverPassword": encrypt_password(data.get("serverPassword") or ""),
        "projectOverview": (data.get("projectOverview") or "").strip(),
        "updated_at": _now(),
    }
    MANAGE_PROJECTS_FILE.write_text(
        json.dumps(project, ensure_ascii=False, indent=2),
        encoding="utf-8",
    )
    logger.info("保存项目信息")
    return {"success": True, "message": "项目信息保存成功"}


def get_project() -> Optional[dict]:
    """读取项目信息（脱敏：密码→has_password 布尔）。"""
    _ensure_dirs()
    if not MANAGE_PROJECTS_FILE.exists():
        return None
    try:
        data = json.loads(MANAGE_PROJECTS_FILE.read_text(encoding="utf-8"))
        data["has_password"] = bool(data.get("serverPassword"))
        data.pop("serverPassword", None)
        return data
    except (json.JSONDecodeError, OSError):
        return None


def save_uploaded_file(file_storage, subdir: str = "") -> dict:
    """保存上传文件到 uploads 目录（未配置目标服务器时的降级暂存）。

    Args:
        file_storage: Flask request.files 中的文件对象
        subdir: 子目录名（如 "license"、"deploy-view"、"self-check"）

    Returns:
        {"success": True, "filename": str, "path": str, "size": int}
    """
    _ensure_dirs()
    target_dir = MANAGE_UPLOADS_DIR
    if subdir:
        target_dir = target_dir / subdir
        target_dir.mkdir(parents=True, exist_ok=True)

    filename = file_storage.filename or f"upload_{uuid.uuid4().hex[:8]}"
    filepath = target_dir / filename
    file_storage.save(str(filepath))
    size = filepath.stat().st_size

    logger.info("上传文件保存: %s (%d bytes)", filename, size)
    return {
        "success": True,
        "filename": filename,
        "path": str(filepath.relative_to(SERVICE_MANAGE_DATA_DIR)),
        "size": size,
    }


def _upload_to_target(file_storage, endpoint: str) -> dict:
    """将上传文件转发到 5.44 指定导出/上传接口（部署视图/自检文档）。

    Args:
        file_storage: Flask request.files 中的文件对象
        endpoint: Five44Client 方法名（"upload_deploy_view" / "upload_self_check"）

    Returns:
        {"success": True, "message": str}
    """
    client = get_five44_client()
    if client is None:
        result = save_uploaded_file(file_storage, subdir=endpoint)
        logger.warning("未配置目标服务器，%s 文件本地暂存", endpoint)
        return {"success": True, "message": "文件已暂存（未对接目标服务器）", "file": result}
    filename = file_storage.filename or f"upload_{uuid.uuid4().hex[:8]}"
    file_bytes = file_storage.read()
    method = getattr(client, endpoint)
    data = method(file_bytes, filename)
    msg = data.get("msg") or data.get("message") or "上传成功"
    return {"success": True, "message": msg}