# =============================================================================
# 企微IT智能服务台 — 火绒终端安全 API 客户端
# =============================================================================
# 说明：封装火绒API的签名、请求、响应处理
# 核心功能：
#   1. HRESS 签名实现（Authorization Header 方式）
#   2. 统一请求封装（超时、重试、异常处理）
#   3. P0 接口：终端列表 _list / 终端详情 _info2 / 高危漏洞 _leak / 病毒事件 _virus_events
#   4. P1 接口：终端隔离/解除 _create(netctrl) / 快速扫描 / 在线终端查询
# 签名算法（来自火绒官方API文档 v1）：
#   Authorization = "HRESS" + AccessKeyId + ":" + Expires + ":" + Signature
#   Signature = urlencode(base64(hmac-sha1(AccessKeySecret,
#             AccessKeyId + "\n" + Expires + "\n" + HTTP-METHOD + "\n"
#             + Content-MD5 + "\n" + CanonicalizedResource)))
#   CanonicalizedResource = API路径（无前导/），含排序后的查询参数
#   Content-MD5 = base64(md5_digest(body_bytes))  — RFC2616
# 使用方式：
#   client = HuorongClient(access_key_id="...", access_key_secret="...", base_url="...")
#   terminals = await client.list_terminals()
# =============================================================================

import base64
import hashlib
import hmac
import json
import logging
import time
from typing import Any, Dict, List, Optional
from urllib.parse import quote

import httpx

from .exceptions import (
    HuorongApiError,
    HuorongAuthError,
    HuorongConnectionError,
    HuorongError,
)
from .models import (
    HuorongApiResponse,
    TerminalBasicInfo,
    TerminalDetailV2,
    TerminalLeakInfo,
    VirusEventStats,
    VirusHandleResult,
)

logger = logging.getLogger(__name__)

# 默认请求超时（秒）— 火绒内网响应通常在1秒内，3秒足够兜底
# 注意：_virus_events 查询全部终端时可能较慢，需要更长超时
DEFAULT_TIMEOUT = 10.0

# 默认分页大小
DEFAULT_PAGE_SIZE = 20

# 签名有效期（秒）— 请求签名中的 Expires 字段
SIGN_EXPIRES_SECONDS = 300


class HuorongClient:
    """火绒终端安全 API 客户端。

    封装了火绒API的签名认证、请求发送和响应解析。
    所有方法均为异步（async），使用 httpx.AsyncClient 发送请求。

    签名方式：HRESS Authorization Header
    参考：火绒终端安全管理系统API说明文档 v1

    Attributes:
        access_key_id: 火绒 AccessKey ID（控制中心显示为 Secret ID）
        access_key_secret: 火绒 AccessKey Secret（控制中心显示为 Secret Key）
        base_url: 火绒API内网地址（如 http://huorong.oa.servyou-it.com:8080）
        timeout: 请求超时秒数
    """

    def __init__(
        self,
        access_key_id: str,
        access_key_secret: str,
        base_url: str,
        timeout: float = DEFAULT_TIMEOUT,
    ):
        """初始化火绒API客户端。

        Args:
            access_key_id: 火绒 AccessKey ID（控制中心显示为 Secret ID）
            access_key_secret: 火绒 AccessKey Secret（控制中心显示为 Secret Key）
            base_url: 火绒API内网地址（不含尾部斜杠）
            timeout: 请求超时秒数，默认3秒
        """
        self.access_key_id = access_key_id
        self.access_key_secret = access_key_secret
        # 确保 base_url 不以 / 结尾，拼接路径时统一加 /
        self.base_url = base_url.rstrip("/")
        self.timeout = timeout

    # ======================================================================
    # 签名实现（HRESS Authorization Header 方式）
    # ======================================================================

    def _compute_content_md5(self, body_bytes: bytes) -> str:
        """计算请求体的 Content-MD5（RFC2616）。

        算法步骤：
        1. 计算请求体的 MD5 二进制摘要（128位）
        2. 对二进制摘要进行 base64 编码

        注意：不是对32位十六进制字符串编码，而是对原始二进制摘要编码。

        Args:
            body_bytes: 请求体的字节内容

        Returns:
            str: base64 编码的 MD5 摘要
        """
        md5_digest = hashlib.md5(body_bytes).digest()
        return base64.b64encode(md5_digest).decode("utf-8")

    def _build_canonicalized_resource(self, path: str) -> str:
        """构建 CanonicalizedResource。

        根据火绒API文档：
        1. 将 CanonicalizedResource 置为空字符串
        2. 设置要访问的资源路径（去掉前导 /），如 "api/clnts/_list"
        3. 如果请求包含子资源（查询参数），按字典序排列，
           以 & 为分隔符生成子资源字符串，末尾添加 ? 和子资源字符串

        示例：
        - /api/clnts/_list → "api/clnts/_list"
        - /api/group/_info?group_id=1 → "api/group/_info?group_id=1"

        Args:
            path: 请求路径（如 /api/clnts/_list）

        Returns:
            str: CanonicalizedResource 字符串
        """
        # 去掉前导 /
        return path.lstrip("/")

    def _sign_request(
        self,
        method: str,
        path: str,
        body_bytes: bytes = b"",
    ) -> Dict[str, str]:
        """生成火绒API请求签名（HRESS Authorization Header 方式）。

        签名算法（来自火绒官方API文档）：
        ┌─────────────────────────────────────────────────────────────────┐
        │ Authorization = "HRESS" + AccessKeyId + ":" + Expires + ":" + Signature  │
        │                                                                 │
        │ Signature = urlencode(base64(hmac-sha1(AccessKeySecret,         │
        │           AccessKeyId + "\\n"                                    │
        │           + Expires + "\\n"                                      │
        │           + HTTP-METHOD + "\\n"                                  │
        │           + Content-MD5 + "\\n"                                  │
        │           + CanonicalizedResource)))                            │
        └─────────────────────────────────────────────────────────────────┘

        其中：
        - AccessKeyId: 标识用户身份
        - Expires: Unix 时间戳，签名过期时间（当前时间 + 300秒）
        - HTTP-METHOD: POST（火绒API统一使用 POST）
        - Content-MD5: 请求体的 RFC2616 MD5（base64编码）
        - CanonicalizedResource: API资源路径（去掉前导/）

        Args:
            method: HTTP方法（POST）
            path: 请求路径（如 /api/clnts/_list）
            body_bytes: 请求体字节内容

        Returns:
            Dict[str, str]: 包含 Authorization 和 Content-Type 的 Header 字典
        """
        # 1. 计算过期时间（Unix时间戳）
        expires = str(int(time.time()) + SIGN_EXPIRES_SECONDS)

        # 2. 计算 Content-MD5（RFC2616: MD5 二进制摘要 → base64）
        content_md5 = self._compute_content_md5(body_bytes) if body_bytes else ""

        # 3. 构建 CanonicalizedResource（去掉前导 /）
        canonicalized_resource = self._build_canonicalized_resource(path)

        # 4. 构建签名字符串
        string_to_sign = (
            self.access_key_id + "\n"
            + expires + "\n"
            + method + "\n"
            + content_md5 + "\n"
            + canonicalized_resource
        )

        # 5. HMAC-SHA1 签名 → base64 编码 → URL 编码
        signature_raw = hmac.new(
            self.access_key_secret.encode("utf-8"),   # 密钥
            string_to_sign.encode("utf-8"),            # 待签名字符串
            hashlib.sha1,                              # 算法
        ).digest()
        signature_b64 = base64.b64encode(signature_raw).decode("utf-8")
        signature_encoded = quote(signature_b64, safe="")

        # 6. 拼接 Authorization Header
        # 格式: "HRESS" + AccessKeyId + ":" + Expires + ":" + Signature
        authorization = f"HRESS{self.access_key_id}:{expires}:{signature_encoded}"

        return {
            "Authorization": authorization,
            "Content-Type": "application/json; charset=utf-8",
        }

    # ======================================================================
    # 通用请求方法
    # ======================================================================

    async def _request(
        self,
        path: str,
        body: Optional[Dict[str, Any]] = None,
    ) -> HuorongApiResponse:
        """发送签名请求到火绒API。

        统一处理：
        1. HRESS 签名 Authorization Header 生成
        2. HTTP请求发送（POST，超时控制）
        3. 响应解析和错误码处理
        4. 异常分类（认证/连接/API业务错误）

        Args:
            path: API路径（如 /api/clnts/_list）
            body: 请求体字典（可选）

        Returns:
            HuorongApiResponse: 火绒API响应

        Raises:
            HuorongConnectionError: 网络不通或超时
            HuorongAuthError: 签名验证失败
            HuorongApiError: 火绒API返回业务错误
        """
        # 构建完整URL
        url = f"{self.base_url}{path}"

        # 序列化请求体为字节（签名基于字节内容）
        body_bytes = json.dumps(body, separators=(",", ":")).encode("utf-8") if body else b"{}"

        # 生成签名 Header
        headers = self._sign_request("POST", path, body_bytes)

        try:
            async with httpx.AsyncClient(timeout=self.timeout) as client:
                logger.debug(
                    f"火绒API请求: POST {url}\n"
                    f"  AccessKeyID: {self.access_key_id}\n"
                    f"  Path: {path}\n"
                    f"  Body: {body_bytes[:200].decode('utf-8', errors='replace')}\n"
                    f"  Authorization: {headers.get('Authorization', 'N/A')[:60]}..."
                )
                response = await client.post(url, headers=headers, content=body_bytes)

                # HTTP层面错误
                if response.status_code == 401:
                    raise HuorongAuthError()
                if response.status_code != 200:
                    raise HuorongApiError(
                        code=response.status_code,
                        message=f"HTTP {response.status_code}: {response.text[:200]}",
                    )

                # 解析JSON响应
                resp_data = response.json()
                api_resp = HuorongApiResponse(**resp_data)

                # 火绒业务错误码处理
                # 官方文档定义的错误码：
                # - errno=0: 成功
                # - errno=1: 认证失败
                # - errno=2: 参数错误
                # - errno=3: 服务器内部错误
                # - errno=4: API未授权
                if api_resp.errcode != 0:
                    if api_resp.errcode == 1 or api_resp.errcode in (401, 403):
                        raise HuorongAuthError(f"认证/权限失败: {api_resp.errmsg}")
                    if api_resp.errcode == 4:
                        raise HuorongApiError(
                            code=api_resp.errcode,
                            message=f"API未授权: {api_resp.errmsg}",
                        )
                    raise HuorongApiError(
                        code=api_resp.errcode,
                        message=api_resp.errmsg,
                    )

                return api_resp

        except httpx.TimeoutException:
            raise HuorongConnectionError(f"火绒API请求超时（{self.timeout}秒）: {url}")
        except httpx.ConnectError:
            raise HuorongConnectionError(f"无法连接火绒服务器: {url}")
        except (HuorongAuthError, HuorongApiError, HuorongConnectionError):
            # 已分类异常，直接向上抛出
            raise
        except Exception as e:
            # 未预期异常，包装为通用错误
            logger.error(f"火绒API未预期异常: {type(e).__name__}: {e}")
            raise HuorongError(code=-1, message=f"火绒API调用异常: {e}")

    # ======================================================================
    # P0 接口：查询能力
    # ======================================================================

    async def list_terminals(
        self,
        group_id: Optional[str] = None,
        page: int = 1,
        per_page: int = DEFAULT_PAGE_SIZE,
    ) -> Dict[str, Any]:
        """查询终端基本信息列表。

        火绒API: POST /api/clnts/_list
        官方参数: limit(每页条数, 默认15, 最大200) + offset(起始索引, 默认0)
        本方法将 page/per_page 转换为 limit/offset，保持外部接口一致。

        Args:
            group_id: 分组ID（可选，不传则查全部分组）
            page: 页码（从1开始，内部转换为offset）
            per_page: 每页条数（内部转换为limit）

        Returns:
            Dict: 包含 total(总数) 和 items(TerminalBasicInfo列表)
        """
        # 火绒API使用 limit/offset 分页，不是 page/per_page
        limit = min(per_page, 200)  # 火绒限制最大200
        offset = (page - 1) * limit

        body: Dict[str, Any] = {
            "limit": limit,
            "offset": offset,
        }
        if group_id:
            body["group_id"] = int(group_id)

        resp = await self._request("/api/clnts/_list", body)

        # 解析响应数据
        data = resp.data or {}
        raw_items = data.get("list", [])
        total = data.get("total", 0)

        items = [TerminalBasicInfo(**item) for item in raw_items]

        return {
            "total": total,
            "items": items,
        }

    async def get_terminal_detail(
        self,
        client_id: str,
        optional_fields: Optional[List[str]] = None,
    ) -> TerminalDetailV2:
        """获取终端详细信息v2。

        火绒API: POST /api/clnts/_info2
        用途：获取终端硬件/软件/资产/网络配置等详细信息

        Args:
            client_id: 终端唯一ID
            optional_fields: 需要返回的可选信息块
                可选值: hardware, software, assets, netconf
                默认全部返回

        Returns:
            TerminalDetailV2: 终端详细信息
        """
        if optional_fields is None:
            optional_fields = ["hardware", "software", "assets", "netconf"]

        body = {
            "client_id": client_id,
            "optional_fields": optional_fields,
        }

        resp = await self._request("/api/clnts/_info2", body)
        data = resp.data or {}

        return TerminalDetailV2(**data)

    async def list_terminal_leaks(
        self,
        group_id: Optional[str] = None,
        page: int = 1,
        per_page: int = DEFAULT_PAGE_SIZE,
    ) -> Dict[str, Any]:
        """查询存在高危漏洞未修复的终端。

        火绒API: POST /api/clnts/_leak
        官方参数: limit(每页条数, 默认15, 最大200) + offset(起始索引, 默认0)
        说明：返回的是"存在高危漏洞的终端列表"，不是漏洞详情。
        每条记录是终端信息，字段名与 _list 不同：
        - cid (非 client_id)
        - hostname (非 computer_name)
        - ip_addr (非 local_ip)
        - stat (1=离线,2=在线,3=异常，非 is_online 布尔值)
        外层有 all_client（终端总数）和 risk_client（高危终端数）统计。

        Args:
            group_id: 分组ID（可选，不传则查全部分组）
            page: 页码（从1开始，内部转换为offset）
            per_page: 每页条数（内部转换为limit）

        Returns:
            Dict: 包含 total(高危终端总数), risk_client(高危终端数),
                  all_client(全部终端数) 和 items(TerminalLeakInfo列表)
        """
        # 火绒API使用 limit/offset 分页
        limit = min(per_page, 200)
        offset = (page - 1) * limit

        body: Dict[str, Any] = {
            "limit": limit,
            "offset": offset,
        }
        if group_id:
            body["group_id"] = int(group_id)

        resp = await self._request("/api/clnts/_leak", body)

        # 解析响应数据
        data = resp.data or {}
        raw_items = data.get("list", [])
        # _leak 不返回 total，但有 all_client 和 risk_client 统计
        all_client = data.get("all_client", 0)
        risk_client = data.get("risk_client", 0)

        items = [TerminalLeakInfo(**item) for item in raw_items]

        return {
            "total": risk_client,  # 高危终端总数 = risk_client
            "all_client": all_client,  # 全部终端数
            "risk_client": risk_client,  # 高危终端数
            "items": items,
        }

    async def get_virus_events(
        self,
        client_id: Optional[str] = None,
        group_id: Optional[str] = None,
        query_type: int = 2,
        begin_time: Optional[int] = None,
        end_time: Optional[int] = None,
        page: int = 1,
        per_page: int = DEFAULT_PAGE_SIZE,
    ) -> Dict[str, Any]:
        """查询终端病毒事件统计。

        火绒API: POST /api/clnts/_virus_events
        官方参数:
        - type: 查询类型（必填）
            0=使用终端唯一标识查询（client_id字段必填）
            1=使用分组ID查询（group_id字段必填）
            2=查询全部终端日志（client_id和group_id字段可忽略）
        - client_id: 终端唯一标识
        - group_id: 分组ID
        - begin_time/end_time: 日志范围时间（Unix时间戳，默认全部时间）
        - limit/offset: 分页参数
        说明：返回终端维度的病毒日志统计，含 count(总数) 和
        result{success/fail/ignored/trusted}（处理结果明细）。

        Args:
            client_id: 终端唯一ID（type=0时必填）
            group_id: 分组ID（type=1时必填）
            query_type: 查询类型，默认2(查全部)
            begin_time: 日志开始时间（Unix时间戳，可选）
            end_time: 日志结束时间（Unix时间戳，可选）
            page: 页码（从1开始，内部转换为offset）
            per_page: 每页条数（内部转换为limit）

        Returns:
            Dict: 包含 total(总数) 和 items(VirusEventStats列表)
        """
        # 火绒API使用 limit/offset 分页
        limit = min(per_page, 200)
        offset = (page - 1) * limit

        body: Dict[str, Any] = {
            "type": query_type,
            "limit": limit,
            "offset": offset,
        }
        # 根据查询类型添加可选参数
        if query_type == 0 and client_id:
            body["client_id"] = client_id
        if query_type in (0, 1) and group_id:
            body["group_id"] = int(group_id)
        # 时间范围过滤
        if begin_time:
            body["begin_time"] = begin_time
        if end_time:
            body["end_time"] = end_time

        resp = await self._request("/api/clnts/_virus_events", body)

        data = resp.data or {}
        raw_items = data.get("list", [])
        total = data.get("total", 0)

        items = [VirusEventStats(**item) for item in raw_items]

        return {
            "total": total,
            "items": items,
        }

    # ======================================================================
    # P1 接口：控制能力
    # ======================================================================

    async def isolate_terminal(
        self,
        client_ids: List[str],
    ) -> Dict[str, Any]:
        """隔离终端（断网）。

        火绒API: POST /api/task/_create (type=netctrl, net_isolation=true)
        安全等级: 🔴 高危操作，调用方必须确保：
            1. 仅 admin 角色可调用
            2. 已完成二次确认
            3. 已记录操作原因

        Args:
            client_ids: 目标终端ID列表

        Returns:
            Dict: 火绒API响应的data部分
        """
        body = {
            "type": "netctrl",
            "net_isolation": True,
            "clients": client_ids,
        }

        resp = await self._request("/api/task/_create", body)
        logger.warning(f"火绒终端隔离操作: client_ids={client_ids}")
        return resp.data or {}

    async def unisolate_terminal(
        self,
        client_ids: List[str],
    ) -> Dict[str, Any]:
        """解除终端隔离（恢复网络）。

        火绒API: POST /api/task/_create (type=netctrl, net_isolation=false)

        Args:
            client_ids: 目标终端ID列表

        Returns:
            Dict: 火绒API响应的data部分
        """
        body = {
            "type": "netctrl",
            "net_isolation": False,
            "clients": client_ids,
        }

        resp = await self._request("/api/task/_create", body)
        logger.info(f"火绒终端解除隔离: client_ids={client_ids}")
        return resp.data or {}

    async def create_scan_task(
        self,
        client_ids: List[str],
        scan_type: str = "quick_scan",
    ) -> Dict[str, Any]:
        """创建终端扫描任务。

        火绒API: POST /api/task/_create
        扫描类型: quick_scan(快速扫描) / full_scan(全盘扫描) / custom_scan(自定义扫描)

        Args:
            client_ids: 目标终端ID列表
            scan_type: 扫描类型，默认快速扫描

        Returns:
            Dict: 火绒API响应的data部分
        """
        body = {
            "type": scan_type,
            "clients": client_ids,
        }

        resp = await self._request("/api/task/_create", body)
        logger.info(f"火绒终端扫描任务: type={scan_type}, client_ids={client_ids}")
        return resp.data or {}

    async def send_notification(
        self,
        client_ids: List[str],
        content: str,
    ) -> Dict[str, Any]:
        """向终端发送通知。

        火绒API: POST /api/task/_create (type=message)

        Args:
            client_ids: 目标终端ID列表
            content: 通知内容

        Returns:
            Dict: 火绒API响应的data部分
        """
        body = {
            "type": "message",
            "clients": client_ids,
            "content": content,
        }

        resp = await self._request("/api/task/_create", body)
        logger.info(f"火绒终端通知: client_ids={client_ids}, content={content[:50]}")
        return resp.data or {}

    # ======================================================================
    # 测试连接
    # ======================================================================

    async def test_connection(self) -> Dict[str, Any]:
        """测试火绒API连接是否正常。

        使用 _list 接口（page=1, per_page=1）进行轻量级连接测试，
        验证签名是否正确、网络是否可达。

        Returns:
            Dict: 包含 success(bool) 和 message(str)
        """
        try:
            result = await self.list_terminals(page=1, per_page=1)
            return {
                "success": True,
                "message": f"连接成功，共 {result.get('total', 0)} 个终端",
                "total_terminals": result.get("total", 0),
            }
        except HuorongAuthError as e:
            return {
                "success": False,
                "message": f"认证失败: {e.message}",
            }
        except HuorongConnectionError as e:
            return {
                "success": False,
                "message": f"连接失败: {e.message}",
            }
        except HuorongError as e:
            return {
                "success": False,
                "message": f"测试失败: {e.message}",
            }
