#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
钉钉 Stream 模式事件监听客户端

取代 HTTP 回调模式，通过 WebSocket 长连接接收钉钉通讯录变更事件。
无需公网回调地址，无需配置 callback_token/callback_aes_key/corp_id。

使用方式：
1. 在钉钉开放平台应用配置中，开启 Stream 模式推送
2. 后端启动后自动建立长连接，接收事件推送

Stream 模式事件数据格式与 HTTP 推送不同：
  HTTP:  { "EventType": "...", "UserId": [...], "DeptId": [...] }
  Stream: event.headers.event_type 为事件类型，
          event.data 为事件体 { "timeStamp": "...", "deptId": [...] / "userId": [...] }
"""
import asyncio
import logging
from collections import OrderedDict
from datetime import datetime
from typing import Optional

import dingtalk_stream
from dingtalk_stream import AckMessage
from dingtalk_stream import EventMessage

logger = logging.getLogger(__name__)

_DEDUP_MAX = 256


class _EventDedup:
    """基于 OrderedDict 的有界去重缓存，保留最近 N 个 event_id"""

    def __init__(self, maxlen: int = _DEDUP_MAX):
        self._seen: OrderedDict[str, None] = OrderedDict()
        self._maxlen = maxlen

    def is_duplicate(self, event_id: str) -> bool:
        if event_id in self._seen:
            self._seen.move_to_end(event_id)
            return True
        self._seen[event_id] = None
        if len(self._seen) > self._maxlen:
            self._seen.popitem(last=False)
        return False


class DingtalkStreamEventHandler(dingtalk_stream.EventHandler):
    """钉钉 Stream 事件处理器 - 处理通讯录变更事件"""

    _dedup = _EventDedup()

    async def process(self, event: EventMessage) -> tuple:
        try:
            event_type = event.headers.event_type if event.headers else ''
            event_data = event.data if isinstance(event.data, dict) else {}

            event_id = getattr(event.headers, 'event_id', None) or ''
            if event_id and self._dedup.is_duplicate(event_id):
                logger.info("跳过重复事件: event_id=%s, type=%s", event_id, event_type)
                return AckMessage.STATUS_OK, 'OK'

            DingtalkStreamManager._record_event(event_type)

            if not event_type:
                logger.warning(f"收到未知事件: {event_data}")
                return AckMessage.STATUS_OK, 'OK'

            event_body = event_data

            logger.info(f"收到钉钉 Stream 事件: {event_type}")

            from core.dingtalk_sync.callback_handler import DingtalkCallbackHandler
            from core.dingtalk_sync.callback_handler import DEPT_EVENTS, USER_EVENTS

            from app.config_manager import config_manager
            config = await config_manager.get_group("sync_dingtalk")
            enable_dept = config.get("enable_dept_event") == "true"
            enable_user = config.get("enable_user_event") == "true"

            if event_type in DEPT_EVENTS and enable_dept:
                normalized = self._normalize_dept_event(event_body)
                await DingtalkCallbackHandler.handle_event(event_type, normalized)

            elif event_type in USER_EVENTS and enable_user:
                normalized = self._normalize_user_event(event_body)
                await DingtalkCallbackHandler.handle_event(event_type, normalized)

            else:
                logger.info(f"忽略未启用的事件: {event_type}")

        except Exception as e:
            logger.error(f"处理 Stream 事件失败: {e}", exc_info=True)

        return AckMessage.STATUS_OK, 'OK'

    @staticmethod
    def _normalize_dept_event(event_body: dict) -> dict:
        """将 Stream 模式的部门事件字段转为 HTTP 回调格式"""
        dept_ids = event_body.get('deptId') or event_body.get('DeptId') or []
        return {"DeptId": dept_ids}

    @staticmethod
    def _normalize_user_event(event_body: dict) -> dict:
        """将 Stream 模式的用户事件字段转为 HTTP 回调格式"""
        user_ids = event_body.get('userId') or event_body.get('UserId') or []
        return {"UserId": user_ids}


class DingtalkStreamManager:
    """钉钉 Stream 模式连接管理器"""

    _client: Optional[dingtalk_stream.DingTalkStreamClient] = None
    _task: Optional[asyncio.Task] = None
    _connected: bool = False
    _started_at: Optional[str] = None

    # 事件统计
    _event_stats = {
        "total_events": 0,
        "last_event_type": None,
        "last_event_time": None,
    }

    @classmethod
    def _record_event(cls, event_type: str):
        cls._event_stats["total_events"] += 1
        cls._event_stats["last_event_type"] = event_type
        cls._event_stats["last_event_time"] = datetime.now().isoformat()

    @classmethod
    async def start(cls):
        """启动 Stream 连接，使用同步配置中的 app_key/app_secret"""
        from app.config_manager import config_manager

        config = await config_manager.get_group("sync_dingtalk")
        app_key = config.get("app_key")
        app_secret = config.get("app_secret")

        if not app_key or not app_secret:
            logger.warning("钉钉同步凭证未配置（app_key/app_secret），Stream 模式未启动")
            return

        credential = dingtalk_stream.Credential(app_key, app_secret)
        client = dingtalk_stream.DingTalkStreamClient(credential)

        handler = DingtalkStreamEventHandler()
        client.register_all_event_handler(handler)

        cls._client = client
        cls._connected = True
        cls._started_at = datetime.now().isoformat()
        cls._task = asyncio.create_task(cls._run(client))
        logger.info("钉钉 Stream 模式已启动（app_key=%s）", app_key[:6] + "***")

    @classmethod
    async def _run(cls, client: dingtalk_stream.DingTalkStreamClient):
        """
        运行 Stream 客户端（带超时和重连）。

        SDK 的 open_connection() 使用同步 requests（短暂阻塞可接受），
        但 websockets.connect() 可能因为网络/防火墙问题长时间卡住，
        因此对整个 start() 加超时保护，超时后自动重试。
        """
        import json as _json
        import websockets as _ws
        from urllib.parse import quote_plus

        client.pre_start()
        while cls._connected:
            try:
                connection = await asyncio.get_event_loop().run_in_executor(
                    None, client.open_connection
                )
                if not connection:
                    logger.error("钉钉 Stream: open_connection 返回空")
                    await asyncio.sleep(10)
                    continue

                logger.info("钉钉 Stream endpoint: %s", connection.get('endpoint', ''))
                uri = f'{connection["endpoint"]}?ticket={quote_plus(connection["ticket"])}'

                async with asyncio.timeout(30):
                    websocket = await _ws.connect(uri)

                logger.info("钉钉 Stream WebSocket 连接成功")
                client.websocket = websocket

                asyncio.create_task(client.keepalive(websocket))
                async for raw_message in websocket:
                    json_message = _json.loads(raw_message)
                    asyncio.create_task(client.background_task(json_message))

            except asyncio.CancelledError:
                logger.info("钉钉 Stream 任务已取消")
                break
            except TimeoutError:
                logger.warning("钉钉 Stream WebSocket 连接超时（30s），将重试")
                await asyncio.sleep(5)
                continue
            except (_ws.exceptions.ConnectionClosedError, ConnectionError, OSError) as e:
                logger.warning(f"钉钉 Stream 连接断开: {e}，10s 后重连")
                await asyncio.sleep(10)
                continue
            except Exception as e:
                logger.error(f"钉钉 Stream 异常: {e}", exc_info=True)
                await asyncio.sleep(5)
                continue

        cls._connected = False

    @classmethod
    async def stop(cls):
        """停止 Stream 连接"""
        cls._connected = False
        if cls._task:
            cls._task.cancel()
            cls._task = None
        if cls._client:
            cls._client = None
        cls._started_at = None
        logger.info("钉钉 Stream 模式已停止")

    @classmethod
    def is_running(cls) -> bool:
        """
        检查 Stream 是否正在运行。
        dingtalk_stream SDK 的 start() 可能在连接建立后返回（task done），
        但连接实际仍然活跃，因此用 _connected 标志而非仅依赖 task 状态。
        """
        if cls._connected and cls._task is not None:
            if cls._task.done():
                ex = cls._task.exception() if not cls._task.cancelled() else None
                if ex or cls._task.cancelled():
                    cls._connected = False
                    return False
                return True
            return True
        return False

    @classmethod
    def status(cls) -> dict:
        """获取 Stream 模式状态"""
        return {
            "stream_mode": True,
            "running": cls.is_running(),
            "started_at": cls._started_at,
        }

    @classmethod
    def get_event_stats(cls) -> dict:
        """获取 Stream 事件统计"""
        return {
            **cls.status(),
            "total_events": cls._event_stats.get("total_events", 0),
            "last_event_type": cls._event_stats.get("last_event_type"),
            "last_event_time": cls._event_stats.get("last_event_time"),
        }
