"""
工作手机SDK v3.0 - 存客宝对接服务
CunKeBao API Service

功能：
  - 封装存客宝 API 调用逻辑（签名、参数转换、错误处理）
  - 实现 report_lead（线索上报）接口
  - 实现 Frida Hook 事件实时上报机制
  - API Key 管理与请求频率限制

作者：Manus AI
版本：v1.0.0
日期：2026-05-15
"""

import hashlib
import time
import logging
import asyncio
import json
import os
from pathlib import Path
from typing import Optional, Dict, Any, List
from dataclasses import dataclass, field
from collections import defaultdict
try:
    import aiohttp
except ImportError:  # NAS armv7 无 aiohttp wheel 时降级
    aiohttp = None  # type: ignore[assignment]

logger = logging.getLogger(__name__)

_CKB_CONFIG_PATH = Path(__file__).resolve().parent.parent / "data" / "cunke_bao_config.json"

# ═══════════════════════════════════════════════
# 一、配置模型
# ═══════════════════════════════════════════════

@dataclass
class CunKeBaoConfig:
    """存客宝对接配置"""
    api_key: str = ""
    base_url: str = "https://ckbapi.quwanzhi.com/v1/api/scenarios"
    timeout: int = 10
    max_retries: int = 3
    rate_limit_per_minute: int = 60  # 每分钟最大请求数
    enabled: bool = False

    @classmethod
    def from_dict(cls, d: dict) -> "CunKeBaoConfig":
        return cls(**{k: v for k, v in d.items() if k in cls.__dataclass_fields__})


# ═══════════════════════════════════════════════
# 二、签名算法
# ═══════════════════════════════════════════════

def generate_ckb_sign(params: Dict[str, Any], api_key: str) -> str:
    """
    存客宝双重MD5签名算法

    规则：
      1. 移除 sign / apiKey / portrait 字段
      2. 移除 null 和空字符串值
      3. 按键名 ASCII 升序排序
      4. 只取值拼接（无分隔符）
      5. 第一次 MD5(拼接字符串)
      6. 第二次 MD5(firstMd5 + apiKey)
    """
    sign_params = {k: v for k, v in params.items()
                   if k not in ("sign", "apiKey", "portrait")
                   and v is not None and v != ""}

    sorted_keys = sorted(sign_params.keys())
    concat_str = "".join(str(sign_params[k]) for k in sorted_keys)

    first_md5 = hashlib.md5(concat_str.encode("utf-8")).hexdigest()
    final_sign = hashlib.md5((first_md5 + api_key).encode("utf-8")).hexdigest()
    return final_sign


# ═══════════════════════════════════════════════
# 三、数据模型
# ═══════════════════════════════════════════════

@dataclass
class LeadData:
    """线索数据模型（对应存客宝 report_lead 接口）"""
    phone: str = ""                    # 手机号（必填）
    name: str = ""                     # 姓名
    source: str = "微信"               # 来源渠道
    wechat_id: str = ""                # 微信号
    wechat_nickname: str = ""          # 微信昵称
    wechat_remark: str = ""            # 备注名
    tags: List[str] = field(default_factory=list)  # 标签列表
    group_name: str = ""               # 所在群名
    device_id: str = ""                # 设备ID
    extra: Dict[str, Any] = field(default_factory=dict)  # 扩展字段

    def to_api_params(self) -> Dict[str, Any]:
        """转换为存客宝 API 参数格式"""
        params: Dict[str, Any] = {
            "timestamp": int(time.time()),
        }
        if self.phone:
            params["phone"] = self.phone
        if self.name:
            params["name"] = self.name
        if self.source:
            params["source"] = self.source
        if self.wechat_id:
            params["wechatId"] = self.wechat_id
        if self.wechat_nickname:
            params["wechatNickname"] = self.wechat_nickname
        if self.wechat_remark:
            params["remark"] = self.wechat_remark
        if self.tags:
            params["tags"] = ",".join(self.tags)
        if self.group_name:
            params["groupName"] = self.group_name
        if self.device_id:
            params["deviceId"] = self.device_id
        # 合并扩展字段
        params.update(self.extra)
        return params


@dataclass
class FriendAddEvent:
    """新好友添加事件"""
    device_id: str
    wechat_id: str
    nickname: str
    avatar: str = ""
    source: str = "微信添加"
    timestamp: int = field(default_factory=lambda: int(time.time()))


@dataclass
class GroupChangeEvent:
    """群变动事件"""
    device_id: str
    group_id: str
    group_name: str
    event_type: str  # "member_join" | "member_leave" | "group_create"
    member_wechat_id: str = ""
    member_nickname: str = ""
    timestamp: int = field(default_factory=lambda: int(time.time()))


# ═══════════════════════════════════════════════
# 四、速率限制器
# ═══════════════════════════════════════════════

class RateLimiter:
    """简单的滑动窗口速率限制器"""

    def __init__(self, max_calls: int, period: int = 60):
        self.max_calls = max_calls
        self.period = period
        self._calls: List[float] = []

    def is_allowed(self) -> bool:
        now = time.time()
        self._calls = [t for t in self._calls if now - t < self.period]
        if len(self._calls) < self.max_calls:
            self._calls.append(now)
            return True
        return False

    def wait_time(self) -> float:
        if not self._calls:
            return 0.0
        oldest = min(self._calls)
        return max(0.0, self.period - (time.time() - oldest))


# ═══════════════════════════════════════════════
# 五、存客宝服务主类
# ═══════════════════════════════════════════════

class CunKeBaoService:
    """
    存客宝对接服务

    职责：
      - 管理 API Key 配置
      - 封装签名与 HTTP 请求
      - 实现线索上报（report_lead）
      - 处理 Frida Hook 事件上报
      - 速率限制与错误重试
    """

    def __init__(self, config: Optional[CunKeBaoConfig] = None):
        self.config = config or CunKeBaoConfig()
        self._session: Optional[aiohttp.ClientSession] = None
        self._rate_limiter = RateLimiter(
            max_calls=self.config.rate_limit_per_minute,
            period=60
        )
        # 统计计数器
        self._stats = {
            "total_requests": 0,
            "success_count": 0,
            "fail_count": 0,
            "last_error": "",
        }

    def update_config(self, config: CunKeBaoConfig, persist: bool = True):
        """动态更新配置"""
        self.config = config
        self._rate_limiter = RateLimiter(
            max_calls=config.rate_limit_per_minute,
            period=60
        )
        if persist:
            _save_ckb_config(config)
        logger.info(f"[CKB] 配置已更新，enabled={config.enabled}, api_key={'***' if config.api_key else '未设置'}")

    async def _get_session(self) -> "aiohttp.ClientSession":
        if aiohttp is None:
            raise RuntimeError("aiohttp 未安装，存客宝 HTTP 不可用")
        if self._session is None or self._session.closed:
            timeout = aiohttp.ClientTimeout(total=self.config.timeout)
            self._session = aiohttp.ClientSession(timeout=timeout)
        return self._session

    async def close(self):
        if self._session and not self._session.closed:
            await self._session.close()

    # ─── 核心请求方法 ───────────────────────────────

    async def _post_urllib(self, params: Dict[str, Any]) -> Dict[str, Any]:
        """aiohttp 不可用时的 urllib 回退（NAS/精简环境）。"""
        import urllib.error
        import urllib.request

        body = json.dumps(params).encode("utf-8")
        req = urllib.request.Request(
            self.config.base_url,
            data=body,
            headers={"Content-Type": "application/json"},
            method="POST",
        )
        loop = asyncio.get_event_loop()
        try:
            with await loop.run_in_executor(
                None, lambda: urllib.request.urlopen(req, timeout=self.config.timeout)
            ) as resp:
                raw = resp.read().decode("utf-8")
                result = json.loads(raw)
                self._stats["success_count"] += 1
                logger.info(f"[CKB] 上报成功(urllib): {result.get('msg', 'ok')}")
                return result
        except urllib.error.HTTPError as e:
            text = e.read().decode("utf-8", errors="replace")[:200]
            raise RuntimeError(f"HTTP {e.code}: {text}") from e

    async def _post(self, params: Dict[str, Any]) -> Dict[str, Any]:
        """
        发送签名请求到存客宝 API

        Returns:
            {"code": 200, "msg": "success", "data": {...}}
        """
        if not self.config.enabled:
            logger.debug("[CKB] 服务未启用，跳过上报")
            return {"code": -1, "msg": "服务未启用", "data": None}

        if not self.config.api_key:
            logger.warning("[CKB] API Key 未配置，跳过上报")
            return {"code": -1, "msg": "API Key 未配置", "data": None}

        if not self._rate_limiter.is_allowed():
            wait = self._rate_limiter.wait_time()
            logger.warning(f"[CKB] 速率限制，需等待 {wait:.1f}s")
            return {"code": 429, "msg": f"速率限制，请 {wait:.1f}s 后重试", "data": None}

        # 添加 apiKey 和签名
        params["apiKey"] = self.config.api_key
        params["sign"] = generate_ckb_sign(params, self.config.api_key)

        self._stats["total_requests"] += 1
        last_error = ""

        if aiohttp is None:
            try:
                return await self._post_urllib(params)
            except Exception as e:
                last_error = str(e)
                self._stats["fail_count"] += 1
                self._stats["last_error"] = last_error
                logger.error(f"[CKB] urllib 上报失败: {last_error}")
                return {"code": 500, "msg": last_error, "data": None}

        for attempt in range(self.config.max_retries):
            try:
                session = await self._get_session()
                async with session.post(
                    self.config.base_url,
                    json=params,
                    headers={"Content-Type": "application/json"}
                ) as resp:
                    if resp.status == 200:
                        result = await resp.json()
                        self._stats["success_count"] += 1
                        logger.info(f"[CKB] 上报成功: {result.get('msg', 'ok')}")
                        return result
                    else:
                        text = await resp.text()
                        last_error = f"HTTP {resp.status}: {text[:200]}"
                        logger.warning(f"[CKB] 请求失败 (attempt {attempt+1}): {last_error}")

            except aiohttp.ClientConnectorError as e:
                last_error = f"连接失败: {e}"
                logger.error(f"[CKB] {last_error} (attempt {attempt+1})")
            except asyncio.TimeoutError:
                last_error = "请求超时"
                logger.error(f"[CKB] 请求超时 (attempt {attempt+1})")
            except Exception as e:
                last_error = str(e)
                logger.error(f"[CKB] 未知错误: {e} (attempt {attempt+1})")

            if attempt < self.config.max_retries - 1:
                await asyncio.sleep(2 ** attempt)  # 指数退避

        self._stats["fail_count"] += 1
        self._stats["last_error"] = last_error
        return {"code": 500, "msg": last_error, "data": None}

    # ─── 业务方法 ───────────────────────────────────

    async def report_lead(self, lead: LeadData) -> Dict[str, Any]:
        """
        上报线索到存客宝

        Args:
            lead: 线索数据

        Returns:
            存客宝 API 响应
        """
        params = lead.to_api_params()
        logger.info(f"[CKB] 上报线索: wechat_id={lead.wechat_id}, nickname={lead.wechat_nickname}")
        result = await self._post(params)
        return result

    async def report_friend_add(self, event: FriendAddEvent) -> Dict[str, Any]:
        """
        上报新好友添加事件

        将 Frida Hook 捕获的新好友事件转换为存客宝线索上报
        """
        lead = LeadData(
            wechat_id=event.wechat_id,
            wechat_nickname=event.nickname,
            source=event.source,
            device_id=event.device_id,
            extra={"avatar": event.avatar, "event_time": event.timestamp}
        )
        return await self.report_lead(lead)

    async def report_group_change(self, event: GroupChangeEvent) -> Dict[str, Any]:
        """
        上报群变动事件

        将 Frida Hook 捕获的群成员变动转换为存客宝线索上报
        """
        if event.event_type == "member_join":
            lead = LeadData(
                wechat_id=event.member_wechat_id,
                wechat_nickname=event.member_nickname,
                source="群加入",
                group_name=event.group_name,
                device_id=event.device_id,
                extra={"group_id": event.group_id, "event_time": event.timestamp}
            )
            return await self.report_lead(lead)
        else:
            logger.debug(f"[CKB] 群事件 {event.event_type} 暂不上报")
            return {"code": 0, "msg": "跳过", "data": None}

    async def batch_report_contacts(
        self,
        contacts: List[Dict[str, Any]],
        device_id: str,
        source: str = "微信通讯录"
    ) -> Dict[str, Any]:
        """
        批量上报联系人（来自 Frida Hook 获取的通讯录）

        Args:
            contacts: 联系人列表，每项包含 wechat_id, nickname, remark, tags 等
            device_id: 设备ID
            source: 来源标识

        Returns:
            {"total": N, "success": M, "failed": K, "errors": [...]}
        """
        results = {"total": len(contacts), "success": 0, "failed": 0, "errors": []}

        for contact in contacts:
            lead = LeadData(
                wechat_id=contact.get("wechat_id", ""),
                wechat_nickname=contact.get("nickname", ""),
                wechat_remark=contact.get("remark", ""),
                tags=contact.get("tags", []),
                phone=contact.get("phone", ""),
                source=source,
                device_id=device_id,
            )
            result = await self.report_lead(lead)
            if result.get("code") == 200:
                results["success"] += 1
            else:
                results["failed"] += 1
                results["errors"].append({
                    "wechat_id": lead.wechat_id,
                    "error": result.get("msg", "未知错误")
                })
            # 批量上报时适当延迟，避免触发速率限制
            await asyncio.sleep(0.1)

        logger.info(f"[CKB] 批量上报完成: 成功={results['success']}, 失败={results['failed']}")
        return results

    def get_stats(self) -> Dict[str, Any]:
        """获取服务统计信息"""
        return {
            **self._stats,
            "enabled": self.config.enabled,
            "api_key_set": bool(self.config.api_key),
            "rate_limit": self.config.rate_limit_per_minute,
        }


# ═══════════════════════════════════════════════
# 六、全局单例
# ═══════════════════════════════════════════════

cunke_bao_service = CunKeBaoService()


def _save_ckb_config(config: CunKeBaoConfig) -> None:
    try:
        _CKB_CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
        payload = {
            "api_key": config.api_key,
            "base_url": config.base_url,
            "timeout": config.timeout,
            "max_retries": config.max_retries,
            "rate_limit_per_minute": config.rate_limit_per_minute,
            "enabled": config.enabled,
        }
        _CKB_CONFIG_PATH.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
    except Exception as exc:
        logger.warning("[CKB] 配置落盘失败: %s", exc)


def _load_ckb_from_file() -> None:
    try:
        if not _CKB_CONFIG_PATH.is_file():
            return
        data = json.loads(_CKB_CONFIG_PATH.read_text(encoding="utf-8"))
        if not data.get("api_key"):
            return
        cunke_bao_service.update_config(CunKeBaoConfig.from_dict(data), persist=False)
        logger.info("[CKB] 已从 %s 加载配置 enabled=%s", _CKB_CONFIG_PATH.name, data.get("enabled"))
    except Exception as exc:
        logger.warning("[CKB] 读取配置失败: %s", exc)


def _bootstrap_ckb_from_env() -> None:
    """启动时从环境变量加载 CKB 凭证（CUNKEBAO_API_KEY / CKB_API_KEY）。"""
    key = (os.environ.get("CUNKEBAO_API_KEY") or os.environ.get("CKB_API_KEY") or "").strip()
    if not key:
        return
    base = (os.environ.get("CUNKEBAO_BASE_URL") or os.environ.get("CKB_BASE_URL") or "").strip()
    cfg = CunKeBaoConfig(
        api_key=key,
        base_url=base or "https://ckbapi.quwanzhi.com/v1/api/scenarios",
        enabled=True,
    )
    cunke_bao_service.update_config(cfg)
    logger.info("[CKB] 已从环境变量启用存客宝对接 enabled=true")


_load_ckb_from_file()
_bootstrap_ckb_from_env()
