"""MoviePilot 活动总结插件 — 定时发送每日/每周/每月活动总结通知"""

import os
from collections import OrderedDict
from dataclasses import dataclass
from typing import Any, List, Dict, Tuple, Optional
from datetime import datetime, timedelta

import pytz
from apscheduler.triggers.cron import CronTrigger

from app.log import logger
from app.plugins import _PluginBase
from app.schemas import NotificationType
from app.core.event import eventmanager, Event
from app.schemas.types import EventType
from app.core.config import settings
from app.db.transferhistory_oper import TransferHistoryOper
from app.db.subscribe_oper import SubscribeOper
from app.db.plugindata_oper import PluginDataOper
from app.db.models.siteuserdata import SiteUserData
from app.db import ScopedSession


# ─── 模块注册表：key → 中文名，各报告按需选取 ───

MODULES = OrderedDict([
    ("download",     "下载记录"),
    ("transfer",     "入库记录"),
    ("signin",       "签到状态"),
    ("brush",        "刷流统计"),
    ("downloader",   "下载器概览"),
    ("site_delta",   "站点增量"),
    ("site_current", "站点快照"),
    ("subscribe",    "订阅进度"),
    ("storage",      "存储空间"),
])

MODULE_OPTIONS = [{"title": name, "value": key} for key, name in MODULES.items()]

# 各报告类型的默认模块
DEFAULT_DAILY_MODULES = ["download", "transfer", "signin", "brush", "downloader", "site_delta"]
DEFAULT_WEEKLY_MODULES = ["download", "transfer", "subscribe", "site_delta", "brush"]
DEFAULT_MONTHLY_MODULES = [
    "download", "transfer", "subscribe", "site_current",
    "site_delta", "storage", "brush", "downloader",
]

WEEKDAY_NAMES = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]

# 历史记录上限
MAX_HISTORY = 100


@dataclass
class TimeRange:
    """报告的时间范围"""
    start: datetime
    end: datetime
    start_str: str       # "YYYY-MM-DD HH:MM:SS" — 用于数据库查询
    start_date: str      # "YYYY-MM-DD"
    end_date: str        # "YYYY-MM-DD"
    report_type: str     # "daily" / "weekly" / "monthly"
    prefix: str          # "今日" / "本周" / "本月"


class DailySummary(_PluginBase):
    plugin_name = "活动总结"
    plugin_desc = "定时发送每日/每周/每月活动总结通知，支持自定义报告模块、历史记录查看"
    plugin_icon = "Bark_A.png"
    plugin_version = "2.0.0"
    plugin_author = "yuhoye"
    author_url = "https://github.com/yuhoye"
    plugin_config_prefix = "dailysummary_"
    plugin_order = 30
    auth_level = 1

    # ─── 配置字段 ───

    _enabled: bool = False
    _notify: bool = True
    _daily_cron: str = "0 23 * * *"
    _weekly_cron: str = "0 23 * * 1"
    _monthly_cron: str = "0 23 1 * *"
    _onlyonce: bool = False
    _test_type: str = "daily"

    _daily_modules: list = None
    _weekly_modules: list = None
    _monthly_modules: list = None

    def init_plugin(self, config: dict = None):
        if config:
            self._enabled = config.get("enabled", False)
            self._notify = config.get("notify", True)
            self._daily_cron = config.get("daily_cron", "0 23 * * *")
            self._weekly_cron = config.get("weekly_cron", "0 23 * * 1")
            self._monthly_cron = config.get("monthly_cron", "0 23 1 * *")
            self._onlyonce = config.get("onlyonce", False)
            self._test_type = config.get("test_type", "daily")
            self._daily_modules = config.get("daily_modules") or DEFAULT_DAILY_MODULES
            self._weekly_modules = config.get("weekly_modules") or DEFAULT_WEEKLY_MODULES
            self._monthly_modules = config.get("monthly_modules") or DEFAULT_MONTHLY_MODULES
        else:
            self._daily_modules = DEFAULT_DAILY_MODULES
            self._weekly_modules = DEFAULT_WEEKLY_MODULES
            self._monthly_modules = DEFAULT_MONTHLY_MODULES

        if self._onlyonce:
            self._onlyonce = False
            self._save_config()
            from apscheduler.schedulers.background import BackgroundScheduler
            scheduler = BackgroundScheduler(timezone=settings.TZ)
            test_func = {
                "daily": self.send_daily,
                "weekly": self.send_weekly,
                "monthly": self.send_monthly,
            }.get(self._test_type, self.send_daily)
            scheduler.add_job(
                func=test_func,
                trigger="date",
                run_date=datetime.now(tz=pytz.timezone(settings.TZ)) + timedelta(seconds=3),
                name="立即测试",
            )
            scheduler.start()

    def _save_config(self):
        self.update_config({
            "enabled": self._enabled,
            "notify": self._notify,
            "daily_cron": self._daily_cron,
            "weekly_cron": self._weekly_cron,
            "monthly_cron": self._monthly_cron,
            "onlyonce": False,
            "test_type": self._test_type,
            "daily_modules": self._daily_modules,
            "weekly_modules": self._weekly_modules,
            "monthly_modules": self._monthly_modules,
        })

    def get_state(self) -> bool:
        return self._enabled

    @staticmethod
    def get_command() -> List[Dict[str, Any]]:
        return [
            {"cmd": "/daily_summary", "event": EventType.PluginAction, "desc": "发送每日总结", "category": "工具", "data": {"action": "daily_summary"}},
            {"cmd": "/weekly_summary", "event": EventType.PluginAction, "desc": "发送每周总结", "category": "工具", "data": {"action": "weekly_summary"}},
            {"cmd": "/monthly_summary", "event": EventType.PluginAction, "desc": "发送每月总结", "category": "工具", "data": {"action": "monthly_summary"}},
        ]

    def get_api(self) -> List[Dict[str, Any]]:
        return [{
            "path": "/clear_history",
            "endpoint": self._api_clear_history,
            "methods": ["POST"],
            "summary": "清除历史记录",
        }]

    def _api_clear_history(self) -> dict:
        self.save_data("history", [])
        logger.info("[DailySummary] 历史记录已清除")
        return {"success": True}

    def get_service(self) -> List[Dict[str, Any]]:
        if not self._enabled:
            return []
        services = []
        if self._daily_cron:
            services.append({
                "id": "DailySummary_daily",
                "name": "每日总结",
                "trigger": CronTrigger.from_crontab(self._daily_cron),
                "func": self.send_daily,
                "kwargs": {},
            })
        if self._weekly_cron:
            services.append({
                "id": "DailySummary_weekly",
                "name": "每周总结",
                "trigger": CronTrigger.from_crontab(self._weekly_cron),
                "func": self.send_weekly,
                "kwargs": {},
            })
        if self._monthly_cron:
            services.append({
                "id": "DailySummary_monthly",
                "name": "每月总结",
                "trigger": CronTrigger.from_crontab(self._monthly_cron),
                "func": self.send_monthly,
                "kwargs": {},
            })
        return services

    # ─── 配置表单：三 Tab 布局 ───

    def get_form(self) -> Tuple[List[dict], Dict[str, Any]]:
        test_options = [
            {"title": "每日总结", "value": "daily"},
            {"title": "每周总结", "value": "weekly"},
            {"title": "每月总结", "value": "monthly"},
        ]
        return [
            {
                "component": "VForm",
                "content": [
                    # ── 基本设置（VTabs 外面，避免弹出菜单被裁剪） ──
                    {
                        'component': 'VRow',
                        'content': [
                            {'component': 'VCol', 'props': {'cols': 12, 'md': 3},
                             'content': [{'component': 'VSwitch', 'props': {'model': 'enabled', 'label': '启用插件'}}]},
                            {'component': 'VCol', 'props': {'cols': 12, 'md': 3},
                             'content': [{'component': 'VSwitch', 'props': {'model': 'notify', 'label': '发送通知'}}]},
                            {'component': 'VCol', 'props': {'cols': 12, 'md': 3},
                             'content': [{'component': 'VSwitch', 'props': {'model': 'onlyonce', 'label': '立即测试一次'}}]},
                            {'component': 'VCol', 'props': {'cols': 12, 'md': 3},
                             'content': [{'component': 'VSelect', 'props': {'model': 'test_type', 'label': '测试类型', 'items': test_options}}]},
                        ],
                    },
                    {
                        'component': 'VRow',
                        'content': [
                            {'component': 'VCol', 'props': {'cols': 12, 'md': 4},
                             'content': [{'component': 'VCronField', 'props': {'model': 'daily_cron', 'label': '每日周期', 'placeholder': '5位cron表达式'}}]},
                            {'component': 'VCol', 'props': {'cols': 12, 'md': 4},
                             'content': [{'component': 'VCronField', 'props': {'model': 'weekly_cron', 'label': '每周周期', 'placeholder': '5位cron表达式'}}]},
                            {'component': 'VCol', 'props': {'cols': 12, 'md': 4},
                             'content': [{'component': 'VCronField', 'props': {'model': 'monthly_cron', 'label': '每月周期', 'placeholder': '5位cron表达式'}}]},
                        ],
                    },
                    # ── 报告内容 ──
                    {
                        "component": "VRow",
                        "props": {"style": "margin-top: 8px;"},
                        "content": [
                            {"component": "VCol", "props": {"cols": 12},
                             "content": [{"component": "VAlert", "props": {"type": "info", "variant": "tonal", "text": "选择各报告中包含的信息模块，模块按选择顺序显示在报告中"}}]},
                        ],
                    },
                    {
                        "component": "VRow",
                        "content": [
                            {"component": "VCol", "props": {"cols": 12, "md": 4},
                             "content": [{"component": "VSelect", "props": {
                                 "model": "daily_modules", "label": "日报模块",
                                 "items": MODULE_OPTIONS, "multiple": True, "chips": True, "closable-chips": True,
                             }}]},
                            {"component": "VCol", "props": {"cols": 12, "md": 4},
                             "content": [{"component": "VSelect", "props": {
                                 "model": "weekly_modules", "label": "周报模块",
                                 "items": MODULE_OPTIONS, "multiple": True, "chips": True, "closable-chips": True,
                             }}]},
                            {"component": "VCol", "props": {"cols": 12, "md": 4},
                             "content": [{"component": "VSelect", "props": {
                                 "model": "monthly_modules", "label": "月报模块",
                                 "items": MODULE_OPTIONS, "multiple": True, "chips": True, "closable-chips": True,
                             }}]},
                        ],
                    },
                ],
            }
        ], {
            "enabled": False,
            "notify": True,
            "daily_cron": "0 23 * * *",
            "weekly_cron": "0 23 * * 1",
            "monthly_cron": "0 23 1 * *",
            "onlyonce": False,
            "test_type": "daily",
            "daily_modules": DEFAULT_DAILY_MODULES,
            "weekly_modules": DEFAULT_WEEKLY_MODULES,
            "monthly_modules": DEFAULT_MONTHLY_MODULES,
        }

    # ─── 历史记录页面 ───

    def get_page(self) -> List[dict]:
        history = self.get_data("history") or []

        # 模块配置摘要
        def _module_names(modules):
            return "、".join(MODULES.get(m, m) for m in (modules or []))

        config_cards = [
            self._config_card('📊 日报模块', _module_names(self._daily_modules), self._daily_cron),
            self._config_card('📈 周报模块', _module_names(self._weekly_modules), self._weekly_cron),
            self._config_card('📅 月报模块', _module_names(self._monthly_modules), self._monthly_cron),
        ]

        if not history:
            return [
                {
                    'component': 'VRow',
                    'content': config_cards,
                },
                {
                    'component': 'div',
                    'text': '暂无发送记录',
                    'props': {'class': 'text-center mt-4'},
                },
            ]

        daily_count = sum(1 for r in history if r.get("type") == "daily")
        weekly_count = sum(1 for r in history if r.get("type") == "weekly")
        monthly_count = sum(1 for r in history if r.get("type") == "monthly")

        items = [
            {
                'time': r.get('time', ''),
                'type_label': {'daily': '日报', 'weekly': '周报', 'monthly': '月报'}.get(r.get('type'), ''),
                'title': r.get('title', ''),
                'preview': (r.get('text', '')[:80] + '...') if len(r.get('text', '')) > 80 else r.get('text', ''),
            }
            for r in reversed(history)
        ]

        return [
            {
                'component': 'VRow',
                'content': config_cards + [
                    # 发送统计
                    self._stat_card('日报', f'{daily_count} 份'),
                    self._stat_card('周报', f'{weekly_count} 份'),
                    self._stat_card('月报', f'{monthly_count} 份'),
                    # 历史记录表格
                    {
                        'component': 'VCol',
                        'props': {'cols': 12, 'class': 'd-none d-sm-block'},
                        'content': [
                            {
                                'component': 'VDataTableVirtual',
                                'props': {
                                    'class': 'text-sm',
                                    'headers': [
                                        {'title': '时间', 'key': 'time', 'sortable': True},
                                        {'title': '类型', 'key': 'type_label', 'sortable': True},
                                        {'title': '标题', 'key': 'title', 'sortable': False},
                                        {'title': '预览', 'key': 'preview', 'sortable': False},
                                    ],
                                    'items': items,
                                    'height': '30rem',
                                    'density': 'compact',
                                    'fixed-header': True,
                                    'hide-no-data': True,
                                    'hover': True,
                                },
                            }
                        ],
                    },
                ],
            }
        ]

    @staticmethod
    def _config_card(title: str, modules_text: str, cron: str) -> dict:
        return {
            'component': 'VCol',
            'props': {'cols': 12, 'md': 4},
            'content': [{
                'component': 'VCard',
                'props': {'variant': 'tonal'},
                'content': [{
                    'component': 'VCardText',
                    'content': [
                        {'component': 'div', 'props': {'class': 'text-subtitle-2 mb-1'}, 'text': f'{title}  ⏰ {cron}'},
                        {'component': 'span', 'props': {'class': 'text-caption'}, 'text': modules_text},
                    ],
                }],
            }],
        }

    @staticmethod
    def _stat_card(title: str, value: str) -> dict:
        return {
            'component': 'VCol',
            'props': {'cols': 4, 'md': 4},
            'content': [{
                'component': 'VCard',
                'props': {'variant': 'tonal'},
                'content': [{
                    'component': 'VCardText',
                    'props': {'class': 'text-center pa-2'},
                    'content': [
                        {'component': 'div', 'props': {'class': 'text-caption'}, 'text': title},
                        {'component': 'div', 'props': {'class': 'text-h6'}, 'text': value},
                    ],
                }],
            }],
        }

    def stop_service(self):
        pass

    # ─── 命令处理 ───

    @eventmanager.register(EventType.PluginAction)
    def handle_command(self, event: Event = None):
        if not event:
            return
        action = (event.event_data or {}).get("action", "")
        handler = {
            "daily_summary": self.send_daily,
            "weekly_summary": self.send_weekly,
            "monthly_summary": self.send_monthly,
        }.get(action)
        if handler:
            handler()

    # ─── 统一报告引擎 ───

    def send_daily(self):
        header, text = self._build_report("daily")
        self._send(report_type="daily", title=header, text=text)

    def send_weekly(self):
        header, text = self._build_report("weekly")
        self._send(report_type="weekly", title=header, text=text)

    def send_monthly(self):
        header, text = self._build_report("monthly")
        self._send(report_type="monthly", title=header, text=text)

    def _build_report(self, report_type: str) -> Tuple[str, str]:
        logger.info(f"[DailySummary] 开始生成 {report_type} 总结")
        tr = self._calc_time_range(report_type)
        modules = {
            "daily": self._daily_modules,
            "weekly": self._weekly_modules,
            "monthly": self._monthly_modules,
        }.get(report_type, self._daily_modules)

        sections = []
        for mod in modules:
            result = self._run_section(mod, tr)
            if result:
                sections.append(result)

        header = self._make_header(report_type, tr)
        text = "\n\n".join(sections) if sections else "无数据"
        return header, text

    def _calc_time_range(self, report_type: str) -> TimeRange:
        tz = pytz.timezone(settings.TZ)
        now = datetime.now(tz)
        today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)

        if report_type == "daily":
            start = today_start
            prefix = "今日"
        elif report_type == "weekly":
            start = today_start - timedelta(days=now.weekday())
            prefix = "本周"
        else:
            start = today_start.replace(day=1)
            prefix = "本月"

        return TimeRange(
            start=start,
            end=now,
            start_str=start.strftime("%Y-%m-%d 00:00:00"),
            start_date=start.strftime("%Y-%m-%d"),
            end_date=today_start.strftime("%Y-%m-%d"),
            report_type=report_type,
            prefix=prefix,
        )

    def _make_header(self, report_type: str, tr: TimeRange) -> str:
        now = tr.end
        if report_type == "daily":
            return f"📊 每日总结 ({now.strftime('%m-%d')} {WEEKDAY_NAMES[now.weekday()]})"
        elif report_type == "weekly":
            return f"📈 周报 ({tr.start.strftime('%m-%d')} ~ {now.strftime('%m-%d')})"
        else:
            return f"📅 月报 ({now.strftime('%Y年%m月')})"

    def _run_section(self, module: str, tr: TimeRange) -> Optional[str]:
        handler = {
            "download":     self._section_download,
            "transfer":     self._section_transfer,
            "signin":       self._section_signin,
            "brush":        self._section_brush,
            "downloader":   self._section_downloader,
            "site_delta":   self._section_site_delta,
            "site_current": self._section_site_current,
            "subscribe":    self._section_subscribe,
            "storage":      self._section_storage,
        }.get(module)
        if not handler:
            return None
        try:
            return handler(tr)
        except Exception as e:
            logger.error(f"[DailySummary] 模块 {module} 执行失败: {e}")
            return f"【{MODULES.get(module, module)}】数据读取失败"

    # ─── 各模块实现 ───

    def _section_download(self, tr: TimeRange) -> Optional[str]:
        downloads = self._get_downloads(tr.start_str)
        if not downloads:
            return f"【{tr.prefix}下载】无"

        # 日报：详细列表；周报/月报：分类汇总
        if tr.report_type == "daily":
            lines = [f"【{tr.prefix}下载 {len(downloads)} 部】"]
            for d in downloads:
                ep = f" {d.seasons}{d.episodes}" if d.episodes else (f" {d.seasons}" if d.seasons else "")
                site = f" - {d.torrent_site}" if d.torrent_site else ""
                lines.append(f"  • {d.title}{ep}{site}")
            return "\n".join(lines)

        type_count = {}
        for d in downloads:
            cat = d.media_category or d.type or "其他"
            type_count[cat] = type_count.get(cat, 0) + 1
        type_summary = " | ".join(f"{k} {v}" for k, v in sorted(type_count.items(), key=lambda x: -x[1]))
        return f"【{tr.prefix}下载】共 {len(downloads)} 部\n  {type_summary}"

    def _section_transfer(self, tr: TimeRange) -> Optional[str]:
        transfers = self._get_transfers(tr.start_str)
        success = [t for t in transfers if t.status]
        if not success:
            return f"【{tr.prefix}入库】无"

        # 日报：详细列表；周报/月报：分类汇总
        if tr.report_type == "daily":
            lines = [f"【{tr.prefix}入库 {len(success)} 部】"]
            for t in success:
                ep = f" {t.seasons}{t.episodes}" if t.episodes else (f" {t.seasons}" if t.seasons else "")
                cat = f" → {t.category}" if t.category else ""
                lines.append(f"  • {t.title}{ep}{cat}")
            return "\n".join(lines)

        cat_count = {}
        for t in success:
            cat = t.category or t.type or "其他"
            cat_count[cat] = cat_count.get(cat, 0) + 1
        cat_summary = " | ".join(f"{k} {v}" for k, v in sorted(cat_count.items(), key=lambda x: -x[1]))
        return f"【{tr.prefix}入库】共 {len(success)} 部\n  {cat_summary}"

    def _section_signin(self, tr: TimeRange) -> str:
        pdo = PluginDataOper()
        plugin_id = "AutoSignIn"
        now = tr.end
        key = f"{now.month}月{now.day}日"
        data = pdo.get_data(plugin_id, key)
        if not data:
            return "【签到】今日无签到记录"

        signin_records = [r for r in data if "模拟登录" not in r.get("status", "")]
        total = len(signin_records)
        success = sum(1 for r in signin_records if "成功" in r.get("status", ""))
        failed = [r for r in signin_records if "成功" not in r.get("status", "")]

        if success == total:
            return f"【签到】✅ 全部成功 ({success}/{total})"

        fail_sites = ", ".join(r["site"] for r in failed)
        return f"【签到】⚠️ {success}/{total} 成功\n  失败: {fail_sites}"

    def _section_brush(self, tr: TimeRange) -> str:
        pdo = PluginDataOper()
        plugin_ids = ["BrushFlow"]

        total_uploaded = 0
        total_downloaded = 0
        total_active = 0
        total_deleted = 0
        total_count = 0

        for pid in plugin_ids:
            stat = pdo.get_data(pid, "statistic")
            if not stat:
                continue
            total_uploaded += stat.get("uploaded", 0) + stat.get("active_uploaded", 0)
            total_downloaded += stat.get("downloaded", 0)
            total_active += stat.get("active", 0)
            total_deleted += stat.get("deleted", 0)
            total_count += stat.get("count", 0)

        return (
            f"【刷流】总种: {total_count} | 活跃: {total_active} | 已删: {total_deleted}\n"
            f"  总↑ {_human_size(total_uploaded)} | 总↓ {_human_size(total_downloaded)}"
        )

    def _section_downloader(self, tr: TimeRange) -> Optional[str]:
        """通过 DownloaderHelper 获取所有已配置下载器的概览"""
        try:
            from app.helper.downloader import DownloaderHelper
        except ImportError:
            logger.warning("[DailySummary] DownloaderHelper 不可用")
            return None

        services = DownloaderHelper().get_services()
        if not services:
            return None

        lines = ["【下载器概览】"]
        for name, svc in services.items():
            if not svc or not svc.instance:
                continue
            inst = svc.instance
            completed = inst.get_completed_torrents() or []
            downloading = inst.get_downloading_torrents() or []
            total = len(completed) + len(downloading)
            ti = inst.transfer_info()
            up_speed = ti.get("up_info_speed", 0) if ti else 0
            dl_speed = ti.get("dl_info_speed", 0) if ti else 0
            lines.append(f"  {name}: 种子 {total} | ↑{_human_size(up_speed)}/s | ↓{_human_size(dl_speed)}/s")

        return "\n".join(lines) if len(lines) > 1 else None

    def _section_site_delta(self, tr: TimeRange) -> Optional[str]:
        with ScopedSession() as db:
            start_data = SiteUserData.get_by_date(db, tr.start_date)
            end_data = SiteUserData.get_by_date(db, tr.end_date)

        if not start_data or not end_data:
            return None

        start_map = {d.domain: d for d in start_data}
        end_map = {d.domain: d for d in end_data}

        label = {"daily": "站点增量", "weekly": "站点周增量", "monthly": "站点月增量"}.get(tr.report_type, "站点增量")
        lines = [f"【{label}】", "  站点        ↑上传        ↓下载       魔力变化"]

        has_data = False
        for domain, end in sorted(end_map.items(), key=lambda x: (x[1].upload or 0), reverse=True):
            start = start_map.get(domain)
            if not start:
                continue
            up_delta = (end.upload or 0) - (start.upload or 0)
            down_delta = (end.download or 0) - (start.download or 0)
            bonus_delta = (end.bonus or 0) - (start.bonus or 0)

            no_change = up_delta == 0 and down_delta == 0 and bonus_delta == 0
            data_anomaly = up_delta < 0 or down_delta < 0
            if no_change or data_anomaly:
                continue

            has_data = True
            name = (end.name or domain)[:6].ljust(6)
            bonus_str = f"+{bonus_delta:.0f}" if bonus_delta >= 0 else f"{bonus_delta:.0f}"
            lines.append(f"  {name}  {_human_size(up_delta):>10}  {_human_size(down_delta):>10}  {bonus_str:>8}")

        return "\n".join(lines) if has_data else None

    def _section_site_current(self, tr: TimeRange) -> Optional[str]:
        with ScopedSession() as db:
            data = SiteUserData.get_latest(db)

        if not data:
            return None

        lines = ["【站点数据】", "  站点        总↑          总↓        分享率   魔力"]

        for d in sorted(data, key=lambda x: (x.upload or 0), reverse=True):
            if not d.upload and not d.download:
                continue
            name = (d.name or d.domain)[:6].ljust(6)
            ratio = f"{d.ratio:.2f}" if d.ratio else "∞"
            bonus = f"{d.bonus:.0f}" if d.bonus else "0"
            lines.append(f"  {name}  {_human_size(d.upload or 0):>10}  {_human_size(d.download or 0):>10}  {ratio:>6}  {bonus:>8}")

        return "\n".join(lines) if len(lines) > 2 else None

    def _section_subscribe(self, tr: TimeRange) -> str:
        subs = SubscribeOper().list(state="R") or []
        if not subs:
            return "【订阅进度】无活跃订阅"

        lines = [f"【订阅进度】{len(subs)} 部进行中"]
        for s in subs:
            total = s.total_episode or 0
            lack = s.lack_episode or 0
            done = total - lack
            season = f" S{s.season}" if s.season else ""
            progress = f" {done}/{total}" if total > 0 else ""
            lines.append(f"  • {s.name}{season}{progress}")
        return "\n".join(lines)

    def _section_storage(self, tr: TimeRange) -> Optional[str]:
        volumes = self._parse_storage_paths()
        if not volumes:
            return None

        lines = ["【存储空间】"]
        has_data = False
        for path, label in volumes:
            if not os.path.exists(path):
                continue
            stat = os.statvfs(path)
            total = stat.f_blocks * stat.f_frsize
            used = (stat.f_blocks - stat.f_bfree) * stat.f_frsize
            if total == 0:
                continue
            has_data = True
            pct = used / total * 100
            lines.append(f"  {label}: 已用 {_human_size(used)} / {_human_size(total)} ({pct:.0f}%)")
        return "\n".join(lines) if has_data else None

    def _parse_storage_paths(self) -> List[Tuple[str, str]]:
        """自动检测 MP 的 LIBRARY_PATH / DOWNLOAD_PATH"""
        paths = []
        if hasattr(settings, "LIBRARY_PATH") and settings.LIBRARY_PATH:
            paths.append((settings.LIBRARY_PATH, "媒体库"))
        if hasattr(settings, "DOWNLOAD_PATH") and settings.DOWNLOAD_PATH:
            paths.append((settings.DOWNLOAD_PATH, "下载目录"))
        return paths

    # ─── 数据查询 ───

    def _get_downloads(self, since: str) -> list:
        try:
            from app.db.models.downloadhistory import DownloadHistory
            with ScopedSession() as db:
                return db.query(DownloadHistory).filter(
                    DownloadHistory.date > since
                ).order_by(DownloadHistory.date.desc()).all()
        except Exception as e:
            logger.error(f"[DailySummary] 查询下载记录失败: {e}")
            return []

    def _get_transfers(self, since: str) -> list:
        try:
            return TransferHistoryOper().list_by_date(since) or []
        except Exception as e:
            logger.error(f"[DailySummary] 查询入库记录失败: {e}")
            return []

    # ─── 发送通知 + 保存历史 ───

    def _send(self, report_type: str, title: str, text: str):
        logger.info(f"[DailySummary] {title}\n{text}")

        if self._notify:
            self.post_message(mtype=NotificationType.Plugin, title=title, text=text)

        # 保存历史记录
        tz = pytz.timezone(settings.TZ)
        now = datetime.now(tz)
        record = {
            "time": now.strftime("%Y-%m-%d %H:%M"),
            "type": report_type,
            "title": title,
            "text": text,
        }
        history = self.get_data("history") or []
        history.append(record)
        # 保留最近 MAX_HISTORY 条
        if len(history) > MAX_HISTORY:
            history = history[-MAX_HISTORY:]
        self.save_data("history", history)


# ─── 工具函数 ───

def _human_size(size_bytes: float) -> str:
    if size_bytes is None or size_bytes == 0:
        return "0 B"
    negative = size_bytes < 0
    size_bytes = abs(size_bytes)
    for unit in ["B", "KB", "MB", "GB", "TB", "PB"]:
        if size_bytes < 1024:
            formatted = f"{size_bytes:.1f} {unit}" if size_bytes != int(size_bytes) else f"{int(size_bytes)} {unit}"
            return f"-{formatted}" if negative else formatted
        size_bytes /= 1024
    return f"{size_bytes:.1f} EB"
