#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
消息中心服务（异步版本）
"""
import asyncio
import logging
import re
from datetime import datetime
from typing import List, Dict, Any, Optional, Tuple

from sqlalchemy import select, update, func, and_, or_
from sqlalchemy.ext.asyncio import AsyncSession

from app.db_compat import json_contains
from core.message.model import Message, Announcement, AnnouncementRead, DingtalkTodoRecord

logger = logging.getLogger(__name__)


class MessageService:
    """消息管理服务"""

    @staticmethod
    async def get_list(
            db: AsyncSession,
            user_id: str,
            msg_type: str = None,
            status: str = None,
            page: int = 1,
            page_size: int = 20,
    ) -> Tuple[List[Message], int]:
        """获取用户消息列表（排除公告类型，公告有独立管理体系）"""
        conditions = [
            Message.recipient_id == user_id,
            Message.is_deleted == False,
            Message.msg_type != "announcement",
        ]

        if msg_type:
            conditions.append(Message.msg_type == msg_type)
        if status:
            conditions.append(Message.status == status)

        # 获取总数
        count_stmt = select(func.count(Message.id)).where(and_(*conditions))
        total_result = await db.execute(count_stmt)
        total = total_result.scalar() or 0

        # 获取列表
        offset = (page - 1) * page_size
        stmt = select(Message).where(and_(*conditions)).order_by(
            Message.sys_create_datetime.desc()
        ).offset(offset).limit(page_size)

        result = await db.execute(stmt)
        items = list(result.scalars().all())

        return items, total

    @staticmethod
    async def get_unread_count(db: AsyncSession, user_id: str) -> int:
        """获取未读消息数量（排除公告类型）"""
        stmt = select(func.count(Message.id)).where(
            Message.recipient_id == user_id,
            Message.status == "unread",
            Message.is_deleted == False,
            Message.msg_type != "announcement",
        )
        result = await db.execute(stmt)
        return result.scalar() or 0

    @staticmethod
    async def get_unread_count_by_type(db: AsyncSession, user_id: str) -> Dict[str, int]:
        """按类型获取未读消息数量（排除公告类型）"""
        stmt = select(Message.msg_type, func.count(Message.id)).where(
            Message.recipient_id == user_id,
            Message.status == "unread",
            Message.is_deleted == False,
            Message.msg_type != "announcement",
        ).group_by(Message.msg_type)

        result = await db.execute(stmt)
        return {row[0]: row[1] for row in result.all()}

    @staticmethod
    async def get_by_id(db: AsyncSession, message_id: str, user_id: str) -> Optional[Message]:
        """获取消息详情"""
        stmt = select(Message).where(
            Message.id == message_id,
            Message.recipient_id == user_id,
            Message.is_deleted == False,
        )
        result = await db.execute(stmt)
        return result.scalar_one_or_none()

    @staticmethod
    async def mark_as_read(db: AsyncSession, message_id: str, user_id: str) -> bool:
        """标记消息为已读"""
        message = await MessageService.get_by_id(db, message_id, user_id)
        if not message:
            return False

        if message.status == "unread":
            message.status = "read"
            message.read_at = datetime.now()
            await db.commit()

        return True

    @staticmethod
    async def mark_all_as_read(db: AsyncSession, user_id: str, msg_type: str = None) -> int:
        """标记所有消息为已读（排除公告类型）"""
        conditions = [
            Message.recipient_id == user_id,
            Message.status == "unread",
            Message.is_deleted == False,
            Message.msg_type != "announcement",
        ]

        if msg_type:
            conditions.append(Message.msg_type == msg_type)

        stmt = update(Message).where(and_(*conditions)).values(
            status="read",
            read_at=datetime.now(),
        )

        result = await db.execute(stmt)
        await db.commit()

        return result.rowcount

    @staticmethod
    async def delete(db: AsyncSession, message_id: str, user_id: str) -> bool:
        """删除消息（软删除）"""
        message = await MessageService.get_by_id(db, message_id, user_id)
        if not message:
            return False

        message.is_deleted = True
        await db.commit()

        return True

    @staticmethod
    async def delete_all_read(db: AsyncSession, user_id: str) -> int:
        """删除所有已读消息（排除公告类型）"""
        stmt = update(Message).where(
            Message.recipient_id == user_id,
            Message.status == "read",
            Message.is_deleted == False,
            Message.msg_type != "announcement",
        ).values(is_deleted=True)

        result = await db.execute(stmt)
        await db.commit()

        return result.rowcount

    @staticmethod
    async def create_message(
            db: AsyncSession,
            recipient_id: str,
            title: str,
            content: str,
            msg_type: str = "system",
            link_type: str = "",
            link_id: str = "",
            sender_id: str = None,
    ) -> Message:
        """创建消息"""
        message = Message(
            recipient_id=recipient_id,
            title=title,
            content=content,
            msg_type=msg_type,
            link_type=link_type,
            link_id=link_id,
            sender_id=sender_id,
        )
        db.add(message)
        await db.commit()
        await db.refresh(message)

        # WebSocket 实时推送
        await MessageService._push_ws_notification(message)

        return message

    @staticmethod
    async def batch_create_messages(
            db: AsyncSession,
            recipient_ids: List[str],
            title: str,
            content: str,
            msg_type: str = "system",
            link_type: str = "",
            link_id: str = "",
            sender_id: str = None,
    ) -> List[Message]:
        """批量创建消息"""
        messages = []
        for recipient_id in recipient_ids:
            message = Message(
                recipient_id=recipient_id,
                title=title,
                content=content,
                msg_type=msg_type,
                link_type=link_type,
                link_id=link_id,
                sender_id=sender_id,
            )
            messages.append(message)

        db.add_all(messages)
        await db.commit()

        # WebSocket 实时推送（并发）
        if messages:
            await asyncio.gather(
                *(MessageService._push_ws_notification(msg) for msg in messages),
                return_exceptions=True,
            )

        return messages

    @staticmethod
    async def _push_ws_notification(message: Message) -> None:
        """通过 WebSocket 推送消息通知给接收人（仅推送到通知组）"""
        try:
            from core.websocket.consumers.base import manager

            notification_data = {
                "type": "notification",
                "message": "新消息",
                "data": {
                    "id": str(message.id),
                    "title": message.title,
                    "content": message.content[:100] if message.content else "",
                    "msg_type": message.msg_type,
                    "link_type": message.link_type or "",
                    "link_id": message.link_id or "",
                    "sender_id": str(message.sender_id) if message.sender_id else None,
                },
            }
            # 通过用户专属通知组广播，避免向监控等其他连接发送无关消息
            await manager.broadcast_to_group(
                f"notifications_user_{message.recipient_id}",
                notification_data
            )
        except Exception as e:
            logger.warning(f"WebSocket推送失败: {e}")


class AnnouncementService:
    """公告管理服务"""

    @staticmethod
    async def get_list(
            db: AsyncSession,
            page: int = 1,
            page_size: int = 20,
            status: str = None,
            keyword: str = None,
    ) -> Tuple[List[Announcement], int]:
        """获取公告列表（管理端）"""
        conditions = [Announcement.is_deleted == False]

        if status:
            conditions.append(Announcement.status == status)
        if keyword:
            conditions.append(
                or_(
                    Announcement.title.ilike(f"%{keyword}%"),
                    Announcement.content.ilike(f"%{keyword}%"),
                )
            )

        # 获取总数
        count_stmt = select(func.count(Announcement.id)).where(and_(*conditions))
        total_result = await db.execute(count_stmt)
        total = total_result.scalar() or 0

        # 获取列表
        offset = (page - 1) * page_size
        stmt = select(Announcement).where(and_(*conditions)).order_by(
            Announcement.is_top.desc(),
            Announcement.priority.desc(),
            Announcement.publish_time.desc(),
        ).offset(offset).limit(page_size)

        result = await db.execute(stmt)
        items = list(result.scalars().all())

        return items, total

    @staticmethod
    async def get_user_announcements(
            db: AsyncSession,
            user_id: str,
            user_dept_ids: List[str] = None,
            user_role_ids: List[str] = None,
            page: int = 1,
            page_size: int = 20,
            unread_only: bool = False,
    ) -> Tuple[List[Announcement], int]:
        """获取用户可见的公告列表"""
        now = datetime.now()

        # 基础查询：已发布且未过期
        conditions = [
            Announcement.status == "published",
            Announcement.is_deleted == False,
            or_(Announcement.expire_time.is_(None), Announcement.expire_time > now),
        ]

        # 过滤接收范围 - 使用跨数据库兼容的 json_contains
        target_conditions = [Announcement.target_type == "all"]

        if user_dept_ids:
            for dept_id in user_dept_ids:
                target_conditions.append(
                    and_(
                        Announcement.target_type == "dept",
                        json_contains(Announcement.target_ids, dept_id)
                    )
                )

        if user_role_ids:
            for role_id in user_role_ids:
                target_conditions.append(
                    and_(
                        Announcement.target_type == "role",
                        json_contains(Announcement.target_ids, role_id)
                    )
                )

        target_conditions.append(
            and_(
                Announcement.target_type == "user",
                json_contains(Announcement.target_ids, user_id)
            )
        )

        conditions.append(or_(*target_conditions))

        # 只看未读
        if unread_only:
            read_stmt = select(AnnouncementRead.announcement_id).where(
                AnnouncementRead.user_id == user_id
            )
            read_result = await db.execute(read_stmt)
            read_ids = [row[0] for row in read_result.all()]
            if read_ids:
                conditions.append(Announcement.id.notin_(read_ids))

        # 获取总数
        count_stmt = select(func.count(Announcement.id)).where(and_(*conditions))
        total_result = await db.execute(count_stmt)
        total = total_result.scalar() or 0

        # 获取列表
        offset = (page - 1) * page_size
        stmt = select(Announcement).where(and_(*conditions)).order_by(
            Announcement.is_top.desc(),
            Announcement.priority.desc(),
            Announcement.publish_time.desc(),
        ).offset(offset).limit(page_size)

        result = await db.execute(stmt)
        items = list(result.scalars().all())

        # 标记已读状态
        if items:
            read_stmt = select(AnnouncementRead.announcement_id).where(
                AnnouncementRead.user_id == user_id,
                AnnouncementRead.announcement_id.in_([a.id for a in items])
            )
            read_result = await db.execute(read_stmt)
            read_ids = set(row[0] for row in read_result.all())

            for item in items:
                item.is_read = item.id in read_ids

        return items, total

    @staticmethod
    async def get_by_id(db: AsyncSession, announcement_id: str) -> Optional[Announcement]:
        """获取公告详情"""
        stmt = select(Announcement).where(
            Announcement.id == announcement_id,
            Announcement.is_deleted == False,
        )
        result = await db.execute(stmt)
        return result.scalar_one_or_none()

    @staticmethod
    async def create(
            db: AsyncSession,
            data: Dict[str, Any],
            user_id: str,
    ) -> Announcement:
        """创建公告"""
        announcement = Announcement(
            title=data["title"],
            content=data["content"],
            summary=data.get("summary", ""),
            status=data.get("status", "draft"),
            priority=data.get("priority", 0),
            is_top=data.get("is_top", False),
            target_type=data.get("target_type", "all"),
            target_ids=data.get("target_ids", []),
            publish_time=data.get("publish_time"),
            expire_time=data.get("expire_time"),
            publisher_id=user_id,
            sys_creator_id=user_id,
            sys_modifier_id=user_id,
        )
        db.add(announcement)
        await db.commit()
        await db.refresh(announcement)

        return announcement

    @staticmethod
    async def update(
            db: AsyncSession,
            announcement_id: str,
            data: Dict[str, Any],
            user_id: str,
    ) -> Optional[Announcement]:
        """更新公告"""
        announcement = await AnnouncementService.get_by_id(db, announcement_id)
        if not announcement:
            return None

        # 更新字段
        for field in ["title", "content", "summary", "status", "priority",
                      "is_top", "target_type", "target_ids", "publish_time", "expire_time"]:
            if field in data and data[field] is not None:
                setattr(announcement, field, data[field])

        announcement.sys_modifier_id = user_id
        await db.commit()
        await db.refresh(announcement)

        return announcement

    @staticmethod
    async def delete(db: AsyncSession, announcement_id: str, user_id: str) -> bool:
        """删除公告（软删除）"""
        announcement = await AnnouncementService.get_by_id(db, announcement_id)
        if not announcement:
            return False

        announcement.is_deleted = True
        announcement.sys_modifier_id = user_id
        await db.commit()

        return True

    @staticmethod
    async def publish(
            db: AsyncSession,
            announcement_id: str,
            user_id: str,
    ) -> Optional[Announcement]:
        """发布公告"""
        announcement = await AnnouncementService.get_by_id(db, announcement_id)
        if not announcement:
            return None

        if announcement.status != "draft":
            raise ValueError("只能发布草稿状态的公告")

        announcement.status = "published"
        announcement.publish_time = datetime.now()
        announcement.publisher_id = user_id
        announcement.sys_modifier_id = user_id
        await db.commit()
        await db.refresh(announcement)

        # WebSocket 实时推送公告通知给所有在线用户
        await AnnouncementService._push_announcement_notification(announcement)

        return announcement

    @staticmethod
    async def _push_announcement_notification(announcement: Announcement) -> None:
        """通过 WebSocket 推送公告通知给所有在线用户"""
        try:
            from core.websocket.consumers.base import manager

            notification_data = {
                "type": "announcement",
                "message": "新公告",
                "data": {
                    "id": str(announcement.id),
                    "title": announcement.title,
                    "summary": announcement.summary or "",
                    "priority": announcement.priority,
                    "is_top": announcement.is_top,
                },
            }
            # 向所有在线用户的通知组广播
            for group_name in list(manager.groups.keys()):
                if group_name.startswith("notifications_user_"):
                    await manager.broadcast_to_group(group_name, notification_data)
        except Exception as e:
            logger.warning(f"WebSocket推送公告失败: {e}")

    @staticmethod
    async def mark_as_read(
            db: AsyncSession,
            announcement_id: str,
            user_id: str,
    ) -> bool:
        """标记公告为已读"""
        announcement = await AnnouncementService.get_by_id(db, announcement_id)
        if not announcement:
            return False

        # 检查是否已读
        stmt = select(AnnouncementRead).where(
            AnnouncementRead.announcement_id == announcement_id,
            AnnouncementRead.user_id == user_id,
        )
        result = await db.execute(stmt)
        existing = result.scalar_one_or_none()

        if not existing:
            # 创建阅读记录
            read_record = AnnouncementRead(
                announcement_id=announcement_id,
                user_id=user_id,
                read_at=datetime.now(),
                sys_creator_id=user_id,
                sys_modifier_id=user_id,
            )
            db.add(read_record)

            # 更新阅读计数
            announcement.read_count = (announcement.read_count or 0) + 1

            await db.commit()

        return True

    @staticmethod
    async def get_unread_count(
            db: AsyncSession,
            user_id: str,
            user_dept_ids: List[str] = None,
            user_role_ids: List[str] = None,
    ) -> int:
        """获取未读公告数量"""
        now = datetime.now()

        # 基础查询
        conditions = [
            Announcement.status == "published",
            Announcement.is_deleted == False,
            or_(Announcement.expire_time.is_(None), Announcement.expire_time > now),
        ]

        # 过滤接收范围 - 使用跨数据库兼容的 json_contains
        target_conditions = [Announcement.target_type == "all"]

        if user_dept_ids:
            for dept_id in user_dept_ids:
                target_conditions.append(
                    and_(
                        Announcement.target_type == "dept",
                        json_contains(Announcement.target_ids, dept_id)
                    )
                )

        if user_role_ids:
            for role_id in user_role_ids:
                target_conditions.append(
                    and_(
                        Announcement.target_type == "role",
                        json_contains(Announcement.target_ids, role_id)
                    )
                )

        target_conditions.append(
            and_(
                Announcement.target_type == "user",
                json_contains(Announcement.target_ids, user_id)
            )
        )

        conditions.append(or_(*target_conditions))

        # 排除已读
        read_stmt = select(AnnouncementRead.announcement_id).where(
            AnnouncementRead.user_id == user_id
        )
        read_result = await db.execute(read_stmt)
        read_ids = [row[0] for row in read_result.all()]
        if read_ids:
            conditions.append(Announcement.id.notin_(read_ids))

        # 获取数量
        count_stmt = select(func.count(Announcement.id)).where(and_(*conditions))
        result = await db.execute(count_stmt)
        return result.scalar() or 0

    @staticmethod
    async def get_read_stats(db: AsyncSession, announcement_id: str) -> Optional[Dict[str, Any]]:
        """获取公告阅读统计"""
        announcement = await AnnouncementService.get_by_id(db, announcement_id)
        if not announcement:
            return None

        # 获取阅读记录
        stmt = select(AnnouncementRead).where(
            AnnouncementRead.announcement_id == announcement_id
        ).order_by(AnnouncementRead.read_at.desc()).limit(100)

        result = await db.execute(stmt)
        reads = result.scalars().all()

        # 获取用户信息
        from core.user.model import User
        user_ids = [r.user_id for r in reads]
        if user_ids:
            user_stmt = select(User).where(User.id.in_(user_ids))
            user_result = await db.execute(user_stmt)
            users = {u.id: u for u in user_result.scalars().all()}
        else:
            users = {}

        return {
            "total_read": len(reads),
            "readers": [
                {
                    "user_id": r.user_id,
                    "user_name": users.get(r.user_id).name if users.get(r.user_id) else "",
                    "read_at": r.read_at.isoformat() if r.read_at else None,
                }
                for r in reads
            ]
        }


class NotifyService:
    """通知服务"""

    @staticmethod
    def parse_template(template: str, context: Dict[str, Any]) -> str:
        """解析模板变量"""
        if not template or not context:
            return template

        def replace_var(match):
            var_path = match.group(1)
            parts = var_path.split(".")
            value = context

            for part in parts:
                if isinstance(value, dict):
                    value = value.get(part, "")
                else:
                    value = ""
                    break

            return str(value) if value else ""

        return re.sub(r"\$\{([^}]+)\}", replace_var, template)

    @staticmethod
    async def send(
            db: AsyncSession,
            recipient_ids: List[str],
            title: str,
            content: str,
            channels: List[str] = None,
            msg_type: str = "system",
            context: Dict[str, Any] = None,
            link_type: str = "",
            link_id: str = "",
            sender_id: str = None,
    ) -> Dict[str, bool]:
        """发送通知"""
        if channels is None:
            channels = ["site"]

        # 解析模板变量
        if context:
            title = NotifyService.parse_template(title, context)
            content = NotifyService.parse_template(content, context)

        results = {}

        for channel in channels:
            try:
                if channel == "site":
                    await MessageService.batch_create_messages(
                        db=db,
                        recipient_ids=recipient_ids,
                        title=title,
                        content=content,
                        msg_type=msg_type,
                        link_type=link_type,
                        link_id=link_id,
                        sender_id=sender_id,
                    )
                    results["site"] = True
                    logger.info(f"站内消息发送成功: {len(recipient_ids)} 条")
                elif channel == "email":
                    results["email"] = await NotifyService._send_email(
                        db=db,
                        recipient_ids=recipient_ids,
                        title=title,
                        content=content,
                    )
                elif channel == "dingtalk":
                    results["dingtalk"] = await NotifyService._send_dingtalk(
                        db=db,
                        recipient_ids=recipient_ids,
                        title=title,
                        content=content,
                    )
                elif channel == "feishu":
                    results["feishu"] = await NotifyService._send_feishu(
                        db=db,
                        recipient_ids=recipient_ids,
                        title=title,
                        content=content,
                    )
                elif channel == "wechat":
                    results["wechat"] = await NotifyService._send_wecom(
                        db=db,
                        recipient_ids=recipient_ids,
                        title=title,
                        content=content,
                    )
                elif channel == "wechat_mp":
                    results["wechat_mp"] = await NotifyService._send_wechat_mp(
                        db=db,
                        recipient_ids=recipient_ids,
                        title=title,
                        content=content,
                    )
                elif channel == "sms":
                    results["sms"] = await NotifyService._send_sms(
                        db=db,
                        recipient_ids=recipient_ids,
                        title=title,
                        content=content,
                    )
                elif channel == "dingtalk_todo":
                    results["dingtalk_todo"] = await NotifyService._send_dingtalk_todo(
                        db=db,
                        recipient_ids=recipient_ids,
                        title=title,
                        content=content,
                        link_type=link_type,
                        link_id=link_id,
                    )
                elif channel == "chat":
                    results["chat"] = await NotifyService._send_chat(
                        db=db,
                        recipient_ids=recipient_ids,
                        title=title,
                        content=content,
                        link_type=link_type,
                        link_id=link_id,
                    )
                else:
                    logger.warning(f"未实现的通知渠道: {channel}")
                    results[channel] = False
            except Exception as e:
                logger.error(f"发送通知失败 [{channel}]: {e}")
                results[channel] = False

        return results

    @staticmethod
    async def _send_email(
            db: AsyncSession,
            recipient_ids: List[str],
            title: str,
            content: str,
    ) -> bool:
        """通过邮件渠道发送通知"""
        from app.email import EmailSender

        if not await EmailSender.is_configured_async():
            logger.warning("SMTP 未配置，跳过邮件发送")
            return False

        # 查询收件人邮箱
        from core.user.model import User
        stmt = select(User.id, User.email).where(
            User.id.in_(recipient_ids),
            User.email.isnot(None),
            User.email != "",
        )
        result = await db.execute(stmt)
        user_emails = {str(row[0]): row[1] for row in result.all()}

        if not user_emails:
            logger.info("没有收件人配置了邮箱，跳过邮件发送")
            return True

        # 构建 HTML 邮件
        html_content = EmailSender.build_notification_html(title, content)

        # 批量发送
        email_list = list(user_emails.values())
        send_results = await EmailSender.send_batch(email_list, title, html_content, html=True)

        success_count = sum(1 for v in send_results.values() if v)
        fail_count = len(send_results) - success_count
        logger.info(f"邮件发送完成: 成功 {success_count}, 失败 {fail_count}")

        return fail_count == 0

    @staticmethod
    async def _send_dingtalk(
            db: AsyncSession,
            recipient_ids: List[str],
            title: str,
            content: str,
    ) -> bool:
        """通过钉钉渠道发送通知"""
        from app.dingtalk import DingTalkWebhook, DingTalkWorkNotice, build_dingtalk_markdown

        markdown_text = build_dingtalk_markdown(title, content)
        sent = False

        # 优先使用工作通知（精准推送到个人）
        if await DingTalkWorkNotice.is_configured_async():
            from core.user.model import User
            stmt = select(User.dingtalk_unionid).where(
                User.id.in_(recipient_ids),
                User.dingtalk_unionid.isnot(None),
                User.dingtalk_unionid != "",
            )
            result = await db.execute(stmt)
            union_ids = [row[0] for row in result.all()]

            if union_ids:
                # 钉钉工作通知 API 的 userid_list 需要钉钉 userId
                # unionId 转 userId 需要额外 API 调用
                user_ids = await NotifyService._convert_dingtalk_unionids(union_ids)
                if user_ids:
                    success = await DingTalkWorkNotice.send_markdown(
                        userid_list=user_ids,
                        title=title,
                        text=markdown_text,
                    )
                    if success:
                        sent = True
                        logger.info(f"钉钉工作通知发送成功: {len(user_ids)} 人")

        # 回退到 Webhook 群通知
        if not sent and await DingTalkWebhook.is_configured_async():
            # 查询收件人手机号用于 @
            from core.user.model import User
            stmt = select(User.mobile).where(
                User.id.in_(recipient_ids),
                User.mobile.isnot(None),
                User.mobile != "",
            )
            result = await db.execute(stmt)
            mobiles = [row[0] for row in result.all()]

            success = await DingTalkWebhook.send_markdown(
                title=title,
                text=markdown_text,
                at_mobiles=mobiles,
            )
            if success:
                sent = True

        if not sent:
            logger.warning("钉钉通知未配置或发送失败")
            return False

        return True

    @staticmethod
    async def _convert_dingtalk_unionids(union_ids: List[str]) -> List[str]:
        """将钉钉 unionId 转换为 userId"""
        from app.dingtalk import DingTalkWorkNotice

        token = await DingTalkWorkNotice._get_access_token()
        if not token:
            return []

        user_ids = []
        try:
            import httpx
            async with httpx.AsyncClient(timeout=10.0) as client:
                for union_id in union_ids:
                    try:
                        resp = await client.post(
                            "https://oapi.dingtalk.com/topapi/user/getbyunionid",
                            params={"access_token": token},
                            json={"unionid": union_id},
                        )
                        resp.raise_for_status()
                        result = resp.json()
                        if result.get("errcode") == 0:
                            uid = result.get("result", {}).get("userid")
                            if uid:
                                user_ids.append(uid)
                    except Exception as e:
                        logger.warning(f"钉钉 unionId 转 userId 失败 [{union_id}]: {e}")
        except Exception as e:
            logger.error(f"钉钉 unionId 批量转换异常: {e}")

        return user_ids

    @staticmethod
    async def _send_dingtalk_todo(
            db: AsyncSession,
            recipient_ids: List[str],
            title: str,
            content: str,
            link_type: str = "",
            link_id: str = "",
    ) -> bool:
        """通过钉钉待办渠道创建待办任务"""
        from app.dingtalk import DingTalkTodo

        if not await DingTalkTodo.is_configured_async():
            logger.warning("钉钉待办未配置（需要 OAuth app_id/app_secret），跳过")
            return False

        from core.user.model import User
        stmt = select(User.id, User.dingtalk_unionid).where(
            User.id.in_(recipient_ids),
            User.dingtalk_unionid.isnot(None),
            User.dingtalk_unionid != "",
        )
        result = await db.execute(stmt)
        user_union_map = {str(row[0]): row[1] for row in result.all()}

        if not user_union_map:
            logger.info("没有收件人绑定了钉钉账号，跳过钉钉待办")
            return True

        success_count = 0
        source_id_prefix = f"{link_type}:{link_id}" if link_type and link_id else ""

        for user_id, union_id in user_union_map.items():
            source_id = f"{source_id_prefix}:{user_id}" if source_id_prefix else None
            task_id = await DingTalkTodo.create_todo(
                union_id=union_id,
                subject=title[:1024],
                description=content[:4096] if content else "",
                executor_ids=[union_id],
                priority=20,
                source_id=source_id,
                link_type=link_type,
                link_id=link_id,
            )
            if task_id:
                record = DingtalkTodoRecord(
                    source_type=link_type or "message",
                    source_id=link_id or "",
                    user_id=user_id,
                    dingtalk_union_id=union_id,
                    dingtalk_task_id=task_id,
                    status="pending",
                )
                db.add(record)
                success_count += 1

        if success_count > 0:
            await db.commit()

        logger.info(f"钉钉待办创建完成: 成功 {success_count}/{len(user_union_map)}")
        return success_count > 0

    @staticmethod
    async def complete_dingtalk_todo(
            db: AsyncSession,
            source_type: str,
            source_id: str,
    ) -> int:
        """完成指定业务来源的所有钉钉待办"""
        from app.dingtalk import DingTalkTodo

        stmt = select(DingtalkTodoRecord).where(
            DingtalkTodoRecord.source_type == source_type,
            DingtalkTodoRecord.source_id == source_id,
            DingtalkTodoRecord.status == "pending",
            DingtalkTodoRecord.is_deleted == False,  # noqa: E712
        )
        result = await db.execute(stmt)
        records = result.scalars().all()

        completed = 0
        for record in records:
            success = await DingTalkTodo.complete_todo(
                union_id=record.dingtalk_union_id,
                task_id=record.dingtalk_task_id,
            )
            if success:
                record.status = "done"
                completed += 1

        if completed > 0:
            await db.commit()

        logger.info(f"钉钉待办完成: {completed}/{len(records)}, source={source_type}:{source_id}")
        return completed

    @staticmethod
    async def delete_dingtalk_todo(
            db: AsyncSession,
            source_type: str,
            source_id: str,
    ) -> int:
        """删除指定业务来源的所有钉钉待办"""
        from app.dingtalk import DingTalkTodo

        stmt = select(DingtalkTodoRecord).where(
            DingtalkTodoRecord.source_type == source_type,
            DingtalkTodoRecord.source_id == source_id,
            DingtalkTodoRecord.status == "pending",
            DingtalkTodoRecord.is_deleted == False,  # noqa: E712
        )
        result = await db.execute(stmt)
        records = result.scalars().all()

        deleted = 0
        for record in records:
            success = await DingTalkTodo.delete_todo(
                union_id=record.dingtalk_union_id,
                task_id=record.dingtalk_task_id,
            )
            if success:
                record.status = "deleted"
                deleted += 1

        if deleted > 0:
            await db.commit()

        logger.info(f"钉钉待办删除: {deleted}/{len(records)}, source={source_type}:{source_id}")
        return deleted

    @staticmethod
    async def _send_feishu(
            db: AsyncSession,
            recipient_ids: List[str],
            title: str,
            content: str,
    ) -> bool:
        """通过飞书渠道发送通知"""
        from app.feishu import FeishuWebhook, FeishuAppMessage, build_feishu_notification_text

        notification_text = build_feishu_notification_text(title, content)
        sent = False

        # 优先使用应用消息（精准推送到个人）
        if await FeishuAppMessage.is_configured_async():
            from core.user.model import User
            stmt = select(User.feishu_union_id).where(
                User.id.in_(recipient_ids),
                User.feishu_union_id.isnot(None),
                User.feishu_union_id != "",
            )
            result = await db.execute(stmt)
            union_ids = [row[0] for row in result.all()]

            if union_ids:
                # union_id 转 open_id
                id_map = await FeishuAppMessage.get_open_ids_by_union_ids(union_ids)
                open_ids = list(id_map.values())

                if open_ids:
                    success = await FeishuAppMessage.send_batch_text(
                        open_ids=open_ids,
                        text=notification_text,
                    )
                    if success:
                        sent = True
                        logger.info(f"飞书应用消息发送成功: {len(open_ids)} 人")

        # 回退到 Webhook 群通知
        if not sent and await FeishuWebhook.is_configured_async():
            success = await FeishuWebhook.send_interactive(
                title=title,
                content=content,
            )
            if success:
                sent = True

        if not sent:
            logger.warning("飞书通知未配置或发送失败")
            return False

        return True

    @staticmethod
    async def _send_wecom(
            db: AsyncSession,
            recipient_ids: List[str],
            title: str,
            content: str,
    ) -> bool:
        """通过企业微信渠道发送通知"""
        from app.wecom import WecomWebhook, WecomAppMessage, build_wecom_markdown

        markdown_text = build_wecom_markdown(title, content)
        sent = False

        # 优先使用应用消息（精准推送到个人）
        if await WecomAppMessage.is_configured_async():
            from core.user.model import User
            stmt = select(User.wecom_userid).where(
                User.id.in_(recipient_ids),
                User.wecom_userid.isnot(None),
                User.wecom_userid != "",
            )
            result = await db.execute(stmt)
            wecom_userids = [row[0] for row in result.all()]

            if wecom_userids:
                success = await WecomAppMessage.send_markdown(
                    userid_list=wecom_userids,
                    content=markdown_text,
                )
                if success:
                    sent = True
                    logger.info(f"企业微信应用消息发送成功: {len(wecom_userids)} 人")

        # 回退到 Webhook 群通知
        if not sent and await WecomWebhook.is_configured_async():
            success = await WecomWebhook.send_markdown(content=markdown_text)
            if success:
                sent = True

        if not sent:
            logger.warning("企业微信通知未配置或发送失败")
            return False

        return True

    @staticmethod
    async def _send_wechat_mp(
            db: AsyncSession,
            recipient_ids: List[str],
            title: str,
            content: str,
    ) -> bool:
        """通过微信公众号模板消息渠道发送通知"""
        from app.wechat import WechatMPMessage

        if not await WechatMPMessage.is_configured_async():
            logger.warning("微信公众号模板消息未配置，跳过发送")
            return False

        # 查询用户的微信 openid
        from core.user.model import User
        stmt = select(User.wechat_openid).where(
            User.id.in_(recipient_ids),
            User.wechat_openid.isnot(None),
            User.wechat_openid != "",
        )
        result = await db.execute(stmt)
        openids = [row[0] for row in result.all()]

        if not openids:
            logger.warning("没有用户绑定微信公众号，跳过发送")
            return False

        success_count = await WechatMPMessage.batch_send_notification(
            openids=openids,
            title=title,
            content=content,
        )
        logger.info(f"微信公众号模板消息发送完成: {success_count}/{len(openids)} 成功")
        return success_count > 0

    @staticmethod
    async def _send_sms(
            db: AsyncSession,
            recipient_ids: List[str],
            title: str,
            content: str,
    ) -> bool:
        """通过短信渠道发送通知"""
        from app.sms import SMSSender

        if not await SMSSender.is_configured_async():
            logger.warning("短信服务未配置，跳过发送")
            return False

        # 查询收件人手机号
        from core.user.model import User
        stmt = select(User.id, User.mobile).where(
            User.id.in_(recipient_ids),
            User.mobile.isnot(None),
            User.mobile != "",
        )
        result = await db.execute(stmt)
        user_mobiles = {str(row[0]): row[1] for row in result.all()}

        if not user_mobiles:
            logger.info("没有收件人配置了手机号，跳过短信发送")
            return True

        phone_list = list(user_mobiles.values())
        send_results = await SMSSender.send(
            phone_numbers=phone_list,
            title=title,
            content=content,
        )

        success_count = sum(1 for v in send_results.values() if v)
        fail_count = len(send_results) - success_count
        logger.info(f"短信发送完成: 成功 {success_count}, 失败 {fail_count}")

        return fail_count == 0

    @staticmethod
    async def _send_chat(
            db: AsyncSession,
            recipient_ids: List[str],
            title: str,
            content: str,
            link_type: str = "",
            link_id: str = "",
    ) -> bool:
        """通过聊天渠道发送通知（以系统通知用户身份发送单聊消息）"""
        from core.chat.service import ChatMessageService

        success_count = 0

        for recipient_id in recipient_ids:
            try:
                msg = await ChatMessageService.send_system_notification(
                    db=db,
                    recipient_id=recipient_id,
                    title=title,
                    content=content,
                    link_type=link_type,
                    link_id=link_id,
                )
                if msg:
                    success_count += 1
            except Exception as e:
                logger.error(f"聊天通知发送失败: recipient={recipient_id}, {e}")

        logger.info(f"聊天通知发送完成: 成功 {success_count}/{len(recipient_ids)}")
        return success_count > 0
