# -*- coding: utf-8 -*-
"""
runner_service.py — 巡检执行编排

职责：
  - 选择执行器（local/SSH）
  - 渲染 config、上传资产、逐模块执行
  - 生成 SSE 事件流（progress / module_done / finished / error）
  - 支持取消运行

运行模型：每次 run_id 独立工作目录与线程，结果累积后落盘。
"""

from __future__ import annotations

import hashlib
import hmac
import logging
import threading
import time
import traceback
import urllib.parse
import uuid
from datetime import datetime
from typing import Dict, Optional

from ..utils.check_modules import get_suite, render_config, CheckModule
from ..utils.parser import parse_module_output, summarize
from ..utils.executor import BaseExecutor
from ..services import target_service, report_service

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

# 运行中任务注册表（支持取消）
_runs: Dict[str, dict] = {}
_runs_lock = threading.Lock()


def _now() -> str:
    return datetime.now().strftime("%Y-%m-%dT%H:%M:%S")


def _register_run(run_id: str, target_id: str, suite: str, total: int) -> None:
    with _runs_lock:
        _runs[run_id] = {
            "target_id": target_id,
            "suite": suite,
            "total": total,
            "done": 0,
            "current": "",
            "cancel": threading.Event(),
            "finished": False,
            "error": None,
        }


def _update_run(run_id: str, **kwargs) -> None:
    with _runs_lock:
        if run_id in _runs:
            _runs[run_id].update(kwargs)


def _pop_run(run_id: str) -> Optional[dict]:
    with _runs_lock:
        return _runs.get(run_id)


def cancel_run(run_id: str) -> bool:
    """请求取消运行。"""
    with _runs_lock:
        run = _runs.get(run_id)
    if not run:
        return False
    run["cancel"].set()
    logger.info("请求取消运行: %s", run_id)
    return True


def get_run_status(run_id: str) -> Optional[dict]:
    """获取运行状态（供轮询/SSE 心跳）。"""
    with _runs_lock:
        run = _runs.get(run_id)
    if not run:
        return None
    return {
        "done": run["done"],
        "total": run["total"],
        "current": run["current"],
        "finished": run["finished"],
        "error": run["error"],
    }


def get_running_for_target(target_id: str) -> Optional[dict]:
    """获取指定目标正在运行的巡检任务（如有）。

    用于防重复触发：同一目标已有未完成的巡检时返回该运行记录，
    前端据此决定是否阻止启动新巡检。
    """
    with _runs_lock:
        for run_id, run in _runs.items():
            if (run.get("target_id") == target_id
                    and not run.get("finished")):
                return {
                    "run_id": run_id,
                    "target_id": run.get("target_id"),
                    "suite": run.get("suite"),
                    "done": run.get("done", 0),
                    "total": run.get("total", 0),
                    "current": run.get("current", ""),
                }
    return None


def run_inspection(target_id: str, suite: str):
    """巡检生成器：yield SSE 事件 dict。

    事件格式：
      {"event": "progress", "run_id", "done", "total", "current"}
      {"event": "module_done", "run_id", "module", "items_count", "summary"}
      {"event": "finished", "run_id", "report_id", "summary"}
      {"event": "error", "run_id", "message"}
      {"event": "cancelled", "run_id"}
    """
    from datetime import datetime as _dt
    # 使用 uuid 避免 run_id 冲突（同一秒多请求）
    run_id = _dt.now().strftime("%Y%m%d_%H%M%S_") + str(uuid.uuid4())[:6]

    target = target_service.get_target(target_id)
    if not target:
        yield {"event": "error", "run_id": run_id, "message": "监测目标不存在"}
        return

    modules = get_suite(suite)
    if not modules:
        yield {"event": "error", "run_id": run_id, "message": f"套件 {suite} 无可用模块"}
        return

    _register_run(run_id, target_id, suite, len(modules))
    started_at = _now()

    # 准备 config（含解密后的凭据）
    target_for_render = dict(target)
    target_for_render["credential_overrides"] = target_service.resolve_credentials(target)
    config_text = render_config(target_for_render)

    executor: Optional[BaseExecutor] = None
    modules_result = []
    try:
        executor = target_service.make_executor(target, run_id)

        # 连通性检查（远程）
        if not executor.is_local:
            yield {"event": "progress", "run_id": run_id, "done": 0, "total": len(modules),
                   "current": "测试SSH连接..."}
            if not executor.test_connection():
                yield {"event": "error", "run_id": run_id, "message": "SSH 连接失败，请检查目标配置"}
                _update_run(run_id, finished=True, error="SSH 连接失败")
                return
            yield {"event": "progress", "run_id": run_id, "done": 0, "total": len(modules),
                   "current": "上传检测模块..."}

        executor.setup()
        executor.upload_assets()

        for idx, module in enumerate(modules):
            # 检查取消
            run = _pop_run(run_id)
            if run and run["cancel"].is_set():
                yield {"event": "cancelled", "run_id": run_id}
                _update_run(run_id, finished=True)
                return

            _update_run(run_id, current=module.name, done=idx)
            yield {"event": "progress", "run_id": run_id, "done": idx,
                   "total": len(modules), "current": module.name}

            raw = executor.run_module(module, config_text, timeout=90)
            items = parse_module_output(raw, module.id, module.category)
            summary = summarize(items)

            module_result = {
                "id": module.id,
                "name": module.name,
                "category": module.category,
                "items": [{"key": it.key, "name": it.name, "value": it.value,
                           "threshold": it.threshold, "status": it.status}
                          for it in items],
                "summary": summary,
            }
            modules_result.append(module_result)
            _update_run(run_id, done=idx + 1)

            yield {"event": "module_done", "run_id": run_id,
                   "module": module.name, "items_count": len(items), "summary": summary}

        report_id = report_service.save(target, suite, modules_result, started_at, _now())
        _update_run(run_id, finished=True)

        # 汇总
        total_summary = {k: 0 for k in ("正常", "警告", "严重", "total")}
        for m in modules_result:
            ms = m["summary"]
            for k in total_summary:
                total_summary[k] += ms.get(k, 0)

        yield {"event": "finished", "run_id": run_id, "report_id": report_id,
               "summary": total_summary}

    except Exception as e:
        logger.error("巡检执行异常: %s\n%s", e, traceback.format_exc())
        _update_run(run_id, finished=True, error=str(e))
        yield {"event": "error", "run_id": run_id, "message": f"巡检执行异常: {e}"}
    finally:
        if executor:
            try:
                executor.cleanup()
            except Exception as e:
                logger.warning("执行器清理失败: %s", e)
        # 延迟移除运行记录，给客户端最后取状态的机会
        time.sleep(0)


def run_inspection_sync(target_id: str, suite: str) -> dict:
    """同步执行巡检（定时任务调用），返回结果摘要。

    与 run_inspection() 生成器共享核心逻辑，但不 yield SSE 事件。
    返回: {"success": bool, "report_id": str, "summary": dict, "finished_at": str}
    """
    from datetime import datetime as _dt
    # 使用 target_id + uuid 避免 run_id 冲突（多个定时任务同一秒触发）
    run_id = _dt.now().strftime("%Y%m%d_%H%M%S_") + f"{target_id[:4]}_" + str(uuid.uuid4())[:6]

    target = target_service.get_target(target_id)
    if not target:
        return {"success": False, "report_id": "", "summary": {}, "finished_at": _now()}

    modules = get_suite(suite)
    if not modules:
        return {"success": False, "report_id": "", "summary": {}, "finished_at": _now()}

    _register_run(run_id, target_id, suite, len(modules))
    started_at = _now()

    # 准备 config（含解密后的凭据）
    target_for_render = dict(target)
    target_for_render["credential_overrides"] = target_service.resolve_credentials(target)
    config_text = render_config(target_for_render)

    executor = None
    modules_result = []
    try:
        executor = target_service.make_executor(target, run_id)

        # 连通性检查（远程）
        if not executor.is_local:
            if not executor.test_connection():
                _update_run(run_id, finished=True, error="SSH 连接失败")
                return {"success": False, "report_id": "", "summary": {}, "finished_at": _now()}

        executor.setup()
        executor.upload_assets()

        for idx, module in enumerate(modules):
            # 检查取消
            run = _pop_run(run_id)
            if run and run["cancel"].is_set():
                _update_run(run_id, finished=True)
                return {"success": False, "report_id": "", "summary": {}, "finished_at": _now()}

            _update_run(run_id, current=module.name, done=idx)
            raw = executor.run_module(module, config_text, timeout=90)
            items = parse_module_output(raw, module.id, module.category)
            summary = summarize(items)

            module_result = {
                "id": module.id,
                "name": module.name,
                "category": module.category,
                "items": [{"key": it.key, "name": it.name, "value": it.value,
                           "threshold": it.threshold, "status": it.status}
                          for it in items],
                "summary": summary,
            }
            modules_result.append(module_result)
            _update_run(run_id, done=idx + 1)

        report_id = report_service.save(target, suite, modules_result, started_at, _now())
        _update_run(run_id, finished=True)

        # 汇总
        total_summary = {k: 0 for k in ("正常", "警告", "严重", "total")}
        for m in modules_result:
            ms = m["summary"]
            for k in total_summary:
                total_summary[k] += ms.get(k, 0)

        # 发送通知（定时任务触发的巡检）
        try:
            from . import notification_service
            report = report_service.get_report(report_id)
            if report:
                # 构建报告链接（需要从配置获取外部访问地址）
                import os
                host = os.environ.get('EXTERNAL_HOST', 'http://192.168.5.60:8088')
                report_url = f"{host}/service-monitor/report/{report_id}"
                # 拼接访问 token，实现免登查看
                if report.get("access_token"):
                    report_url = f"{report_url}?token={report['access_token']}"
                notification_service.send_notification(report, report_url)

                # 连续异常告警检查
                try:
                    alert_items = report_service.check_consecutive_abnormal(target_id, report)
                    if alert_items:
                        _send_consecutive_alert(target, alert_items, report_url, report)
                except Exception as e:
                    logger.warning("连续异常告警检查失败: %s", e)
        except Exception as e:
            logger.warning("发送通知失败: %s", e)

        return {
            "success": True,
            "report_id": report_id,
            "summary": total_summary,
            "finished_at": _now(),
        }

    except Exception as e:
        logger.error("同步巡检执行异常: %s\n%s", e, traceback.format_exc())
        _update_run(run_id, finished=True, error=str(e))
        return {"success": False, "report_id": "", "summary": {}, "finished_at": _now()}
    finally:
        if executor:
            try:
                executor.cleanup()
            except Exception as e:
                logger.warning("执行器清理失败: %s", e)


def _send_consecutive_alert(target: dict, alert_items: list,
                            report_url: str, report: dict) -> None:
    """发送连续异常告警通知。"""
    from . import notification_service
    config = notification_service.get_config()
    alert_config = config.get("alert_on_consecutive", {})
    channels = alert_config.get("channels", [])

    target_name = target.get("name", "未知目标")

    # 构建告警消息
    for item in alert_items:
        key = item.get("key", "")
        name = item.get("name", key)
        count = item.get("consecutive_count", 3)
        value = item.get("value", "-")
        status = item.get("status", "-")
        first_seen = item.get("first_seen", "-")
        if first_seen:
            first_seen = first_seen.replace("T", " ")

        # Markdown 消息（钉钉/企微）
        md_msg = (
            f"### ⚠️ 连续异常告警\n\n"
            f"**目标**：{target_name}\n\n"
            f"**检测项**：{key} ({name})\n\n"
            f"**连续异常**：{count} 次\n\n"
            f"**当前值**：{value}\n\n"
            f"**首次异常**：{first_seen}\n\n"
            f"[查看详情]({report_url})"
        )

        # 发送邮件
        if "email" in channels and config.get("email", {}).get("enabled"):
            try:
                _send_alert_email(config, target_name, name, count, value,
                                  status, first_seen, report_url)
            except Exception as e:
                logger.warning("连续异常告警邮件发送失败: %s", e)

        # 发送钉钉
        if "dingtalk" in channels and config.get("dingtalk", {}).get("enabled"):
            try:
                _send_alert_dingtalk(config, md_msg, target_name)
            except Exception as e:
                logger.warning("连续异常告警钉钉发送失败: %s", e)

        # 发送企微
        if "wecom" in channels and config.get("wecom", {}).get("enabled"):
            try:
                _send_alert_wecom(config, md_msg)
            except Exception as e:
                logger.warning("连续异常告警企微发送失败: %s", e)

    logger.info("连续异常告警已发送: %s, %d 项", target_name, len(alert_items))


def _send_alert_email(config: dict, target_name: str, item_name: str,
                      count: int, value: str, status: str,
                      first_seen: str, report_url: str) -> None:
    """发送连续异常告警邮件。"""
    import smtplib
    from email.mime.multipart import MIMEMultipart
    from email.mime.text import MIMEText

    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 not all([smtp_host, smtp_user, smtp_password, recipients]):
        return

    subject = f"【连续异常告警】{target_name} - {item_name} 连续 {count} 次异常"

    body = (
        f"<h2>⚠️ 连续异常告警</h2>"
        f"<p><b>目标：</b>{target_name}</p>"
        f"<p><b>检测项：</b>{item_name}</p>"
        f"<p><b>连续异常次数：</b>{count}</p>"
        f"<p><b>当前值：</b>{value}</p>"
        f"<p><b>当前状态：</b>{status}</p>"
        f"<p><b>首次异常：</b>{first_seen}</p>"
        f"<hr><p><a href='{report_url}'>查看详情</a></p>"
        f"<p style='color:#666;font-size:12px;'>此邮件由服务监测模块自动发送</p>"
    )

    msg = MIMEMultipart()
    msg["From"] = smtp_user
    msg["To"] = ", ".join(recipients)
    msg["Subject"] = subject
    msg.attach(MIMEText(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()


def _send_alert_dingtalk(config: dict, md_msg: str, target_name: str) -> None:
    """发送连续异常告警钉钉消息。"""
    import requests
    import base64

    ding_cfg = config.get("dingtalk", {})
    webhook_url = ding_cfg.get("webhook_url", "").strip()
    secret = ding_cfg.get("secret", "").strip()

    if not webhook_url:
        return

    content = {
        "msgtype": "markdown",
        "markdown": {
            "title": f"连续异常告警 - {target_name}",
            "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)
    if resp.status_code == 200:
        result = resp.json()
        if result.get("errcode") != 0:
            logger.warning("钉钉告警发送失败: %s", result.get("errmsg"))
    else:
        logger.warning("钉钉告警请求失败: HTTP %d", resp.status_code)


def _send_alert_wecom(config: dict, md_msg: str) -> None:
    """发送连续异常告警企微消息。"""
    import requests

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

    if not webhook:
        return

    content = {
        "msgtype": "markdown",
        "markdown": {"content": md_msg}
    }

    resp = requests.post(webhook, json=content, timeout=10)
    if resp.status_code == 200:
        result = resp.json()
        if result.get("errcode") != 0:
            logger.warning("企微告警发送失败: %s", result.get("errmsg"))
    else:
        logger.warning("企微告警请求失败: HTTP %d", resp.status_code)
