# -*- coding: utf-8 -*-
"""
notification_service.py — 通知配置与发送服务

支持渠道：
  - 邮件（SMTP）
  - 钉钉机器人
  - 企业微信机器人

数据存储：service_monitor/data/notifications.json
"""

from __future__ import annotations

import hashlib
import hmac
import json
import logging
import smtplib
import time
import urllib.parse
from datetime import datetime
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from typing import Optional

from ..utils.paths import NOTIFICATIONS_FILE, ensure_dirs
from ..utils.crypto import encrypt_password, decrypt_password

logger = logging.getLogger("service_monitor.notification")

# 默认配置模板
DEFAULT_CONFIG = {
    "email": {
        "enabled": False,
        "smtp_host": "",
        "smtp_port": 465,
        "smtp_user": "",
        "smtp_password": "",
        "use_ssl": True,
        "recipients": [],
        "subject_template": "【巡检报告】{target} - {status}"
    },
    "dingtalk": {
        "enabled": False,
        "webhook_url": "",
        "secret": "",
        "at_mobiles": []
    },
    "wecom": {
        "enabled": False,
        "webhook": ""
    },
    "trigger": {
        "on_complete": True,
        "on_abnormal_only": False,
        "include_details": True,
        "include_link": True
    },
    "report_missing": {
        "enabled": False,
        "missing_days": 2,
        "channels": [],
    },
    "updated_at": None,
    "updated_by": None
}


def _load_config() -> dict:
    """加载通知配置（解密敏感字段）。"""
    if not NOTIFICATIONS_FILE.exists():
        return DEFAULT_CONFIG.copy()
    try:
        data = json.loads(NOTIFICATIONS_FILE.read_text(encoding="utf-8"))
        # 兼容旧字段名 webhook -> webhook_url
        if "dingtalk" in data and "webhook" in data["dingtalk"]:
            if not data["dingtalk"].get("webhook_url"):
                data["dingtalk"]["webhook_url"] = data["dingtalk"].pop("webhook")
            else:
                data["dingtalk"].pop("webhook", None)
        # 解密敏感字段
        if data.get("email", {}).get("smtp_password"):
            data["email"]["smtp_password"] = decrypt_password(data["email"]["smtp_password"])
        if data.get("dingtalk", {}).get("secret"):
            data["dingtalk"]["secret"] = decrypt_password(data["dingtalk"]["secret"])
        return data
    except (json.JSONDecodeError, OSError) as e:
        logger.error("加载通知配置失败: %s", e)
        return DEFAULT_CONFIG.copy()


def _save_config(data: dict) -> None:
    """保存通知配置（加密敏感字段）。"""
    ensure_dirs()
    to_save = json.loads(json.dumps(data))  # 深拷贝
    # 加密敏感字段
    if to_save.get("email", {}).get("smtp_password"):
        to_save["email"]["smtp_password"] = encrypt_password(to_save["email"]["smtp_password"])
    if to_save.get("dingtalk", {}).get("secret"):
        to_save["dingtalk"]["secret"] = encrypt_password(to_save["dingtalk"]["secret"])
    NOTIFICATIONS_FILE.write_text(
        json.dumps(to_save, ensure_ascii=False, indent=2),
        encoding="utf-8"
    )


def get_config() -> dict:
    """获取通知配置（敏感字段已解密，可直接展示）。"""
    return _load_config()


def get_config_masked() -> dict:
    """获取通知配置（敏感字段脱敏，用于前端展示）。"""
    config = _load_config()
    # 密码脱敏
    if config.get("email", {}).get("smtp_password"):
        config["email"]["smtp_password"] = "******"
    if config.get("dingtalk", {}).get("secret"):
        config["dingtalk"]["secret"] = "******"
    return config


def save_config(data: dict, updated_by: str = "admin") -> dict:
    """保存通知配置。"""
    data["updated_at"] = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
    data["updated_by"] = updated_by
    _save_config(data)
    logger.info("通知配置已更新 by %s", updated_by)
    return get_config_masked()


# ============================================================
# 测试发送
# ============================================================

def test_email() -> dict:
    """发送测试邮件。"""
    config = _load_config()
    email_cfg = config.get("email", {})

    if not email_cfg.get("enabled"):
        return {"success": False, "message": "邮件通知未启用"}

    smtp_host = email_cfg.get("smtp_host", "").strip()
    smtp_port = int(email_cfg.get("smtp_port", 465))
    smtp_user = email_cfg.get("smtp_user", "").strip()
    smtp_password = email_cfg.get("smtp_password", "").strip()
    use_ssl = email_cfg.get("use_ssl", True)
    recipients = email_cfg.get("recipients", [])

    if not smtp_host or not smtp_user or not smtp_password or not recipients:
        return {"success": False, "message": "邮件配置不完整"}

    # 构建测试邮件
    msg = MIMEMultipart()
    msg["From"] = smtp_user
    msg["To"] = ", ".join(recipients)
    msg["Subject"] = "【测试】巡检报告通知配置测试"

    body = """
    <h2>通知配置测试</h2>
    <p>这是一封测试邮件，用于验证邮件通知配置是否正确。</p>
    <p>发送时间：{time}</p>
    <hr>
    <p style="color: #666; font-size: 12px;">此邮件由服务监测模块自动发送</p>
    """.format(time=datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
    msg.attach(MIMEText(body, "html", "utf-8"))

    try:
        if use_ssl:
            smtp = smtplib.SMTP_SSL(smtp_host, smtp_port, timeout=10)
        else:
            smtp = smtplib.SMTP(smtp_host, smtp_port, timeout=10)
            smtp.starttls()

        smtp.login(smtp_user, smtp_password)
        smtp.sendmail(smtp_user, recipients, msg.as_string())
        smtp.quit()
        logger.info("测试邮件发送成功 -> %s", recipients)
        return {"success": True, "message": f"测试邮件已发送至 {', '.join(recipients)}"}
    except smtplib.SMTPAuthenticationError:
        return {"success": False, "message": "SMTP 认证失败，请检查用户名和密码"}
    except smtplib.SMTPException as e:
        return {"success": False, "message": f"SMTP 错误: {e}"}
    except Exception as e:
        return {"success": False, "message": f"发送失败: {e}"}


def test_dingtalk() -> dict:
    """发送测试钉钉消息（Markdown 格式）。"""
    import requests
    import base64

    config = _load_config()
    ding_cfg = config.get("dingtalk", {})

    if not ding_cfg.get("enabled"):
        return {"success": False, "message": "钉钉通知未启用"}

    webhook_url = ding_cfg.get("webhook_url", "").strip()
    secret = ding_cfg.get("secret", "").strip()

    if not webhook_url:
        return {"success": False, "message": "Webhook 地址未配置"}

    # 构建测试消息（Markdown 格式）
    now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    text = (
        f"🔧 **【钉钉通知测试】**\n\n"
        f"这是一条测试消息，用于验证钉钉通知配置是否正确。\n\n"
        f"⏰ 发送时间：{now_str}\n"
    )
    content = {
        "msgtype": "markdown",
        "markdown": {"title": "钉钉通知测试", "text": text}
    }

    # 签名（如果有）
    url = webhook_url
    if secret:
        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).decode())
        url = f"{webhook_url}&timestamp={timestamp}&sign={sign}"

    try:
        resp = requests.post(url, json=content, timeout=10)
        result = resp.json()
        if result.get("errcode") == 0:
            logger.info("钉钉测试消息发送成功")
            return {"success": True, "message": "测试消息已发送到钉钉群"}
        else:
            return {"success": False, "message": f"钉钉返回错误: {result.get('errmsg', '未知错误')}"}
    except Exception as e:
        return {"success": False, "message": f"发送失败: {e}"}


def test_wecom() -> dict:
    """发送测试企业微信消息。"""
    import requests

    config = _load_config()
    wecom_cfg = config.get("wecom", {})

    if not wecom_cfg.get("enabled"):
        return {"success": False, "message": "企业微信通知未启用"}

    webhook = wecom_cfg.get("webhook", "").strip()

    if not webhook:
        return {"success": False, "message": "Webhook 地址未配置"}

    # 构建消息
    content = {
        "msgtype": "text",
        "text": {
            "content": f"【测试】巡检报告通知配置测试\n\n发送时间：{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n此消息用于验证企业微信通知配置是否正确。"
        }
    }

    try:
        resp = requests.post(webhook, json=content, timeout=10)
        result = resp.json()
        if result.get("errcode") == 0:
            logger.info("企业微信测试消息发送成功")
            return {"success": True, "message": "测试消息已发送到企业微信群"}
        else:
            return {"success": False, "message": f"企业微信返回错误: {result.get('errmsg', '未知错误')}"}
    except Exception as e:
        return {"success": False, "message": f"发送失败: {e}"}


# ============================================================
# 发送通知
# ============================================================

def send_notification(report: dict, report_url: str) -> bool:
    """根据配置发送通知。

    Args:
        report: 巡检报告数据
        report_url: 报告链接地址

    Returns:
        是否发送成功（至少一个渠道成功）
    """
    config = _load_config()
    trigger_cfg = config.get("trigger", {})

    # 判断是否需要通知
    summary = report.get("summary", {})
    has_abnormal = summary.get("警告", 0) > 0 or summary.get("严重", 0) > 0

    if trigger_cfg.get("on_abnormal_only") and not has_abnormal:
        logger.info("无异常且配置为仅异常时通知，跳过通知")
        return False

    success = False

    # 发送邮件
    if config.get("email", {}).get("enabled"):
        if _send_email_notification(report, report_url, config):
            success = True

    # 发送钉钉
    if config.get("dingtalk", {}).get("enabled"):
        if _send_dingtalk_notification(report, report_url, config):
            success = True

    # 发送企业微信
    if config.get("wecom", {}).get("enabled"):
        if _send_wecom_notification(report, report_url, config):
            success = True

    return success


def send_missing_report_notification(
    missing_targets: list,
    report_missing_cfg: dict,
) -> bool:
    """发送报告缺失告警通知。

    Args:
        missing_targets: 缺失报告的目标列表 [
            {"target_name": str, "last_report_at": str or None, "missing_days": int},
            ...
        ]
        report_missing_cfg: 报告缺失告警配置（含 channels）

    Returns:
        是否发送成功（至少一个渠道成功）
    """
    if not missing_targets:
        return False

    config = _load_config()
    channels = report_missing_cfg.get("channels", [])

    if not channels:
        logger.info("报告缺失告警未配置渠道，跳过")
        return False

    # 构建告警消息
    target_count = len(missing_targets)
    lines = []
    for mt in missing_targets:
        name = mt.get("target_name", "未知")
        last_time = mt.get("last_report_at", "无报告")
        days = mt.get("missing_days", 0)
        if last_time:
            last_time = last_time.replace("T", " ")[:19]
        lines.append(f"- **{name}**：已 {days} 天无新报告（上次：{last_time}）")

    md_msg = (
        f"### ⚠️ 报告缺失告警\n\n"
        f"以下 {target_count} 个目标长时间未产生巡检报告：\n\n"
        + "\n".join(lines)
        + "\n\n请检查对应目标的运行状态。"
    )

    email_body = (
        f"<h2>⚠️ 报告缺失告警</h2>"
        f"<p>以下 {target_count} 个目标长时间未产生巡检报告：</p>"
        f"<ul>"
        + "".join(
            f"<li><b>{mt.get('target_name', '未知')}</b>：已 {mt.get('missing_days', 0)} 天无新报告"
            f"（上次：{(mt.get('last_report_at') or '无报告').replace('T', ' ')[:19]}）</li>"
            for mt in missing_targets
        )
        + "</ul>"
        f"<hr><p style='color:#666;font-size:12px;'>请检查对应目标的运行状态。</p>"
    )

    success = False

    # 发送邮件
    if "email" in channels and config.get("email", {}).get("enabled"):
        try:
            email_cfg = config.get("email", {})
            smtp_host = email_cfg.get("smtp_host", "").strip()
            smtp_port = int(email_cfg.get("smtp_port", 465))
            smtp_user = email_cfg.get("smtp_user", "").strip()
            smtp_password = email_cfg.get("smtp_password", "").strip()
            use_ssl = email_cfg.get("use_ssl", True)
            recipients = email_cfg.get("recipients", [])

            if all([smtp_host, smtp_user, smtp_password, recipients]):
                from email.mime.multipart import MIMEMultipart
                from email.mime.text import MIMEText

                msg = MIMEMultipart()
                msg["From"] = smtp_user
                msg["To"] = ", ".join(recipients)
                msg["Subject"] = f"【报告缺失告警】{target_count} 个目标长时间无报告"
                msg.attach(MIMEText(email_body, "html", "utf-8"))

                if use_ssl:
                    smtp = smtplib.SMTP_SSL(smtp_host, smtp_port, timeout=10)
                else:
                    smtp = smtplib.SMTP(smtp_host, smtp_port, timeout=10)
                    smtp.starttls()
                smtp.login(smtp_user, smtp_password)
                smtp.sendmail(smtp_user, recipients, msg.as_string())
                smtp.quit()
                success = True
        except Exception as e:
            logger.warning("报告缺失告警邮件发送失败: %s", e)

    # 发送钉钉
    if "dingtalk" in channels and config.get("dingtalk", {}).get("enabled"):
        try:
            import requests, base64
            ding_cfg = config.get("dingtalk", {})
            webhook_url = ding_cfg.get("webhook_url", "").strip()
            secret = ding_cfg.get("secret", "").strip()

            if webhook_url:
                content = {
                    "msgtype": "markdown",
                    "markdown": {
                        "title": f"报告缺失告警 - {target_count} 个目标",
                        "text": md_msg,
                    }
                }
                url = webhook_url
                if secret:
                    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).decode())
                    url = f"{webhook_url}&timestamp={timestamp}&sign={sign}"

                resp = requests.post(url, json=content, timeout=10)
                result = resp.json()
                if result.get("errcode") == 0:
                    success = True
        except Exception as e:
            logger.warning("报告缺失告警钉钉发送失败: %s", e)

    # 发送企微
    if "wecom" in channels and config.get("wecom", {}).get("enabled"):
        try:
            import requests
            wecom_cfg = config.get("wecom", {})
            webhook = wecom_cfg.get("webhook", "").strip()

            if webhook:
                content = {
                    "msgtype": "markdown",
                    "markdown": {"content": md_msg},
                }
                resp = requests.post(webhook, json=content, timeout=10)
                result = resp.json()
                if result.get("errcode") == 0:
                    success = True
        except Exception as e:
            logger.warning("报告缺失告警企微发送失败: %s", e)

    if success:
        logger.info("报告缺失告警已发送: %d 个目标无报告", target_count)
    return success


def _send_email_notification(report: dict, report_url: str, config: dict) -> bool:
    """发送邮件通知。"""
    import requests

    email_cfg = config.get("email", {})
    trigger_cfg = config.get("trigger", {})

    smtp_host = email_cfg.get("smtp_host", "").strip()
    smtp_port = int(email_cfg.get("smtp_port", 465))
    smtp_user = email_cfg.get("smtp_user", "").strip()
    smtp_password = email_cfg.get("smtp_password", "").strip()
    use_ssl = email_cfg.get("use_ssl", True)
    recipients = email_cfg.get("recipients", [])

    if not all([smtp_host, smtp_user, smtp_password, recipients]):
        logger.warning("邮件配置不完整，跳过发送")
        return False

    # 构建主题
    summary = report.get("summary", {})
    status = "正常" if summary.get("警告", 0) == 0 and summary.get("严重", 0) == 0 else "异常"
    subject = email_cfg.get("subject_template", "【巡检报告】{target} - {status}").format(
        target=report.get("target_name", "未知目标"),
        status=status
    )

    # 构建正文
    body_lines = [
        "<h2>巡检报告通知</h2>",
        f"<p><b>目标：</b>{report.get('target_name', '-')}</p>",
        f"<p><b>套件：</b>{'快速巡检' if report.get('suite') == 'quick' else '全量巡检'}</p>",
        f"<p><b>完成时间：</b>{report.get('finished_at', '-')}</p>",
        "<hr>",
        f"<p><b>汇总：</b>正常 {summary.get('正常', 0)} / 警告 {summary.get('警告', 0)} / 严重 {summary.get('严重', 0)}</p>",
    ]

    # 异常项详情
    if trigger_cfg.get("include_details") and (summary.get("警告", 0) > 0 or summary.get("严重", 0) > 0):
        body_lines.append("<h3>⚠️ 异常项</h3><ul>")
        for mod in report.get("modules", []):
            for item in mod.get("items", []):
                if item.get("status") in ("警告", "严重"):
                    body_lines.append(
                        f"<li><b>{mod.get('name')}</b>: {item.get('name')} = {item.get('value')} "
                        f"（阈值：{item.get('threshold') or '-'}，状态：{item.get('status')}）</li>"
                    )
        body_lines.append("</ul>")

    # 报告链接
    if trigger_cfg.get("include_link") and report_url:
        body_lines.append(f"<hr><p><a href='{report_url}'>查看完整报告</a></p>")

    body_lines.append("<p style='color: #666; font-size: 12px; margin-top: 20px;'>此邮件由服务监测模块自动发送</p>")

    msg = MIMEMultipart()
    msg["From"] = smtp_user
    msg["To"] = ", ".join(recipients)
    msg["Subject"] = subject
    msg.attach(MIMEText("\n".join(body_lines), "html", "utf-8"))

    try:
        if use_ssl:
            smtp = smtplib.SMTP_SSL(smtp_host, smtp_port, timeout=10)
        else:
            smtp = smtplib.SMTP(smtp_host, smtp_port, timeout=10)
            smtp.starttls()

        smtp.login(smtp_user, smtp_password)
        smtp.sendmail(smtp_user, recipients, msg.as_string())
        smtp.quit()
        logger.info("邮件通知已发送 -> %s", recipients)
        return True
    except Exception as e:
        logger.error("邮件通知发送失败: %s", e)
        return False


def _send_dingtalk_notification(report: dict, report_url: str, config: dict) -> bool:
    """发送钉钉通知（Markdown 格式，突出异常项）。"""
    import requests
    import base64

    ding_cfg = config.get("dingtalk", {})
    trigger_cfg = config.get("trigger", {})

    webhook_url = ding_cfg.get("webhook_url", "").strip()
    secret = ding_cfg.get("secret", "").strip()
    at_mobiles = ding_cfg.get("at_mobiles", [])

    if not webhook_url:
        return False

    summary = report.get("summary", {})
    has_abnormal = summary.get("警告", 0) > 0 or summary.get("严重", 0) > 0

    # 构建消息
    title_emoji = "⚠️" if has_abnormal else "✅"
    title_prefix = "巡检告警" if has_abnormal else "巡检报告"

    lines = [
        f"{title_emoji} **【{title_prefix}】{report.get('target_name', '-')}**",
        "",
        f"📊 **汇总：** 正常 {summary.get('正常', 0)} / 警告 {summary.get('警告', 0)} / 严重 {summary.get('严重', 0)}",
        f"🕐 **时间：** {report.get('finished_at', '-')[:19].replace('T', ' ')}",
        f"🎯 **目标：** {report.get('target_name', '-')} ｜ {report.get('suite', '-')}巡检",
        "",
        f"📊 总项数：{summary.get('total', 0)}",
    ]

    # 提取异常项
    critical_items = []  # 严重
    warning_items = []   # 警告

    for mod in report.get("modules", []):
        mod_name = mod.get("name", "未知模块")
        for item in mod.get("items", []):
            if item.get("status") == "严重":
                critical_items.append({
                    "module": mod_name,
                    "name": item.get("name", "-"),
                    "value": item.get("value", "-"),
                    "threshold": item.get("threshold") or "-"
                })
            elif item.get("status") == "警告":
                warning_items.append({
                    "module": mod_name,
                    "name": item.get("name", "-"),
                    "value": item.get("value", "-"),
                    "threshold": item.get("threshold") or "-"
                })

    # 严重项（最多 5 个）
    if critical_items:
        lines.append("")
        lines.append(f"🔴 **严重（{len(critical_items)}项）：**")
        for item in critical_items[:5]:
            lines.append(f"- {item['module']}：{item['name']} = {item['value']}（阈值：{item['threshold']}）")
        if len(critical_items) > 5:
            lines.append(f"- ... 还有 {len(critical_items) - 5} 项")

    # 警告项（最多 3 个）
    if warning_items:
        lines.append("")
        lines.append(f"🟡 **警告（{len(warning_items)}项）：**")
        for item in warning_items[:3]:
            lines.append(f"- {item['module']}：{item['name']} = {item['value']}（阈值：{item['threshold']}）")
        if len(warning_items) > 3:
            lines.append(f"- ... 还有 {len(warning_items) - 3} 项")

    # 报告链接
    if trigger_cfg.get("include_link") and report_url:
        lines.append("")
        lines.append(f"📎 [查看完整报告]({report_url})")

    content = {
        "msgtype": "markdown",
        "markdown": {
            "title": f"{title_prefix} - {report.get('target_name', '-')}",
            "text": "\n".join(lines)
        }
    }

    if at_mobiles:
        content["at"] = {"atMobiles": at_mobiles, "isAtAll": False}

    # 签名
    url = webhook_url
    if secret:
        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).decode())
        url = f"{webhook_url}&timestamp={timestamp}&sign={sign}"

    try:
        resp = requests.post(url, json=content, timeout=10)
        result = resp.json()
        if result.get("errcode") == 0:
            logger.info("钉钉通知已发送")
            return True
        else:
            logger.error("钉钉通知失败: %s", result.get("errmsg"))
            return False
    except Exception as e:
        logger.error("钉钉通知发送失败: %s", e)
        return False


def _send_wecom_notification(report: dict, report_url: str, config: dict) -> bool:
    """发送企业微信通知。"""
    import requests

    wecom_cfg = config.get("wecom", {})
    trigger_cfg = config.get("trigger", {})

    webhook = wecom_cfg.get("webhook", "").strip()

    if not webhook:
        return False

    summary = report.get("summary", {})
    status_emoji = "✅" if summary.get("警告", 0) == 0 and summary.get("严重", 0) == 0 else "⚠️"

    text_lines = [
        f"{status_emoji} 【巡检报告】{report.get('target_name', '-')}",
        f"套件：{'快速巡检' if report.get('suite') == 'quick' else '全量巡检'}",
        f"时间：{report.get('finished_at', '-')}",
        f"结果：正常 {summary.get('正常', 0)} / 警告 {summary.get('警告', 0)} / 严重 {summary.get('严重', 0)}",
    ]

    if trigger_cfg.get("include_link") and report_url:
        text_lines.append(f"链接：{report_url}")

    content = {
        "msgtype": "text",
        "text": {"content": "\n".join(text_lines)}
    }

    try:
        resp = requests.post(webhook, json=content, timeout=10)
        result = resp.json()
        if result.get("errcode") == 0:
            logger.info("企业微信通知已发送")
            return True
        else:
            logger.error("企业微信通知失败: %s", result.get("errmsg"))
            return False
    except Exception as e:
        logger.error("企业微信通知发送失败: %s", e)
        return False
