"""
钉钉通知工具

支持三种通知方式：
1. Webhook 群机器人 - 向钉钉群聊发送消息
2. 工作通知 - 通过企业内部应用向个人发送工作通知
3. 待办任务 - 通过钉钉待办 API 创建/管理第三方待办
"""
import base64
import hashlib
import hmac
import logging
import time
import urllib.parse
from typing import Any, Dict, List, Optional

import httpx

from app.config import settings

logger = logging.getLogger(__name__)


class DingTalkWebhook:
    """钉钉群机器人 Webhook"""

    @staticmethod
    async def _get_config() -> dict:
        """通过 config_manager 三级获取钉钉 Webhook 配置"""
        from app.config_manager import config_manager
        group_config = await config_manager.get_group("notify_dingtalk")
        return {
            "webhook_url": group_config.get("webhook_url") or None,
            "webhook_secret": group_config.get("webhook_secret") or None,
        }

    @staticmethod
    def is_configured() -> bool:
        """检查 Webhook 是否已配置（仅检查 env，同步兼容）"""
        return bool(settings.DINGTALK_WEBHOOK_URL)

    @staticmethod
    async def is_configured_async() -> bool:
        """检查 Webhook 是否已配置（通过 config_manager 三级获取）"""
        config = await DingTalkWebhook._get_config()
        return bool(config["webhook_url"])

    @staticmethod
    def _sign_with_secret(secret: str) -> Dict[str, str]:
        """使用指定 secret 生成签名参数"""
        if not secret:
            return {}

        timestamp = str(round(time.time() * 1000))
        string_to_sign = f"{timestamp}\n{secret}"
        hmac_code = hmac.new(
            secret.encode("utf-8"),
            string_to_sign.encode("utf-8"),
            digestmod=hashlib.sha256,
        ).digest()
        sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
        return {"timestamp": timestamp, "sign": sign}

    @staticmethod
    async def send_text(content: str, at_mobiles: List[str] = None, at_all: bool = False) -> bool:
        """
        发送文本消息

        Args:
            content: 消息内容
            at_mobiles: @指定手机号列表
            at_all: 是否@所有人
        """
        config = await DingTalkWebhook._get_config()
        if not config["webhook_url"]:
            logger.warning("钉钉 Webhook 未配置，跳过发送")
            return False

        payload: Dict[str, Any] = {
            "msgtype": "text",
            "text": {"content": content},
            "at": {
                "atMobiles": at_mobiles or [],
                "isAtAll": at_all,
            },
        }
        return await DingTalkWebhook._post(payload, config)

    @staticmethod
    async def send_markdown(title: str, text: str, at_mobiles: List[str] = None, at_all: bool = False) -> bool:
        """
        发送 Markdown 消息

        Args:
            title: 消息标题（会话列表中展示）
            text: Markdown 格式内容
            at_mobiles: @指定手机号列表
            at_all: 是否@所有人
        """
        config = await DingTalkWebhook._get_config()
        if not config["webhook_url"]:
            logger.warning("钉钉 Webhook 未配置，跳过发送")
            return False

        payload: Dict[str, Any] = {
            "msgtype": "markdown",
            "markdown": {"title": title, "text": text},
            "at": {
                "atMobiles": at_mobiles or [],
                "isAtAll": at_all,
            },
        }
        return await DingTalkWebhook._post(payload, config)

    @staticmethod
    async def send_action_card(title: str, text: str, single_url: str = "", single_title: str = "查看详情") -> bool:
        """
        发送 ActionCard 消息

        Args:
            title: 消息标题
            text: Markdown 格式内容
            single_url: 跳转链接
            single_title: 按钮文字
        """
        config = await DingTalkWebhook._get_config()
        if not config["webhook_url"]:
            logger.warning("钉钉 Webhook 未配置，跳过发送")
            return False

        payload: Dict[str, Any] = {
            "msgtype": "actionCard",
            "actionCard": {
                "title": title,
                "text": text,
                "singleTitle": single_title,
                "singleURL": single_url,
            },
        }
        return await DingTalkWebhook._post(payload, config)

    @staticmethod
    async def _post(payload: Dict[str, Any], config: dict = None) -> bool:
        """发送请求到钉钉 Webhook"""
        try:
            if config is None:
                config = await DingTalkWebhook._get_config()
            url = config["webhook_url"]
            sign_params = DingTalkWebhook._sign_with_secret(config.get("webhook_secret") or "")
            if sign_params:
                separator = "&" if "?" in url else "?"
                url = f"{url}{separator}timestamp={sign_params['timestamp']}&sign={sign_params['sign']}"

            async with httpx.AsyncClient(timeout=10.0) as client:
                resp = await client.post(url, json=payload)
                resp.raise_for_status()
                result = resp.json()

                if result.get("errcode") == 0:
                    logger.info("钉钉 Webhook 消息发送成功")
                    return True
                else:
                    logger.error(f"钉钉 Webhook 发送失败: {result}")
                    return False
        except Exception as e:
            logger.error(f"钉钉 Webhook 请求异常: {e}")
            return False


class DingTalkWorkNotice:
    """钉钉工作通知（企业内部应用）"""

    TOKEN_URL = "https://oapi.dingtalk.com/gettoken"
    SEND_URL = "https://oapi.dingtalk.com/topapi/message/corpconversation/asyncsend_v2"

    _access_token: Optional[str] = None
    _token_expires_at: float = 0

    @classmethod
    async def _get_config(cls) -> dict:
        """通过 config_manager 三级获取钉钉工作通知配置"""
        from app.config_manager import config_manager
        group_config = await config_manager.get_group("notify_dingtalk")
        return {
            "agent_id": group_config.get("agent_id") or None,
            "corp_id": group_config.get("corp_id") or None,
            "app_id": group_config.get("webhook_url") and None,  # webhook_url 不用于工作通知
        }

    @classmethod
    def is_configured(cls) -> bool:
        """检查工作通知是否已配置（仅检查 env，同步兼容）"""
        return bool(
            settings.DINGTALK_APP_ID
            and settings.DINGTALK_APP_SECRET
            and settings.DINGTALK_AGENT_ID
        )

    @classmethod
    async def is_configured_async(cls) -> bool:
        """检查工作通知是否已配置（通过 config_manager 三级获取）"""
        from app.config_manager import config_manager
        group_config = await config_manager.get_group("notify_dingtalk")
        # 工作通知需要 agent_id + OAuth 配置中的 app_id/app_secret
        agent_id = group_config.get("agent_id") or None
        # app_id/app_secret 来自 oauth_dingtalk 分组
        oauth_config = await config_manager.get_group("oauth_dingtalk")
        app_id = oauth_config.get("app_id") or None
        app_secret = oauth_config.get("app_secret") or None
        return bool(app_id and app_secret and agent_id)

    @classmethod
    async def _get_access_token(cls) -> Optional[str]:
        """获取企业内部应用的 access_token（带缓存）"""
        now = time.time()
        if cls._access_token and now < cls._token_expires_at:
            return cls._access_token

        from app.config_manager import config_manager
        oauth_config = await config_manager.get_group("oauth_dingtalk")
        app_id = oauth_config.get("app_id")
        app_secret = oauth_config.get("app_secret")
        if not (app_id and app_secret):
            logger.warning("钉钉 app_id/app_secret 未配置")
            return None

        try:
            async with httpx.AsyncClient(timeout=10.0) as client:
                resp = await client.get(
                    cls.TOKEN_URL,
                    params={
                        "appkey": app_id,
                        "appsecret": app_secret,
                    },
                )
                resp.raise_for_status()
                result = resp.json()

                if result.get("errcode") == 0:
                    cls._access_token = result["access_token"]
                    cls._token_expires_at = now + result.get("expires_in", 7200) - 300
                    logger.info("钉钉 access_token 获取成功")
                    return cls._access_token
                else:
                    logger.error(f"获取钉钉 access_token 失败: {result}")
                    return None
        except Exception as e:
            logger.error(f"获取钉钉 access_token 异常: {e}")
            return None

    @classmethod
    async def send_text(cls, userid_list: List[str], content: str) -> bool:
        """
        发送文本工作通知

        Args:
            userid_list: 钉钉用户 userId 列表（最多100个）
            content: 消息内容
        """
        return await cls._send(userid_list, {"msgtype": "text", "text": {"content": content}})

    @classmethod
    async def send_markdown(cls, userid_list: List[str], title: str, text: str) -> bool:
        """
        发送 Markdown 工作通知

        Args:
            userid_list: 钉钉用户 userId 列表
            title: 消息标题
            text: Markdown 格式内容
        """
        return await cls._send(userid_list, {"msgtype": "markdown", "markdown": {"title": title, "text": text}})

    @classmethod
    async def send_action_card(
            cls,
            userid_list: List[str],
            title: str,
            markdown: str,
            single_url: str = "",
            single_title: str = "查看详情",
    ) -> bool:
        """
        发送 ActionCard 工作通知

        Args:
            userid_list: 钉钉用户 userId 列表
            title: 消息标题
            markdown: Markdown 格式内容
            single_url: 跳转链接
            single_title: 按钮文字
        """
        return await cls._send(userid_list, {
            "msgtype": "action_card",
            "action_card": {
                "title": title,
                "markdown": markdown,
                "single_title": single_title,
                "single_url": single_url,
            },
        })

    @classmethod
    async def _send(cls, userid_list: List[str], msg: Dict[str, Any]) -> bool:
        """发送工作通知"""
        if not await cls.is_configured_async():
            logger.warning("钉钉工作通知未配置，跳过发送")
            return False

        token = await cls._get_access_token()
        if not token:
            return False

        from app.config_manager import config_manager
        group_config = await config_manager.get_group("notify_dingtalk")
        agent_id = group_config.get("agent_id") or settings.DINGTALK_AGENT_ID

        try:
            payload = {
                "agent_id": agent_id,
                "userid_list": ",".join(userid_list[:100]),
                "msg": msg,
            }

            async with httpx.AsyncClient(timeout=10.0) as client:
                resp = await client.post(
                    cls.SEND_URL,
                    params={"access_token": token},
                    json=payload,
                )
                resp.raise_for_status()
                result = resp.json()

                if result.get("errcode") == 0:
                    logger.info(f"钉钉工作通知发送成功: {len(userid_list)} 人")
                    return True
                else:
                    logger.error(f"钉钉工作通知发送失败: {result}")
                    return False
        except Exception as e:
            logger.error(f"钉钉工作通知请求异常: {e}")
            return False


class DingTalkTodo:
    """钉钉待办任务（v1.0 API）"""

    BASE_URL = "https://api.dingtalk.com/v1.0/todo/users"

    @classmethod
    async def is_configured_async(cls) -> bool:
        """检查待办功能是否已配置（复用 OAuth 配置中的 app_id/app_secret）"""
        from app.config_manager import config_manager
        oauth_config = await config_manager.get_group("oauth_dingtalk")
        app_id = oauth_config.get("app_id") or None
        app_secret = oauth_config.get("app_secret") or None
        return bool(app_id and app_secret)

    @classmethod
    async def _get_headers(cls) -> Optional[Dict[str, str]]:
        """获取请求头（含 access_token）"""
        token = await DingTalkWorkNotice._get_access_token()
        if not token:
            return None
        return {
            "x-acs-dingtalk-access-token": token,
            "Content-Type": "application/json",
        }

    @classmethod
    async def _get_detail_urls(cls, link_type: str, link_id: str) -> Dict[str, str]:
        """根据业务类型和ID拼接待办详情页跳转URL"""
        from app.config_manager import config_manager
        notify_config = await config_manager.get_group("notify_dingtalk")
        pc_base = (notify_config.get("todo_pc_url") or "").rstrip("/")
        app_base = (notify_config.get("todo_app_url") or "").rstrip("/")

        pc_url = ""
        app_url = ""

        if link_type == "workflow_task" and link_id:
            if pc_base:
                pc_url = f"{pc_base}/ai-platform/workflow-runs?id={link_id}"
            if app_base:
                app_url = f"{app_base}/pages-workflow/task-detail?taskId={link_id}"
        elif link_type == "workflow_instance" and link_id:
            if pc_base:
                pc_url = f"{pc_base}/ai-platform/workflow-runs?id={link_id}"
            if app_base:
                app_url = f"{app_base}/pages-workflow/instance-detail?instanceId={link_id}"

        return {"pcUrl": pc_url, "appUrl": app_url}

    @classmethod
    async def create_todo(
            cls,
            union_id: str,
            subject: str,
            description: str = "",
            executor_ids: List[str] = None,
            due_time: int = None,
            priority: int = 20,
            source_id: str = None,
            link_type: str = "",
            link_id: str = "",
    ) -> Optional[str]:
        """
        创建钉钉待办任务

        Args:
            union_id: 创建者的 unionId
            subject: 待办标题（最大1024字符）
            description: 备注描述（最大4096字符）
            executor_ids: 执行者的 unionId 列表
            due_time: 截止时间（Unix 时间戳，单位毫秒）
            priority: 优先级 10:较低 20:普通 30:较高 40:紧急
            source_id: 业务系统唯一标识
            link_type: 关联类型（用于拼接详情页URL）
            link_id: 关联对象ID

        Returns:
            钉钉待办 taskId，失败返回 None
        """
        headers = await cls._get_headers()
        if not headers:
            return None

        detail_url = await cls._get_detail_urls(link_type, link_id)

        payload: Dict[str, Any] = {
            "subject": subject[:1024],
            "creatorId": union_id,
            "priority": priority,
            "notifyConfigs": {
                "dingNotify": "1",
            },
        }

        if description:
            payload["description"] = description[:4096]
        if executor_ids:
            payload["executorIds"] = executor_ids[:100]
        if due_time:
            payload["dueTime"] = due_time
        if source_id:
            payload["sourceId"] = source_id

        if detail_url.get("pcUrl") or detail_url.get("appUrl"):
            payload["detailUrl"] = {}
            if detail_url.get("pcUrl"):
                payload["detailUrl"]["pcUrl"] = detail_url["pcUrl"]
            if detail_url.get("appUrl"):
                payload["detailUrl"]["appUrl"] = detail_url["appUrl"]

        try:
            url = f"{cls.BASE_URL}/{union_id}/tasks"
            async with httpx.AsyncClient(timeout=10.0) as client:
                resp = await client.post(url, headers=headers, json=payload)
                if resp.status_code == 200:
                    result = resp.json()
                    task_id = result.get("id")
                    logger.info(f"钉钉待办创建成功: taskId={task_id}")
                    return task_id
                else:
                    logger.error(f"钉钉待办创建失败: status={resp.status_code}, body={resp.text}")
                    return None
        except Exception as e:
            logger.error(f"钉钉待办创建异常: {e}")
            return None

    @classmethod
    async def update_todo(
            cls,
            union_id: str,
            task_id: str,
            subject: str = None,
            description: str = None,
            done: bool = None,
            due_time: int = None,
            executor_ids: List[str] = None,
    ) -> bool:
        """
        更新钉钉待办任务

        Args:
            union_id: 操作者 unionId
            task_id: 钉钉待办 taskId
            subject: 待办标题
            description: 备注描述
            done: 完成状态
            due_time: 截止时间
            executor_ids: 执行者列表
        """
        headers = await cls._get_headers()
        if not headers:
            return False

        payload: Dict[str, Any] = {}
        if subject is not None:
            payload["subject"] = subject[:1024]
        if description is not None:
            payload["description"] = description[:4096]
        if done is not None:
            payload["done"] = done
        if due_time is not None:
            payload["dueTime"] = due_time
        if executor_ids is not None:
            payload["executorIds"] = executor_ids[:100]

        if not payload:
            return True

        try:
            url = f"{cls.BASE_URL}/{union_id}/tasks/{task_id}"
            async with httpx.AsyncClient(timeout=10.0) as client:
                resp = await client.put(url, headers=headers, json=payload)
                if resp.status_code == 200:
                    logger.info(f"钉钉待办更新成功: taskId={task_id}")
                    return True
                else:
                    logger.error(f"钉钉待办更新失败: status={resp.status_code}, body={resp.text}")
                    return False
        except Exception as e:
            logger.error(f"钉钉待办更新异常: {e}")
            return False

    @classmethod
    async def complete_todo(cls, union_id: str, task_id: str) -> bool:
        """完成钉钉待办"""
        return await cls.update_todo(union_id, task_id, done=True)

    @classmethod
    async def delete_todo(cls, union_id: str, task_id: str) -> bool:
        """删除钉钉待办"""
        headers = await cls._get_headers()
        if not headers:
            return False

        try:
            url = f"{cls.BASE_URL}/{union_id}/tasks/{task_id}"
            async with httpx.AsyncClient(timeout=10.0) as client:
                resp = await client.delete(url, headers=headers)
                if resp.status_code == 200:
                    logger.info(f"钉钉待办删除成功: taskId={task_id}")
                    return True
                else:
                    logger.error(f"钉钉待办删除失败: status={resp.status_code}, body={resp.text}")
                    return False
        except Exception as e:
            logger.error(f"钉钉待办删除异常: {e}")
            return False


def build_dingtalk_markdown(title: str, content: str, app_name: str = None) -> str:
    """
    构建钉钉通知的 Markdown 内容

    Args:
        title: 通知标题
        content: 通知内容
        app_name: 应用名称
    """
    app = app_name or settings.APP_NAME
    return f"### {title}\n\n{content}\n\n---\n> 来自 {app}"
