"""
设备连接方案 · 可切换驱动层（Connection Provider Manager）

产品真源（卡若 2026-05-29）：
  存客宝四端（存客宝/触客宝/AI数智员工/SuperAdmin）的「所有设备连接」需要一个
  **可切换的多方案开关**——同一套业务接口（/api/v3/* · BFF /v1/workphone/*）背后，
  可在以下「设备连接解决方案」之间一键切换，且都能直接使用：

    1. jiqing   机擎工作手机（本 SDK 原生：WebSocket Agent + 设备本机 Frida，device_transport）—— 默认
    2. aochuang 奥创工作手机（007 私域管理 HTTP API · 007.siyuguanli.com）
    3. legacy   现有连接形式（S2 / 旧接口 / 自建工作手机，配置式 HTTP 驱动）
    4. custom_* 预留接口（配置式 HTTP 驱动模板，新增方案无需改代码即可接入）

设计要点：
  - 统一 Provider 抽象 `BaseConnectionProvider`：list_devices / send_message / get_contacts / execute / health。
  - `ConnectionProviderManager`：注册表 + 三级开关（全局 / 按项目 / 按设备）+ 持久化。
  - 切换不影响既有 339 路由：jiqing 直接复用 device_transport（不绕过无线主控铁律）。
  - http 驱动用「端点映射 + 字段映射」描述任意第三方工作手机 API，确保「预留其他方案」可落地。

真源文档：开发文档/1、需求/修改/工作手机_设备Agent与基础设施_20260529.md §3.6 连接方案可切换驱动层
"""

from __future__ import annotations

import json
import logging
import os
import threading
import time
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Any, Dict, List, Optional

logger = logging.getLogger(__name__)

# 持久化配置路径（与 hook/device_modules.json 同级 data 目录）
_DATA_DIR = Path(__file__).resolve().parents[1] / "data"
_CONFIG_PATH = _DATA_DIR / "connection_providers.json"

# 切换作用域
SCOPE_GLOBAL = "global"
SCOPE_PROJECT = "project"
SCOPE_DEVICE = "device"

# 规范化动作（canonical action）——所有 Provider 须能映射这些动作
CANONICAL_ACTIONS = (
    "list_devices",
    "send_message",
    "batch_send_message",
    "get_contacts",
    "get_messages",
    "add_friend",
    "post_moments",
    "execute",  # 通用 {script, action, params}
)


def _env(name: str, default: str = "") -> str:
    return os.environ.get(name, default) or default


# =============================================================================
# Provider 抽象
# =============================================================================

class BaseConnectionProvider(ABC):
    """单个「设备连接方案/解决方案」的统一抽象。"""

    kind: str = "base"

    def __init__(self, config: Dict[str, Any]):
        self.id: str = config.get("id", "")
        self.name: str = config.get("name", self.id)
        self.enabled: bool = bool(config.get("enabled", True))
        self.builtin: bool = bool(config.get("builtin", False))
        self.config: Dict[str, Any] = config

    # --- 元信息 ---
    def meta(self) -> Dict[str, Any]:
        return {
            "id": self.id,
            "name": self.name,
            "kind": self.kind,
            "enabled": self.enabled,
            "builtin": self.builtin,
            "capabilities": self.capabilities(),
            "desc": self.config.get("desc", ""),
        }

    def capabilities(self) -> List[str]:
        return list(CANONICAL_ACTIONS)

    # --- 健康检查 ---
    @abstractmethod
    async def health(self) -> Dict[str, Any]:
        ...

    # --- 业务执行（统一入口）---
    @abstractmethod
    async def execute(self, device_id: str, script: str, action: str, params: Dict[str, Any],
                      *, timeout: int = 120) -> Dict[str, Any]:
        ...

    async def list_devices(self) -> Dict[str, Any]:
        return await self.execute("", "system", "list_devices", {})

    async def send_message(self, device_id: str, platform: str, to_id: str, content: str,
                           *, msg_type: str = "text", **kw) -> Dict[str, Any]:
        params = {"to_id": to_id, "content": content, "msg_type": msg_type, **kw}
        return await self.execute(device_id, platform, "send_message", params)


# =============================================================================
# 1. 机擎原生 Provider（默认） — 复用 device_transport（无线主控铁律）
# =============================================================================

class JiqingNativeProvider(BaseConnectionProvider):
    """机擎工作手机：本 SDK 原生通道（WebSocket Agent + 设备本机 Frida）。"""

    kind = "native"

    async def health(self) -> Dict[str, Any]:
        try:
            from services.ws_hub import ws_hub
            return {
                "provider": self.id,
                "ok": True,
                "ws_online": len(ws_hub.connections),
                "online_device_ids": list(ws_hub.connections.keys()),
            }
        except Exception as e:  # pragma: no cover
            return {"provider": self.id, "ok": False, "error": str(e)}

    async def list_devices(self) -> Dict[str, Any]:
        from services.ws_hub import ws_hub
        devices = ws_hub.get_online_devices()
        return {"code": 200, "success": True, "provider": self.id,
                "data": devices, "count": len(devices)}

    async def execute(self, device_id: str, script: str, action: str, params: Dict[str, Any],
                      *, timeout: int = 120) -> Dict[str, Any]:
        if action == "list_devices":
            return await self.list_devices()
        from services.device_transport import device_transport
        hook_only = device_transport.should_force_hook_only(script, action, bool(params.get("hook_only")))
        result = await device_transport.execute_via_ws(
            device_id, script, action, params, timeout=timeout, hook_only=hook_only,
        )
        result.setdefault("provider", self.id)
        return result


# =============================================================================
# 2/3/4. 配置式 HTTP Provider（奥创 / 现有连接 / 自定义预留）
# =============================================================================

class HttpConnectionProvider(BaseConnectionProvider):
    """
    配置式 HTTP 工作手机驱动——通过「端点映射 + 字段映射」对接任意第三方工作手机 API。

    config 示例（奥创 007 私域）：
    {
      "id": "aochuang", "name": "奥创工作手机(007私域)", "kind": "http", "enabled": false,
      "base_url": "https://007.siyuguanli.com/api",
      "auth": {"type": "bearer", "token": "", "token_env": "AOCHUANG_TOKEN", "header": "Authorization"},
      "endpoints": {
        "list_devices": {"method": "GET", "path": "/devices", "data_path": "data"},
        "send_message": {"method": "POST", "path": "/message/send",
                          "body_map": {"device_id": "deviceId", "to_id": "wxid", "content": "content"}},
        "get_contacts": {"method": "GET", "path": "/wechat/contacts", "query_map": {"device_id": "deviceId"}}
      }
    }
    """

    kind = "http"

    def base_url(self) -> str:
        return (self.config.get("base_url") or _env(self.config.get("base_url_env", ""))).rstrip("/")

    def _auth_headers(self) -> Dict[str, str]:
        auth = self.config.get("auth") or {}
        token = auth.get("token") or _env(auth.get("token_env", ""))
        if not token:
            return {}
        header = auth.get("header", "Authorization")
        atype = (auth.get("type") or "bearer").lower()
        if atype == "bearer":
            return {header: f"Bearer {token}"}
        if atype == "apikey":
            return {header: token}
        return {header: token}

    def capabilities(self) -> List[str]:
        eps = self.config.get("endpoints") or {}
        caps = [a for a in CANONICAL_ACTIONS if a in eps]
        return caps or ["execute"]

    def _endpoint(self, action: str) -> Optional[Dict[str, Any]]:
        return (self.config.get("endpoints") or {}).get(action)

    @staticmethod
    def _apply_map(src: Dict[str, Any], mapping: Optional[Dict[str, str]]) -> Dict[str, Any]:
        if not mapping:
            return dict(src)
        out: Dict[str, Any] = {}
        for canon, target in mapping.items():
            if canon in src and src[canon] is not None:
                out[target] = src[canon]
        # 透传未在映射表中的字段（保守：保留原键）
        for k, v in src.items():
            if k not in mapping and k not in out and v is not None:
                out[k] = v
        return out

    async def _request(self, method: str, path: str, *, query: Optional[Dict] = None,
                       body: Optional[Dict] = None, timeout: int = 60) -> Dict[str, Any]:
        base = self.base_url()
        if not base:
            return {"code": 503, "success": False, "provider": self.id,
                    "message": f"连接方案 {self.id} 未配置 base_url（请先配置/启用）"}
        url = base + path
        headers = {"Content-Type": "application/json", **self._auth_headers()}
        try:
            import httpx  # type: ignore
            async with httpx.AsyncClient(timeout=timeout) as client:
                resp = await client.request(method.upper(), url, params=query, json=body, headers=headers)
                try:
                    data = resp.json()
                except Exception:
                    data = {"raw": resp.text}
                ok = 200 <= resp.status_code < 300
                return {"code": resp.status_code, "success": ok, "provider": self.id, "data": data,
                        "_channel_used": f"http/{self.id}"}
        except ImportError:
            # 无 httpx 时退回标准库（同步，放线程池）
            import asyncio
            return await asyncio.get_running_loop().run_in_executor(
                None, lambda: self._request_urllib(method, url, headers, query, body, timeout))
        except Exception as e:
            return {"code": 502, "success": False, "provider": self.id,
                    "message": f"连接方案 {self.id} 请求失败: {e}", "_channel_used": f"http/{self.id}"}

    def _request_urllib(self, method: str, url: str, headers: Dict[str, str],
                        query: Optional[Dict], body: Optional[Dict], timeout: int) -> Dict[str, Any]:
        import urllib.parse
        import urllib.request
        try:
            if query:
                url = f"{url}?{urllib.parse.urlencode(query)}"
            data = json.dumps(body).encode("utf-8") if body is not None else None
            req = urllib.request.Request(url, data=data, headers=headers, method=method.upper())
            with urllib.request.urlopen(req, timeout=timeout) as resp:
                raw = resp.read().decode("utf-8")
                try:
                    parsed = json.loads(raw)
                except Exception:
                    parsed = {"raw": raw}
                return {"code": resp.status, "success": 200 <= resp.status < 300,
                        "provider": self.id, "data": parsed, "_channel_used": f"http/{self.id}"}
        except Exception as e:
            return {"code": 502, "success": False, "provider": self.id,
                    "message": f"连接方案 {self.id} 请求失败: {e}", "_channel_used": f"http/{self.id}"}

    async def health(self) -> Dict[str, Any]:
        base = self.base_url()
        if not base:
            return {"provider": self.id, "ok": False, "error": "未配置 base_url"}
        hc = self.config.get("health") or {}
        method = hc.get("method", "GET")
        path = hc.get("path", "/health")
        res = await self._request(method, path, timeout=10)
        return {"provider": self.id, "ok": bool(res.get("success")), "detail": res}

    async def execute(self, device_id: str, script: str, action: str, params: Dict[str, Any],
                      *, timeout: int = 120) -> Dict[str, Any]:
        ep = self._endpoint(action)
        if not ep:
            return {"code": 501, "success": False, "provider": self.id,
                    "message": f"连接方案 {self.id} 未定义动作映射: {action}（可在 provider.endpoints 中配置）"}
        method = ep.get("method", "POST")
        path = ep.get("path", "/")
        src = {"device_id": device_id, "platform": script, **(params or {})}
        query = None
        body = None
        if method.upper() == "GET":
            query = self._apply_map(src, ep.get("query_map"))
        else:
            body = self._apply_map(src, ep.get("body_map"))
        res = await self._request(method, path, query=query, body=body, timeout=timeout)
        # 可选 data_path 提取
        data_path = ep.get("data_path")
        if data_path and isinstance(res.get("data"), dict):
            res["data"] = res["data"].get(data_path, res["data"])
        return res


# =============================================================================
# 管理器
# =============================================================================

class ConnectionProviderManager:
    """连接方案注册表 + 三级开关 + 持久化。"""

    def __init__(self):
        self._lock = threading.RLock()
        self._providers: Dict[str, BaseConnectionProvider] = {}
        self._active: Dict[str, Any] = {"global": "jiqing", "projects": {}, "devices": {}}
        self._loaded = False

    # ---- 默认配置 ----
    @staticmethod
    def _default_config() -> Dict[str, Any]:
        return {
            "active": {"global": "jiqing", "projects": {}, "devices": {}},
            "providers": {
                "jiqing": {
                    "id": "jiqing", "name": "机擎工作手机（本SDK原生·WS+Frida）",
                    "kind": "native", "enabled": True, "builtin": True,
                    "desc": "默认方案：WebSocket Agent + 设备本机 Frida，经 device_transport 无线主控",
                },
                "aochuang": {
                    "id": "aochuang", "name": "奥创工作手机（007私域）",
                    "kind": "http", "enabled": False, "builtin": True,
                    "desc": "奥创云脑工作手机 007 私域管理 HTTP API（Hook 注入方案）",
                    "base_url": "", "base_url_env": "AOCHUANG_BASE_URL",
                    "auth": {"type": "bearer", "token": "", "token_env": "AOCHUANG_TOKEN",
                             "header": "Authorization"},
                    "health": {"method": "GET", "path": "/devices"},
                    "endpoints": {
                        "list_devices": {"method": "GET", "path": "/devices", "data_path": "data"},
                        "send_message": {"method": "POST", "path": "/message/send",
                                         "body_map": {"device_id": "deviceId", "to_id": "wxid",
                                                      "content": "content", "msg_type": "msgType"}},
                        "batch_send_message": {"method": "POST", "path": "/message/batch-send",
                                               "body_map": {"device_id": "deviceId"}},
                        "get_contacts": {"method": "GET", "path": "/wechat/contacts",
                                         "query_map": {"device_id": "deviceId"}, "data_path": "data"},
                        "add_friend": {"method": "POST", "path": "/friend/add",
                                       "body_map": {"device_id": "deviceId", "to_id": "keyword"}},
                        "post_moments": {"method": "POST", "path": "/moments/post",
                                         "body_map": {"device_id": "deviceId", "content": "content"}},
                    },
                },
                "legacy": {
                    "id": "legacy", "name": "现有连接形式（S2/旧接口）",
                    "kind": "http", "enabled": False, "builtin": True,
                    "desc": "存客宝现有/历史设备连接（自建工作手机或 S2 旧接口），配置式 HTTP 驱动",
                    "base_url": "", "base_url_env": "LEGACY_PROVIDER_BASE_URL",
                    "auth": {"type": "apikey", "token": "", "token_env": "LEGACY_PROVIDER_TOKEN",
                             "header": "X-API-Key"},
                    "endpoints": {
                        "send_message": {"method": "POST", "path": "/send"},
                        "list_devices": {"method": "GET", "path": "/devices"},
                    },
                },
            },
        }

    def load(self, force: bool = False) -> None:
        with self._lock:
            if self._loaded and not force:
                return
            cfg = self._default_config()
            if _CONFIG_PATH.exists():
                try:
                    saved = json.loads(_CONFIG_PATH.read_text("utf-8"))
                    # 合并：保留内置 + 覆盖/追加自定义
                    if isinstance(saved.get("providers"), dict):
                        cfg["providers"].update(saved["providers"])
                    if isinstance(saved.get("active"), dict):
                        cfg["active"].update(saved["active"])
                except Exception as e:
                    logger.warning(f"[connection_provider] 读取配置失败，用默认: {e}")
            self._active = cfg["active"]
            self._providers = {}
            for pid, pconf in cfg["providers"].items():
                pconf["id"] = pid
                self._providers[pid] = self._build(pconf)
            self._loaded = True
            logger.info(f"[connection_provider] 已加载 {len(self._providers)} 个连接方案，active.global={self._active.get('global')}")

    @staticmethod
    def _build(pconf: Dict[str, Any]) -> BaseConnectionProvider:
        kind = (pconf.get("kind") or "http").lower()
        if kind == "native":
            return JiqingNativeProvider(pconf)
        return HttpConnectionProvider(pconf)

    def _save(self) -> None:
        with self._lock:
            _DATA_DIR.mkdir(parents=True, exist_ok=True)
            cfg = {
                "active": self._active,
                "providers": {pid: p.config for pid, p in self._providers.items()},
                "_updated_at": int(time.time()),
            }
            _CONFIG_PATH.write_text(json.dumps(cfg, ensure_ascii=False, indent=2), "utf-8")

    # ---- 查询 ----
    def list_providers(self) -> List[Dict[str, Any]]:
        self.load()
        return [p.meta() for p in self._providers.values()]

    def get(self, provider_id: str) -> Optional[BaseConnectionProvider]:
        self.load()
        return self._providers.get(provider_id)

    def active_config(self) -> Dict[str, Any]:
        self.load()
        return dict(self._active)

    def resolve_active_id(self, device_id: str = "", project_id: str = "") -> str:
        """优先级：设备级 > 项目级 > 全局。若解析到的方案被禁用，回退 jiqing。"""
        self.load()
        chosen = ""
        if device_id and device_id in self._active.get("devices", {}):
            chosen = self._active["devices"][device_id]
        elif project_id and project_id in self._active.get("projects", {}):
            chosen = self._active["projects"][project_id]
        else:
            chosen = self._active.get("global", "jiqing")
        prov = self._providers.get(chosen)
        if not prov or not prov.enabled:
            if chosen != "jiqing":
                logger.warning(f"[connection_provider] 方案 {chosen} 不可用/未启用，回退 jiqing")
            return "jiqing"
        return chosen

    def resolve_active(self, device_id: str = "", project_id: str = "") -> BaseConnectionProvider:
        return self._providers[self.resolve_active_id(device_id, project_id)]

    # ---- 开关 ----
    def switch(self, provider_id: str, scope: str = SCOPE_GLOBAL,
               project_id: str = "", device_id: str = "") -> Dict[str, Any]:
        self.load()
        if provider_id not in self._providers:
            raise ValueError(f"未知连接方案: {provider_id}")
        if not self._providers[provider_id].enabled:
            raise ValueError(f"连接方案 {provider_id} 未启用（请先 enable 并配置）")
        with self._lock:
            if scope == SCOPE_GLOBAL:
                self._active["global"] = provider_id
            elif scope == SCOPE_PROJECT:
                if not project_id:
                    raise ValueError("scope=project 需 project_id")
                self._active.setdefault("projects", {})[project_id] = provider_id
            elif scope == SCOPE_DEVICE:
                if not device_id:
                    raise ValueError("scope=device 需 device_id")
                self._active.setdefault("devices", {})[device_id] = provider_id
            else:
                raise ValueError(f"未知 scope: {scope}")
            self._save()
        return {"active": self._active, "switched_to": provider_id, "scope": scope}

    # ---- 注册/更新/删除自定义方案（预留接口）----
    def register(self, pconf: Dict[str, Any]) -> Dict[str, Any]:
        self.load()
        pid = pconf.get("id")
        if not pid:
            raise ValueError("缺少 provider id")
        if pid in self._providers and self._providers[pid].builtin:
            # 内置方案只允许更新配置（enable/base_url/endpoints），不允许改 kind/builtin
            # 跳过空字符串，避免清掉内置 name/desc
            base = self._providers[pid].config
            base.update({
                k: v for k, v in pconf.items()
                if k not in ("kind", "builtin", "id") and not (isinstance(v, str) and v == "")
            })
            pconf = base
        pconf["id"] = pid
        pconf.setdefault("builtin", False)
        with self._lock:
            self._providers[pid] = self._build(pconf)
            self._save()
        return self._providers[pid].meta()

    def remove(self, provider_id: str) -> Dict[str, Any]:
        self.load()
        prov = self._providers.get(provider_id)
        if not prov:
            raise ValueError(f"未知连接方案: {provider_id}")
        if prov.builtin:
            raise ValueError("内置连接方案不可删除（可改用 enable=false 禁用）")
        with self._lock:
            self._providers.pop(provider_id, None)
            # 清理引用
            if self._active.get("global") == provider_id:
                self._active["global"] = "jiqing"
            for scope_key in ("projects", "devices"):
                self._active[scope_key] = {
                    k: v for k, v in self._active.get(scope_key, {}).items() if v != provider_id
                }
            self._save()
        return {"removed": provider_id, "active": self._active}


connection_provider_manager = ConnectionProviderManager()
