# -*- coding: utf-8 -*-
"""
report_service.py — 巡检报告管理

报告存储：service_monitor/data/reports/{report_id}.json
提供：保存 / 读取 / 列表 / 导出(MD/JSON) / 过期清理
"""

from __future__ import annotations

import json
import logging
import secrets
import uuid
from datetime import datetime, timedelta
from typing import List, Optional

try:
    from pytz import timezone as pytz_timezone
    SHANGHAI_TZ = pytz_timezone("Asia/Shanghai")
except ImportError:
    SHANGHAI_TZ = None

from ..utils.paths import REPORTS_DIR, ensure_dirs

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

# 报告默认保留天数（可被配置覆盖）
DEFAULT_RETENTION_DAYS = 14

# Token 访问有效期（天）
ACCESS_TOKEN_DAYS = 7

NORMAL, WARNING, CRITICAL = "正常", "警告", "严重"


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


def _report_path(report_id: str):
    return REPORTS_DIR / f"{report_id}.json"


def save(target: dict, suite: str, modules_result: list,
         started_at: str, finished_at: Optional[str] = None) -> str:
    """保存巡检报告，返回 report_id。

    modules_result: [
      {"id","name","category","items":[{key,name,value,threshold,status}], "summary":{...}}
    ]
    """
    ensure_dirs()
    report_id = datetime.now().strftime("%Y%m%d_%H%M%S_") + str(uuid.uuid4())[:6]

    # 汇总
    total = {NORMAL: 0, WARNING: 0, CRITICAL: 0, "total": 0}
    for m in modules_result:
        for it in m.get("items", []):
            total["total"] += 1
            st = it.get("status", NORMAL)
            if st in total:
                total[st] += 1

    # Token 过期时间
    now = datetime.now(SHANGHAI_TZ) if SHANGHAI_TZ else datetime.now()
    expires_at = (now + timedelta(days=ACCESS_TOKEN_DAYS)).isoformat()

    report = {
        "id": report_id,
        "target_id": target.get("id"),
        "target_name": target.get("name"),
        "target_type": target.get("type"),
        "suite": suite,
        "started_at": started_at,
        "finished_at": finished_at or _now(),
        "summary": total,
        "modules": modules_result,
        "access_token": f"rpt_{secrets.token_hex(32)}",
        "access_token_expires_at": expires_at,
    }

    _report_path(report_id).write_text(
        json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8"
    )
    logger.info("保存报告: %s (目标=%s, 套件=%s, 项=%d)",
                report_id, target.get("name"), suite, total["total"])
    return report_id


def get_report(report_id: str) -> Optional[dict]:
    """读取报告。"""
    p = _report_path(report_id)
    if not p.exists():
        return None
    try:
        return json.loads(p.read_text(encoding="utf-8"))
    except (json.JSONDecodeError, OSError) as e:
        logger.error("读取报告失败 %s: %s", report_id, e)
        return None


def list_reports(target_id: Optional[str] = None, limit: int = 50) -> List[dict]:
    """列出报告摘要（按时间倒序）。"""
    ensure_dirs()
    summaries = []
    for p in sorted(REPORTS_DIR.glob("*.json"), reverse=True):
        try:
            r = json.loads(p.read_text(encoding="utf-8"))
        except (json.JSONDecodeError, OSError):
            continue
        if target_id and r.get("target_id") != target_id:
            continue
        summaries.append({
            "id": r.get("id"),
            "target_id": r.get("target_id"),
            "target_name": r.get("target_name"),
            "target_type": r.get("target_type"),
            "suite": r.get("suite"),
            "started_at": r.get("started_at"),
            "finished_at": r.get("finished_at"),
            "summary": r.get("summary", {}),
        })
        if len(summaries) >= limit:
            break
    return summaries


def delete_report(report_id: str) -> bool:
    """删除单份报告。"""
    p = _report_path(report_id)
    if p.exists():
        p.unlink()
        logger.info("删除报告: %s", report_id)
        return True
    return False


def cleanup_expired(retention_days: int = DEFAULT_RETENTION_DAYS) -> int:
    """清理过期报告，返回清理数量。"""
    ensure_dirs()
    cutoff = datetime.now() - timedelta(days=retention_days)
    removed = 0
    for p in REPORTS_DIR.glob("*.json"):
        try:
            r = json.loads(p.read_text(encoding="utf-8"))
            finished = r.get("finished_at") or r.get("started_at")
            if finished:
                dt = datetime.strptime(finished[:19], "%Y-%m-%dT%H:%M:%S")
                if dt < cutoff:
                    p.unlink()
                    removed += 1
        except (json.JSONDecodeError, OSError, ValueError):
            continue
    if removed:
        logger.info("清理过期报告 %d 份（保留 %d 天）", removed, retention_days)
    return removed


# ============================================================
# 导出
# ============================================================

def export_json(report_id: str) -> Optional[dict]:
    """导出 JSON（即报告本身）。"""
    return get_report(report_id)


def export_markdown(report_id: str) -> Optional[str]:
    """导出 Markdown 报告。"""
    r = get_report(report_id)
    if not r:
        return None

    s = r.get("summary", {})
    lines = [
        f"# 服务巡检报告",
        "",
        f"- **目标**：{r.get('target_name')}（{r.get('target_type')}）",
        f"- **套件**：{'快速巡检' if r.get('suite') == 'quick' else '全量巡检'}",
        f"- **开始时间**：{r.get('started_at')}",
        f"- **完成时间**：{r.get('finished_at')}",
        "",
        f"## 汇总",
        "",
        f"| 总项数 | 正常 | 警告 | 严重 |",
        f"|--------|------|------|------|",
        f"| {s.get('total', 0)} | {s.get(NORMAL, 0)} | {s.get(WARNING, 0)} | {s.get(CRITICAL, 0)} |",
        "",
    ]

    # 异常项置顶
    abnormal = []
    for m in r.get("modules", []):
        for it in m.get("items", []):
            if it.get("status") in (WARNING, CRITICAL):
                abnormal.append((m.get("name"), it))
    if abnormal:
        lines += ["## ⚠️ 异常项", "", "| 模块 | 检测项 | 当前值 | 阈值 | 状态 |",
                  "|------|--------|--------|------|------|"]
        for mod_name, it in abnormal:
            lines.append(
                f"| {mod_name} | {it.get('name')} | {it.get('value')} | "
                f"{it.get('threshold') or '-'} | {it.get('status')} |"
            )
        lines.append("")

    # 各模块明细
    lines += ["## 检测明细", ""]
    for m in r.get("modules", []):
        lines += [f"### {m.get('name')}", "",
                  "| 检测项 | 当前值 | 阈值 | 状态 |",
                  "|--------|--------|------|------|"]
        for it in m.get("items", []):
            lines.append(
                f"| {it.get('name')} | {it.get('value')} | "
                f"{it.get('threshold') or '-'} | {it.get('status')} |"
            )
        lines.append("")

    return "\n".join(lines)


# ============================================================
# Token 验证
# ============================================================

def validate_access_token(report_id: str, token: str) -> bool:
    """验证报告访问 token 是否有效。

    Args:
        report_id: 报告 ID
        token: 访问 token（rpt_xxx 格式）

    Returns:
        True=token 有效，False=无效或过期
    """
    if not token or not token.startswith("rpt_"):
        return False

    report = get_report(report_id)
    if not report:
        return False

    stored_token = report.get("access_token", "")
    if not stored_token or stored_token != token:
        return False

    expires_at_str = report.get("access_token_expires_at", "")
    if not expires_at_str:
        return False

    try:
        expires_at = datetime.fromisoformat(expires_at_str)
        now = datetime.now(SHANGHAI_TZ) if SHANGHAI_TZ else datetime.now()
        # 如果 expires_at 无时区信息，假设为本地时间
        if expires_at.tzinfo is None and now.tzinfo is not None:
            expires_at = expires_at.replace(tzinfo=now.tzinfo)
        elif expires_at.tzinfo is not None and now.tzinfo is None:
            now = now.replace(tzinfo=expires_at.tzinfo)
        if now > expires_at:
            return False
    except (ValueError, TypeError):
        return False

    return True


# ============================================================
# 连续异常告警
# ============================================================

from typing import List as _List

# 冷却期记录：{(target_id, item_key): last_alert_time}
_ALERT_COOLDOWN: Dict[str, str] = {}


def check_consecutive_abnormal(target_id: str, current_report: dict) -> _List[dict]:
    """检查连续异常检测项，返回需告警列表。

    Args:
        target_id: 目标 ID
        current_report: 当前报告数据

    Returns:
        连续异常检测项列表 [
            {
                "key": "CPU_USAGE",
                "name": "CPU使用率",
                "consecutive_count": 3,
                "first_seen": "2026-07-21T10:00:00",
                "last_value": "78%",
                "last_status": "warning"
            }
        ]
    """
    # 获取告警配置
    from . import notification_service
    config = notification_service.get_config()
    alert_config = config.get("alert_on_consecutive", {})

    if not alert_config.get("enabled"):
        return []

    threshold = alert_config.get("consecutive_threshold", 3)
    alert_level = alert_config.get("alert_level", "critical")  # "critical" or "all"

    # 提取当前报告异常项
    current_abnormal = _get_abnormal_items(current_report, alert_level)
    if not current_abnormal:
        return []

    # 查询最近 N-1 份报告
    recent_reports = list_reports(target_id=target_id, limit=threshold)
    # 排除当前报告
    current_id = current_report.get("id", "")
    recent_reports = [r for r in recent_reports if r.get("id") != current_id]

    if len(recent_reports) < threshold - 1:
        # 历史报告不足，无法判断连续性
        return []

    # 检查连续性
    alert_items = []
    for item in current_abnormal:
        key = item.get("key", "")
        if _is_consecutive_abnormal(key, recent_reports, threshold - 1, alert_level):
            # 检查冷却期
            cooldown_key = f"{target_id}:{key}"
            if not _in_cooldown(cooldown_key):
                # 查找首次异常时间
                first_seen = _find_first_abnormal_time(key, recent_reports, threshold - 1)
                item["consecutive_count"] = threshold
                item["first_seen"] = first_seen
                alert_items.append(item)
                _set_cooldown(cooldown_key)

    return alert_items


def _get_abnormal_items(report: dict, alert_level: str) -> _List[dict]:
    """提取报告中的异常检测项。"""
    items = []
    for mod in report.get("modules", []):
        for it in mod.get("items", []):
            status = it.get("status", "")
            if alert_level == "critical" and status == CRITICAL:
                items.append({
                    "key": it.get("key", ""),
                    "name": it.get("name", ""),
                    "value": it.get("value", ""),
                    "status": status,
                    "module_name": mod.get("name", ""),
                })
            elif alert_level == "all" and status in (WARNING, CRITICAL):
                items.append({
                    "key": it.get("key", ""),
                    "name": it.get("name", ""),
                    "value": it.get("value", ""),
                    "status": status,
                    "module_name": mod.get("name", ""),
                })
    return items


def _is_consecutive_abnormal(item_key: str, recent_reports: _List[dict],
                              required_count: int, alert_level: str) -> bool:
    """检查检测项是否连续异常。"""
    count = 0
    for report_summary in recent_reports[:required_count]:
        report_id = report_summary.get("id", "")
        if not report_id:
            continue
        report = get_report(report_id)
        if not report:
            continue
        # 检查该检测项是否异常
        abnormal_items = _get_abnormal_items(report, alert_level)
        if any(it.get("key") == item_key for it in abnormal_items):
            count += 1
        else:
            break  # 非连续
    return count >= required_count


def _find_first_abnormal_time(item_key: str, recent_reports: _List[dict],
                               count: int) -> str:
    """查找首次异常时间（从最近第 N 份报告）。"""
    for report_summary in reversed(recent_reports[:count]):
        report_id = report_summary.get("id", "")
        if not report_id:
            continue
        report = get_report(report_id)
        if not report:
            continue
        abnormal_items = _get_abnormal_items(report, "all")  # 任意级别
        if any(it.get("key") == item_key for it in abnormal_items):
            return report.get("finished_at") or report.get("started_at", "")
    return ""


def _in_cooldown(cooldown_key: str) -> bool:
    """检查是否在冷却期内。"""
    if cooldown_key not in _ALERT_COOLDOWN:
        return False
    last_alert = _ALERT_COOLDOWN[cooldown_key]
    # 冷却期 24 小时
    try:
        from datetime import timedelta
        last_dt = datetime.strptime(last_alert[:19], "%Y-%m-%dT%H:%M:%S")
        if datetime.now() - last_dt < timedelta(hours=24):
            return True
    except (ValueError, TypeError):
        pass
    return False


def _set_cooldown(cooldown_key: str) -> None:
    """设置冷却期。"""
    _ALERT_COOLDOWN[cooldown_key] = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
