"""
通知模块 - 支持钉钉、日志等多种通知方式
"""

import requests
import time
import hmac
import hashlib
import base64
import urllib.parse
import os
from loguru import logger
from typing import Optional

from core.common.oss_utils import upload_image_to_oss


def _get_dingtalk_config() -> dict:
    """获取钉钉机器人配置（第一群）"""
    return {
        "webhook": os.getenv("DINGTALK_WEBHOOK", ""),
        "secret": os.getenv("DINGTALK_SECRET", ""),
    }


def _get_all_dingtalk_configs() -> list:
    """获取所有钉钉机器人配置（支持多群）
    环境变量格式: DINGTALK_WEBHOOK_1 + DINGTALK_SECRET_1, ...
    """
    configs = []
    i = 1
    while True:
        webhook = os.getenv(f"DINGTALK_WEBHOOK_{i}", "")
        secret = os.getenv(f"DINGTALK_SECRET_{i}", "")
        if not webhook:
            break
        configs.append({"webhook": webhook, "secret": secret})
        i += 1
    return configs


class DingTalkBot:
    """钉钉机器人类"""

    def __init__(self, webhook: str = None, secret: str = None):
        """
        初始化钉钉机器人

        Args:
            webhook: 钉钉自定义机器人webhook地址
            secret: 加签密钥（可选）
        """
        config = _get_dingtalk_config()
        self.webhook = webhook or config.get("webhook", "")
        self.secret = secret or config.get("secret", "")

        if not self.webhook:
            logger.warning("钉钉webhook未配置，消息将不会被发送")

    def _gen_signed_url(self) -> str:
        """生成带签名的URL"""
        if not self.secret:
            return self.webhook

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

    def send_text(
        self, content: str, at_mobiles: list = None, is_at_all: bool = False
    ) -> bool:
        """
        发送文本消息

        Args:
            content: 消息内容
            at_mobiles: 需要@的手机号列表
            is_at_all: 是否@所有人

        Returns:
            bool: 是否发送成功
        """
        if not self.webhook:
            logger.warning(f"[钉钉消息未发送] {content[:100]}...")
            return False

        at_mobiles = at_mobiles or []
        data = {
            "msgtype": "text",
            "text": {"content": content},
            "at": {"atMobiles": at_mobiles, "isAtAll": is_at_all},
        }

        url = self._gen_signed_url()

        try:
            response = requests.post(url, json=data, timeout=5)
            response.raise_for_status()
            result = response.json()
            if result.get("errcode", -1) != 0:
                logger.error(f"钉钉消息发送失败: {result}")
                return False
            logger.info("钉钉消息发送成功")
            return True
        except Exception as e:
            logger.error(f"钉钉消息发送异常: {e}")
            return False

    def send_markdown(
        self,
        title: str,
        text: str,
        at_mobiles: list = None,
        is_at_all: bool = False,
    ) -> bool:
        """
        发送markdown消息

        Args:
            title: 消息标题
            text: markdown格式的消息内容
            at_mobiles: 需要@的手机号列表
            is_at_all: 是否@所有人

        Returns:
            bool: 是否发送成功
        """
        if not self.webhook:
            logger.warning(f"[钉钉Markdown未发送] {title}")
            return False

        at_mobiles = at_mobiles or []
        data = {
            "msgtype": "markdown",
            "markdown": {"title": title, "text": text},
            "at": {"atMobiles": at_mobiles, "isAtAll": is_at_all},
        }

        url = self._gen_signed_url()

        try:
            response = requests.post(url, json=data, timeout=5)
            response.raise_for_status()
            result = response.json()
            if result.get("errcode", -1) != 0:
                logger.error(f"钉钉markdown消息发送失败: {result}")
                return False
            logger.info("钉钉markdown消息发送成功")
            return True
        except Exception as e:
            logger.error(f"钉钉markdown消息发送异常: {e}")
            return False

    def send_image(self, image_path: str, title: str = "图片", max_size_kb: int = 6) -> bool:
        """
        发送图片消息（自动压缩以适应钉钉20KB限制）
        注意：钉钉限制的是整个请求body大小，base64编码会增加约33%体积，所以图片本身需要更小

        Args:
            image_path: 图片文件路径（本地路径）
            title: 消息标题
            max_size_kb: 最大图片大小（KB），默认6KB（base64后约8KB，加上其他字段约10KB）

        Returns:
            bool: 是否发送成功
        """
        if not self.webhook:
            logger.warning(f"[钉钉图片未发送] {title}")
            return False

        if not os.path.exists(image_path):
            logger.error(f"图片文件不存在: {image_path}")
            return False

        try:
            # 读取并压缩图片
            image_data = self._compress_image(image_path, max_size_kb)

            if not image_data:
                logger.error(f"图片压缩失败: {image_path}")
                return False

            # 转为base64
            image_base64 = base64.b64encode(image_data).decode("utf-8")
            # 计算图片md5（钉钉需要）
            image_md5 = hashlib.md5(image_data).hexdigest()

            data = {
                "msgtype": "image",
                "image": {
                    "base64": image_base64,
                    "md5": image_md5
                }
            }

            url = self._gen_signed_url()
            response = requests.post(url, json=data, timeout=10)
            response.raise_for_status()
            result = response.json()

            if result.get("errcode", -1) != 0:
                logger.error(f"钉钉图片发送失败: {result}")
                return False

            logger.info(f"钉钉图片发送成功: {image_path}")
            return True

        except Exception as e:
            logger.error(f"钉钉图片发送异常: {e}")
            return False

    def _compress_image(self, image_path: str, max_size_kb: int) -> bytes:
        """
        压缩图片到指定大小以下

        Args:
            image_path: 图片路径
            max_size_kb: 最大大小（KB）

        Returns:
            bytes: 压缩后的图片数据
        """
        from PIL import Image
        import io

        max_size_bytes = max_size_kb * 1024

        # 先尝试读取原图
        with open(image_path, "rb") as f:
            image_data = f.read()

        # 如果已经小于限制，直接返回
        if len(image_data) <= max_size_bytes:
            return image_data

        # 需要压缩，使用PIL重新保存
        img = Image.open(image_path)

        # 转换为RGB（去除alpha通道，减小大小）
        if img.mode in ('RGBA', 'P'):
            img = img.convert('RGB')

        # 逐步降低质量直到满足大小要求
        quality = 85
        min_quality = 30

        while quality >= min_quality:
            buffer = io.BytesIO()
            img.save(buffer, format='JPEG', quality=quality, optimize=True)
            compressed_data = buffer.getvalue()

            if len(compressed_data) <= max_size_bytes:
                logger.info(f"图片压缩成功: {len(image_data)/1024:.1f}KB -> {len(compressed_data)/1024:.1f}KB (quality={quality})")
                return compressed_data

            quality -= 10

        # 如果质量降到最小还不行，尝试缩小尺寸
        logger.warning(f"降低质量无法满足要求，尝试缩小尺寸")
        width, height = img.size

        while width > 200 and height > 200:
            width = int(width * 0.8)
            height = int(height * 0.8)
            resized_img = img.resize((width, height), Image.Resampling.LANCZOS)

            buffer = io.BytesIO()
            resized_img.save(buffer, format='JPEG', quality=min_quality, optimize=True)
            compressed_data = buffer.getvalue()

            if len(compressed_data) <= max_size_bytes:
                logger.info(f"图片压缩成功: {len(image_data)/1024:.1f}KB -> {len(compressed_data)/1024:.1f}KB ({width}x{height})")
                return compressed_data

        logger.error(f"无法将图片压缩到 {max_size_kb}KB 以下")
        return None

    def send_image_with_text(self, image_path: str, title: str = "图片", text: str = "") -> bool:
        """
        发送图文消息（markdown格式嵌入图片链接）
        注意：需要使用钉钉的媒体文件上传接口获取URL，这里使用简化的markdown图片语法

        Args:
            image_path: 图片文件路径
            title: 消息标题
            text:  accompanying text

        Returns:
            bool: 是否发送成功
        """
        if not self.webhook:
            logger.warning(f"[钉钉图文未发送] {title}")
            return False

        if not os.path.exists(image_path):
            logger.error(f"图片文件不存在: {image_path}")
            return False

        # 先尝试直接发送图片
        success = self.send_image(image_path, title)

        # 如果图片发送成功且有文字，再发送文字
        if success and text:
            time.sleep(0.5)  # 避免发送过快
            return self.send_text(f"{title}\n{text}")

        return success

    def send_file(self, file_path: str, title: str = "文件") -> bool:
        """
        发送文件（通过钉钉文件上传接口）
        注意：需要企业版钉钉机器人，个人版可能不支持

        Args:
            file_path: 文件路径
            title: 文件标题

        Returns:
            bool: 是否发送成功
        """
        if not self.webhook:
            logger.warning(f"[钉钉文件未发送] {title}")
            return False

        if not os.path.exists(file_path):
            logger.error(f"文件不存在: {file_path}")
            return False

        try:
            # 获取文件大小
            file_size = os.path.getsize(file_path)
            file_name = os.path.basename(file_path)

            # 钉钉文件大小限制：20MB
            if file_size > 20 * 1024 * 1024:
                logger.error(f"文件过大: {file_size/1024/1024:.1f}MB > 20MB")
                return False

            # 读取文件并转为base64
            with open(file_path, "rb") as f:
                file_data = f.read()

            file_base64 = base64.b64encode(file_data).decode("utf-8")

            # 构建消息
            data = {
                "msgtype": "file",
                "file": {
                    "base64": file_base64,
                    "name": file_name
                }
            }

            url = self._gen_signed_url()
            response = requests.post(url, json=data, timeout=30)
            response.raise_for_status()
            result = response.json()

            if result.get("errcode", -1) != 0:
                # 如果文件发送失败，尝试作为图片发送
                if file_path.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.bmp')):
                    logger.warning(f"文件发送失败，尝试作为图片发送: {result}")
                    return self.send_image(file_path, title)
                logger.error(f"钉钉文件发送失败: {result}")
                return False

            logger.info(f"钉钉文件发送成功: {file_name}")
            return True

        except Exception as e:
            logger.error(f"钉钉文件发送异常: {e}")
            return False

    def send_local_image_as_link(self, image_path: str, title: str = "图片", text: str = "") -> bool:
        """
        发送本地图片（转换为base64嵌入markdown）
        注意：钉钉不支持直接显示base64图片，此方法会发送图片链接文本

        Args:
            image_path: 图片文件路径
            title: 消息标题
            text: 附加文本

        Returns:
            bool: 是否发送成功
        """
        if not self.webhook:
            logger.warning(f"[钉钉图片链接未发送] {title}")
            return False

        if not os.path.exists(image_path):
            logger.error(f"图片文件不存在: {image_path}")
            return False

        try:
            # 读取图片
            with open(image_path, "rb") as f:
                image_data = f.read()

            # 转为base64
            image_base64 = base64.b64encode(image_data).decode("utf-8")

            # 获取图片格式
            ext = os.path.splitext(image_path)[1].lower().replace('.', '')
            if ext == 'jpg':
                ext = 'jpeg'
            if ext not in ['png', 'jpeg', 'gif', 'bmp']:
                ext = 'png'

            # 构建data URL（钉钉可能不支持直接显示，但可以作为链接）
            data_url = f"data:image/{ext};base64,{image_base64[:100]}..."

            # 发送markdown消息
            markdown = f"## {title}\n\n"
            if text:
                markdown += f"{text}\n\n"
            markdown += f"**图片**: {os.path.basename(image_path)}\n\n"
            markdown += f"大小: {len(image_data)/1024:.1f}KB\n"

            return self.send_markdown(title, markdown)

        except Exception as e:
            logger.error(f"发送图片链接异常: {e}")
            return False

    def send_image_via_oss(
        self,
        image_path: str,
        title: str = "策略图表",
        text: str = "",
        expire_days: int = 7,
    ) -> bool:
        """
        上传图片到 OSS 并通过 Markdown 发送到钉钉
        这是发送图片的推荐方式，不受 20KB 限制

        Args:
            image_path: 本地图片路径
            title: 消息标题
            text: 附加文本
            expire_days: OSS 链接有效期（天）

        Returns:
            bool: 是否发送成功
        """
        if not self.webhook:
            logger.warning(f"[钉钉OSS图片未发送] {title}")
            return False

        if not os.path.exists(image_path):
            logger.error(f"图片文件不存在: {image_path}")
            return False

        try:
            # 上传图片到 OSS
            image_url = upload_image_to_oss(image_path, expire_days)

            if not image_url:
                logger.error("图片上传到 OSS 失败")
                # 尝试直接发送压缩后的图片
                return self.send_image(image_path, title)

            # 构建 Markdown 消息
            markdown = f"## {title}\n\n"

            if text:
                markdown += f"{text}\n\n"

            # 添加图片（钉钉 Markdown 支持图片语法）
            markdown += f"![{title}]({image_url})\n\n"

            # 添加图片信息
            file_size = os.path.getsize(image_path) / 1024
            markdown += f"---\n"
            markdown += f"**图片**: {os.path.basename(image_path)} ({file_size:.1f}KB)\n"
            markdown += f"**有效期**: {expire_days}天\n"

            # 发送 Markdown 消息
            return self.send_markdown(title, markdown)

        except Exception as e:
            logger.error(f"发送 OSS 图片异常: {e}")
            # 失败时尝试直接发送
            return self.send_image(image_path, title)


def send_to_all_groups(
    send_func_name: str,
    **kwargs,
) -> bool:
    """
    向所有已配置的钉钉群发送消息

    Args:
        send_func_name: DingTalkBot 的发送方法名，如 'send_text', 'send_markdown', 'send_image_via_oss'
        **kwargs: 传递给发送方法的参数

    Returns:
        bool: 是否全部发送成功
    """
    configs = _get_all_dingtalk_configs()
    if not configs:
        logger.warning("没有配置任何钉钉群，消息未发送")
        return False

    all_success = True
    for i, cfg in enumerate(configs, 1):
        bot = DingTalkBot(webhook=cfg["webhook"], secret=cfg["secret"])
        method = getattr(bot, send_func_name, None)
        if method is None:
            logger.error(f"DingTalkBot 没有方法: {send_func_name}")
            return False
        try:
            success = method(**kwargs)
            if success:
                logger.info(f"群{i} 发送成功")
            else:
                logger.error(f"群{i} 发送失败")
                all_success = False
        except Exception as e:
            logger.error(f"群{i} 发送异常: {e}")
            all_success = False
    return all_success


class NotificationManager:
    """通知管理器 - 统一管理多种通知渠道"""

    def __init__(self):
        self.dingtalk = DingTalkBot()

    def notify(self, message: str, title: str = "系统通知", use_markdown: bool = False):
        """
        发送通知（优先使用钉钉，失败则记录日志）

        Args:
            message: 消息内容
            title: 消息标题（markdown模式使用）
            use_markdown: 是否使用markdown格式
        """
        if use_markdown:
            success = self.dingtalk.send_markdown(title, message)
        else:
            success = self.dingtalk.send_text(message)

        if not success:
            # 钉钉发送失败，记录到日志
            logger.info(f"[通知] {title}: {message}")

    def notify_error(self, error_msg: str):
        """发送错误通知"""
        markdown = f"""## 错误告警

**时间**: {time.strftime('%Y-%m-%d %H:%M:%S')}

**错误信息**:
```
{error_msg}
```
"""
        self.notify(markdown, title="系统错误", use_markdown=True)

    def notify_signal(self, signals: list, signal_type: str = "CCI超卖"):
        """
        发送交易信号通知

        Args:
            signals: 信号列表，每项为dict包含code, name等指标
            signal_type: 信号类型名称
        """
        if not signals:
            logger.info(f"[{signal_type}] 无信号")
            return

        # 构建markdown表格
        if signals:
            headers = signals[0].keys()
            header_line = " | ".join(headers)
            separator = " | ".join(["---"] * len(headers))

            rows = []
            for s in signals:
                row = " | ".join(str(v) for v in s.values())
                rows.append(row)

            table = f"{header_line}\n{separator}\n" + "\n".join(rows)
        else:
            table = "无"

        markdown = f"""## {signal_type}信号

**时间**: {time.strftime('%Y-%m-%d %H:%M:%S')}

**筛选结果**:

{table}

共 {len(signals)} 个标的符合筛选条件。
"""
        self.notify(markdown, title=f"{signal_type}信号", use_markdown=True)
