"""
Telegram 财经快讯机器人 - 主入口
功能：自动抓取财经/科技快讯，AI 评分过滤，推送到 Telegram
"""
import os
import time
import logging
import asyncio
from datetime import datetime

from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import (
    Application, CommandHandler, CallbackQueryHandler,
    ContextTypes, MessageHandler, filters,
)
from telegram.constants import ParseMode

import sources
import scorer
import storage
import summarizer

# 日志配置
logging.basicConfig(
    format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
    level=logging.INFO,
)
logger = logging.getLogger(__name__)

# 配置
BOT_TOKEN = os.environ.get("BOT_TOKEN", "")
ADMIN_ID = int(os.environ.get("ADMIN_ID", "165067365"))

# 分页状态缓存
_page_cache = {}


# ========== 工具函数 ==========

def _check_admin(user_id: int) -> bool:
    """检查是否为管理员"""
    return user_id == ADMIN_ID


def _format_news_item(item: dict) -> str:
    """格式化单条新闻（支持 HTML 链接）"""
    score = item.get("score", 0)
    if score >= 9:
        emoji = "🔥"
    elif score >= 8:
        emoji = "⚡"
    elif score >= 7:
        emoji = "📌"
    elif score >= 6:
        emoji = "🔹"
    else:
        emoji = "•"
    src = item.get("source_name", "")
    ts = item.get("time_str", "")
    title = item.get("title", "")
    url = item.get("url", "")
    # 转义 HTML 特殊字符
    import html as html_mod
    title_safe = html_mod.escape(title)
    if url:
        title_display = f'<a href="{url}">{title_safe}</a>'
    else:
        title_display = title_safe
    return f"{emoji} {title_display}\n   📡 {src}  🕐 {ts}  评分:{score}"


# ========== /start 命令 ==========

async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """欢迎消息 + 主菜单"""
    if not _check_admin(update.effective_user.id):
        await update.message.reply_text("⛔ 仅限管理员使用")
        return

    keyboard = [
        [InlineKeyboardButton("📰 最新快讯", callback_data="news_0"),
         InlineKeyboardButton("📊 新闻总结", callback_data="summary_menu")],
        [InlineKeyboardButton("📡 订阅源管理", callback_data="sources_menu"),
         InlineKeyboardButton("🔑 关键词管理", callback_data="keywords_menu")],
        [InlineKeyboardButton("⚙️ 设置", callback_data="settings_menu")],
    ]
    text = (
        "👋 <b>欢迎使用财经快讯机器人</b>\n\n"
        "🤖 自动抓取金十、华尔街见闻、36氪、新浪财经快讯\n"
        "📊 AI 评分过滤，只推送相关内容\n"
        "⏰ 定时总结，不错过重要新闻\n\n"
        "请选择功能："
    )
    await update.message.reply_text(
        text, parse_mode=ParseMode.HTML,
        reply_markup=InlineKeyboardMarkup(keyboard),
    )


# ========== /news 命令 ==========

async def cmd_news(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """查看最新快讯"""
    if not _check_admin(update.effective_user.id):
        return
    await _show_news_page(update.message, 0)


async def _show_news_page(message_or_query, page: int):
    """显示新闻分页"""
    settings = storage.get_settings()
    news_list = storage.get_news(limit=100)

    # 评分
    scorer.score_and_filter(news_list, settings.get("keywords", []))
    # 只显示评分 >= 4 的
    news_list = [n for n in news_list if n.get("score", 0) >= 4]
    news_list.sort(key=lambda x: x.get("timestamp", 0), reverse=True)

    page_size = 10
    total_pages = max(1, (len(news_list) + page_size - 1) // page_size)
    page = max(0, min(page, total_pages - 1))
    start = page * page_size
    page_items = news_list[start:start + page_size]

    if not page_items:
        text = "📭 暂无快讯，等待抓取中..."
    else:
        lines = [f"📰 <b>最新快讯</b> (第{page+1}/{total_pages}页)\n"]
        for item in page_items:
            lines.append(_format_news_item(item))
        text = "\n".join(lines)

    # 分页按钮
    buttons = []
    if page > 0:
        buttons.append(InlineKeyboardButton("⬅️ 上一页", callback_data=f"news_{page-1}"))
    buttons.append(InlineKeyboardButton("🔄 刷新", callback_data=f"news_{page}"))
    if page < total_pages - 1:
        buttons.append(InlineKeyboardButton("➡️ 下一页", callback_data=f"news_{page+1}"))
    keyboard = [buttons, [InlineKeyboardButton("🏠 主菜单", callback_data="main_menu")]]

    if hasattr(message_or_query, "edit_message_text"):
        try:
            await message_or_query.edit_message_text(
                text, parse_mode=ParseMode.HTML,
                reply_markup=InlineKeyboardMarkup(keyboard),
            )
        except Exception:
            await message_or_query.edit_message_text(
                text, reply_markup=InlineKeyboardMarkup(keyboard),
            )
    else:
        await message_or_query.reply_text(
            text, parse_mode=ParseMode.HTML,
            reply_markup=InlineKeyboardMarkup(keyboard),
        )


# ========== /summary 命令 ==========

async def cmd_summary(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """手动触发新闻总结"""
    if not _check_admin(update.effective_user.id):
        return
    await _show_summary_menu(update.message)


async def _show_summary_menu(message_or_query):
    """显示总结时间范围选择"""
    keyboard = [
        [InlineKeyboardButton("⏱ 最近1小时", callback_data="summary_最近1小时")],
        [InlineKeyboardButton("🌅 上午", callback_data="summary_上午")],
        [InlineKeyboardButton("📅 全天", callback_data="summary_全天")],
        [InlineKeyboardButton("🏠 主菜单", callback_data="main_menu")],
    ]
    text = "📊 <b>新闻总结</b>\n\n请选择时间范围："
    if hasattr(message_or_query, "edit_message_text"):
        await message_or_query.edit_message_text(
            text, parse_mode=ParseMode.HTML,
            reply_markup=InlineKeyboardMarkup(keyboard),
        )
    else:
        await message_or_query.reply_text(
            text, parse_mode=ParseMode.HTML,
            reply_markup=InlineKeyboardMarkup(keyboard),
        )


# ========== /sources 命令 ==========

async def cmd_sources(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """查看/管理订阅源"""
    if not _check_admin(update.effective_user.id):
        return
    await _show_sources_menu(update.message)


async def _show_sources_menu(message_or_query):
    """显示订阅源管理菜单"""
    settings = storage.get_settings()
    srcs = settings.get("sources", {})
    source_info = {
        "jin10":      "金十数据",
        "wallstreet": "华尔街见闻",
        "kr36":       "36氪",
        "sina":       "新浪财经",
    }
    keyboard = []
    for key, name in source_info.items():
        enabled = srcs.get(key, True)
        status = "✅" if enabled else "❌"
        keyboard.append([InlineKeyboardButton(
            f"{status} {name}", callback_data=f"toggle_src_{key}"
        )])
    keyboard.append([InlineKeyboardButton("🏠 主菜单", callback_data="main_menu")])
    text = "📡 <b>订阅源管理</b>\n\n点击切换开/关："
    if hasattr(message_or_query, "edit_message_text"):
        await message_or_query.edit_message_text(
            text, parse_mode=ParseMode.HTML,
            reply_markup=InlineKeyboardMarkup(keyboard),
        )
    else:
        await message_or_query.reply_text(
            text, parse_mode=ParseMode.HTML,
            reply_markup=InlineKeyboardMarkup(keyboard),
        )


# ========== /keywords 命令 ==========

async def cmd_keywords(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """管理关注关键词"""
    if not _check_admin(update.effective_user.id):
        return
    await _show_keywords_menu(update.message)


async def _show_keywords_menu(message_or_query):
    """显示关键词管理菜单"""
    settings = storage.get_settings()
    kw_list = settings.get("keywords", [])
    # 每行显示3个关键词删除按钮
    keyboard = []
    row = []
    for kw in kw_list[:30]:
        row.append(InlineKeyboardButton(f"❌ {kw}", callback_data=f"del_kw_{kw}"))
        if len(row) == 3:
            keyboard.append(row)
            row = []
    if row:
        keyboard.append(row)
    keyboard.append([InlineKeyboardButton("➕ 添加关键词", callback_data="add_kw_prompt")])
    keyboard.append([InlineKeyboardButton("🏠 主菜单", callback_data="main_menu")])
    text = f"🔑 <b>关键词管理</b>\n\n当前 {len(kw_list)} 个关键词\n（点击关键词可删除）"
    if hasattr(message_or_query, "edit_message_text"):
        await message_or_query.edit_message_text(
            text, parse_mode=ParseMode.HTML,
            reply_markup=InlineKeyboardMarkup(keyboard),
        )
    else:
        await message_or_query.reply_text(
            text, parse_mode=ParseMode.HTML,
            reply_markup=InlineKeyboardMarkup(keyboard),
        )


# ========== /settings 命令 ==========

async def cmd_settings(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """设置菜单"""
    if not _check_admin(update.effective_user.id):
        return
    await _show_settings_menu(update.message)


async def _show_settings_menu(message_or_query):
    """显示设置菜单"""
    settings = storage.get_settings()
    push_on = settings.get("push_enabled", True)
    min_score = settings.get("min_score", 6)
    instant_score = settings.get("instant_score", 8)
    interval = settings.get("batch_interval", 30)

    push_txt = "✅ 推送已开" if push_on else "❌ 推送已关"
    keyboard = [
        [InlineKeyboardButton(push_txt, callback_data="toggle_push")],
        [
            InlineKeyboardButton(f"最低评分: {min_score} ➖", callback_data="score_min_down"),
            InlineKeyboardButton(f"最低评分: {min_score} ➕", callback_data="score_min_up"),
        ],
        [
            InlineKeyboardButton(f"即时阈值: {instant_score} ➖", callback_data="score_instant_down"),
            InlineKeyboardButton(f"即时阈值: {instant_score} ➕", callback_data="score_instant_up"),
        ],
        [
            InlineKeyboardButton(f"汇总间隔: {interval}min ➖", callback_data="interval_down"),
            InlineKeyboardButton(f"汇总间隔: {interval}min ➕", callback_data="interval_up"),
        ],
        [InlineKeyboardButton("🏠 主菜单", callback_data="main_menu")],
    ]
    text = (
        f"⚙️ <b>设置</b>\n\n"
        f"推送状态: {'开启' if push_on else '关闭'}\n"
        f"最低推送评分: {min_score}\n"
        f"即时推送阈值: {instant_score}\n"
        f"汇总推送间隔: {interval} 分钟"
    )
    if hasattr(message_or_query, "edit_message_text"):
        await message_or_query.edit_message_text(
            text, parse_mode=ParseMode.HTML,
            reply_markup=InlineKeyboardMarkup(keyboard),
        )
    else:
        await message_or_query.reply_text(
            text, parse_mode=ParseMode.HTML,
            reply_markup=InlineKeyboardMarkup(keyboard),
        )


# ========== CallbackQuery 处理器 ==========

# 等待用户输入关键词的状态
_waiting_kw_add = set()


async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """统一处理所有按钮回调"""
    query = update.callback_query
    await query.answer()
    data = query.data
    user_id = query.from_user.id

    if not _check_admin(user_id):
        return

    # ---------- 主菜单 ----------
    if data == "main_menu":
        keyboard = [
            [InlineKeyboardButton("📰 最新快讯", callback_data="news_0"),
             InlineKeyboardButton("📊 新闻总结", callback_data="summary_menu")],
            [InlineKeyboardButton("📡 订阅源管理", callback_data="sources_menu"),
             InlineKeyboardButton("🔑 关键词管理", callback_data="keywords_menu")],
            [InlineKeyboardButton("⚙️ 设置", callback_data="settings_menu")],
        ]
        await query.edit_message_text(
            "🏠 <b>主菜单</b>\n\n请选择功能：",
            parse_mode=ParseMode.HTML,
            reply_markup=InlineKeyboardMarkup(keyboard),
        )

    # ---------- 新闻分页 ----------
    elif data.startswith("news_"):
        page = int(data.split("_", 1)[1])
        await _show_news_page(query, page)

    # ---------- 总结菜单 ----------
    elif data == "summary_menu":
        await _show_summary_menu(query)

    elif data.startswith("summary_"):
        period = data[len("summary_"):]
        start_ts, end_ts = summarizer.get_period_range(period)
        settings = storage.get_settings()
        news_list = storage.get_news(limit=500, since_ts=start_ts)
        news_list = [n for n in news_list if n.get("timestamp", 0) <= end_ts]
        scorer.score_and_filter(news_list, settings.get("keywords", []))
        news_list = [n for n in news_list if n.get("score", 0) >= settings.get("min_score", 6)]
        text = summarizer.generate_summary(news_list, period)
        back_btn = InlineKeyboardMarkup([[
            InlineKeyboardButton("🔙 返回", callback_data="summary_menu"),
            InlineKeyboardButton("🏠 主菜单", callback_data="main_menu"),
        ]])
        try:
            await query.edit_message_text(text, parse_mode=ParseMode.HTML, reply_markup=back_btn)
        except Exception:
            await query.edit_message_text(text, reply_markup=back_btn)

    # ---------- 订阅源 ----------
    elif data == "sources_menu":
        await _show_sources_menu(query)

    elif data.startswith("toggle_src_"):
        src = data[len("toggle_src_"):]
        new_state = storage.toggle_source(src)
        state_txt = "开启" if new_state else "关闭"
        await query.answer(f"已{state_txt} {sources.SOURCE_NAMES.get(src, src)}", show_alert=False)
        await _show_sources_menu(query)

    # ---------- 关键词 ----------
    elif data == "keywords_menu":
        await _show_keywords_menu(query)

    elif data.startswith("del_kw_"):
        kw = data[len("del_kw_"):]
        storage.remove_keyword(kw)
        await query.answer(f"已删除关键词: {kw}", show_alert=False)
        await _show_keywords_menu(query)

    elif data == "add_kw_prompt":
        _waiting_kw_add.add(user_id)
        await query.edit_message_text(
            "🔑 <b>添加关键词</b>\n\n请直接发送关键词文字（可空格分隔多个）：",
            parse_mode=ParseMode.HTML,
            reply_markup=InlineKeyboardMarkup([[
                InlineKeyboardButton("取消", callback_data="keywords_menu")
            ]]),
        )

    # ---------- 设置 ----------
    elif data == "settings_menu":
        await _show_settings_menu(query)

    elif data == "toggle_push":
        settings = storage.get_settings()
        storage.update_settings({"push_enabled": not settings.get("push_enabled", True)})
        await _show_settings_menu(query)

    elif data == "score_min_up":
        s = storage.get_settings()
        storage.update_settings({"min_score": min(10, s.get("min_score", 6) + 1)})
        await _show_settings_menu(query)

    elif data == "score_min_down":
        s = storage.get_settings()
        storage.update_settings({"min_score": max(1, s.get("min_score", 6) - 1)})
        await _show_settings_menu(query)

    elif data == "score_instant_up":
        s = storage.get_settings()
        storage.update_settings({"instant_score": min(10, s.get("instant_score", 8) + 1)})
        await _show_settings_menu(query)

    elif data == "score_instant_down":
        s = storage.get_settings()
        storage.update_settings({"instant_score": max(1, s.get("instant_score", 8) - 1)})
        await _show_settings_menu(query)

    elif data == "interval_up":
        s = storage.get_settings()
        storage.update_settings({"batch_interval": min(120, s.get("batch_interval", 30) + 5)})
        await _show_settings_menu(query)

    elif data == "interval_down":
        s = storage.get_settings()
        storage.update_settings({"batch_interval": max(5, s.get("batch_interval", 30) - 5)})
        await _show_settings_menu(query)


async def handle_text(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """处理普通文本消息（用于添加关键词）"""
    user_id = update.effective_user.id
    if not _check_admin(user_id):
        return
    if user_id not in _waiting_kw_add:
        return
    _waiting_kw_add.discard(user_id)
    text = update.message.text.strip()
    added = []
    for kw in text.split():
        if storage.add_keyword(kw):
            added.append(kw)
    if added:
        await update.message.reply_text(
            f"✅ 已添加关键词: {', '.join(added)}",
            reply_markup=InlineKeyboardMarkup([[
                InlineKeyboardButton("🔑 关键词管理", callback_data="keywords_menu")
            ]]),
        )
    else:
        await update.message.reply_text("⚠️ 关键词已存在或无效")


# ========== 后台抓取任务 ==========

# 汇总队列（评分6-7的新闻暂存）
_batch_queue: list = []
_last_batch_push: float = 0.0
_pushed_in_memory: set = set()  # 内存去重，防止并发重复推送


async def _fetch_and_process(app: Application):
    """核心抓取+处理循环"""
    settings = storage.get_settings()
    enabled = settings.get("sources", {})
    push_on = settings.get("push_enabled", True)
    min_score = settings.get("min_score", 6)
    instant_score = settings.get("instant_score", 8)

    logger.info("开始抓取新闻...")
    try:
        all_news = await sources.fetch_all(enabled)
    except Exception as e:
        logger.error(f"fetch_all 异常: {e}")
        return

    if not all_news:
        return

    # 评分
    scorer.score_and_filter(all_news, settings.get("keywords", []))

    # 去重（全局标题去重）
    all_news = scorer.dedup_news(all_news)

    # 过滤已推送（内存+文件双重去重）
    pushed_ids = storage.get_pushed_ids() | _pushed_in_memory
    new_items = [n for n in all_news if n["id"] not in pushed_ids and n.get("score", 0) >= min_score]

    if not new_items:
        return

    # 保存到存储
    storage.save_news(new_items)

    if not push_on:
        return

    instant_items = [n for n in new_items if n.get("score", 0) >= instant_score]
    batch_items = [n for n in new_items if min_score <= n.get("score", 0) < instant_score]

    # 即时推送
    for item in instant_items:
        await _push_instant(app, item)

    # 加入汇总队列（去重）
    existing_ids = {n["id"] for n in _batch_queue}
    for item in batch_items:
        if item["id"] not in existing_ids:
            _batch_queue.append(item)
            existing_ids.add(item["id"])

    # 标记已推送（即时+批量都标记，防止重复）
    pushed = [n["id"] for n in instant_items + batch_items]
    _pushed_in_memory.update(pushed)
    storage.mark_pushed(pushed)


async def _push_instant(app: Application, item: dict):
    """立即推送单条重磅新闻"""
    score = item.get("score", 0)
    if score >= 9:
        emoji = "🔥🔥🔥"
    elif score >= 8:
        emoji = "⚡ 重磅"
    else:
        emoji = "📌"
    src = item.get("source_name", "")
    title = item.get("title", "")
    ts = item.get("time_str", "")
    url = item.get("url", "")
    import html as html_mod
    title_safe = html_mod.escape(title)
    if url:
        title_display = f'<a href="{url}">{title_safe}</a>'
    else:
        title_display = f"<b>{title_safe}</b>"
    text = f"{emoji}\n\n{title_display}\n\n📡 {src}  🕐 {ts}  评分:{score}"
    try:
        await app.bot.send_message(
            chat_id=ADMIN_ID, text=text, parse_mode=ParseMode.HTML,
        )
    except Exception as e:
        logger.error(f"即时推送失败: {e}")
        try:
            await app.bot.send_message(chat_id=ADMIN_ID, text=f"{emoji}\n\n{title}\n\n{src} {ts}")
        except Exception as e2:
            logger.error(f"即时推送备用失败: {e2}")


async def _push_batch(app: Application):
    """汇总推送（每N分钟一次）"""
    global _batch_queue, _last_batch_push
    if not _batch_queue:
        return

    settings = storage.get_settings()
    interval = settings.get("batch_interval", 30) * 60
    now = time.time()
    if now - _last_batch_push < interval:
        return

    items = _batch_queue[:]
    _batch_queue.clear()
    _last_batch_push = now

    # 标记已推送
    storage.mark_pushed([n["id"] for n in items])

    if not items:
        return

    import html as html_mod
    from collections import defaultdict

    # 按来源分组
    source_map = defaultdict(list)
    for item in sorted(items, key=lambda x: x.get("score", 0), reverse=True):
        src = item.get("source_name", "未知")
        source_map[src].append(item)

    source_emoji = {
        "金十数据": "💰", "华尔街见闻": "📈", "36氪": "🚀",
        "新浪财经": "📊", "Google News": "🌐",
        "Finviz": "📉", "TechCrunch": "💻",
    }

    lines = [f"🟡 <b>快讯汇总</b>（{len(items)}条）"]
    lines.append("")
    for src, src_items in source_map.items():
        emoji = source_emoji.get(src, "📰")
        lines.append(f"{emoji} <b>{src}</b>（{len(src_items)}）")
        for item in src_items[:10]:
            title = item.get("title", "")
            url = item.get("url", "")
            title_safe = html_mod.escape(title)
            if url:
                lines.append(f"  • <a href=\"{url}\">{title_safe}</a>")
            else:
                lines.append(f"  • {title_safe}")
        if len(src_items) > 10:
            lines.append(f"  ... 还有{len(src_items) - 10}条")
        lines.append("")

    lines.append(f"⏰ {datetime.now().strftime('%H:%M')}")

    text = "\n".join(lines)
    try:
        await app.bot.send_message(
            chat_id=ADMIN_ID, text=text, parse_mode=ParseMode.HTML,
        )
    except Exception as e:
        logger.error(f"汇总推送失败: {e}")
        try:
            await app.bot.send_message(chat_id=ADMIN_ID, text="\n".join(lines[:15]))
        except Exception as e2:
            logger.error(f"汇总推送备用失败: {e2}")


async def _send_scheduled_summary(app: Application, period: str):
    """发送定时总结"""
    settings = storage.get_settings()
    start_ts, end_ts = summarizer.get_period_range(period)
    news_list = storage.get_news(limit=500, since_ts=start_ts)
    news_list = [n for n in news_list if n.get("timestamp", 0) <= end_ts]
    scorer.score_and_filter(news_list, settings.get("keywords", []))
    news_list = [n for n in news_list if n.get("score", 0) >= settings.get("min_score", 6)]
    text = summarizer.generate_summary(news_list, period)
    try:
        await app.bot.send_message(
            chat_id=ADMIN_ID, text=text, parse_mode=ParseMode.HTML,
        )
    except Exception as e:
        logger.error(f"定时总结推送失败: {e}")
        try:
            await app.bot.send_message(chat_id=ADMIN_ID, text=text)
        except Exception:
            pass


# ========== 调度器 ==========

class Scheduler:
    """轻量调度器，基于 asyncio"""

    def __init__(self, app: Application):
        self.app = app
        self._tasks: list[asyncio.Task] = []

    def start(self):
        self._tasks.append(asyncio.create_task(self._fast_loop()))
        self._tasks.append(asyncio.create_task(self._slow_loop()))
        self._tasks.append(asyncio.create_task(self._batch_loop()))
        self._tasks.append(asyncio.create_task(self._summary_loop()))
        logger.info("调度器已启动")

    async def _fast_loop(self):
        """金十/华尔街见闻 每2分钟抓取"""
        while True:
            try:
                await _fetch_and_process(self.app)
            except Exception as e:
                logger.error(f"fast_loop 异常: {e}")
            await asyncio.sleep(120)

    async def _slow_loop(self):
        """36氪/新浪 每5分钟额外触发（fast_loop已包含全部，这里可扩展）"""
        await asyncio.sleep(60)  # 错开启动时间
        while True:
            try:
                await _fetch_and_process(self.app)
            except Exception as e:
                logger.error(f"slow_loop 异常: {e}")
            await asyncio.sleep(300)

    async def _batch_loop(self):
        """每分钟检查是否需要汇总推送"""
        await asyncio.sleep(30)
        while True:
            try:
                await _push_batch(self.app)
            except Exception as e:
                logger.error(f"batch_loop 异常: {e}")
            await asyncio.sleep(60)

    async def _summary_loop(self):
        """定时总结 08:00 / 11:30 / 20:00"""
        schedule = [
            (8, 0,  "昨晚到今早"),
            (11, 30, "上午"),
            (20, 0,  "全天"),
        ]
        triggered = set()
        while True:
            now = datetime.now()
            key = f"{now.date()}"
            for hour, minute, period in schedule:
                tid = f"{key}_{hour}_{minute}"
                if tid not in triggered:
                    # 在目标时间 ±2 分钟内触发
                    target = now.replace(hour=hour, minute=minute, second=0)
                    diff = abs((now - target).total_seconds())
                    if diff <= 120:
                        triggered.add(tid)
                        try:
                            await _send_scheduled_summary(self.app, period)
                        except Exception as e:
                            logger.error(f"定时总结异常: {e}")
            # 每分钟检查一次
            await asyncio.sleep(60)

    def stop(self):
        for t in self._tasks:
            t.cancel()


# ========== 主函数 ==========

def main():
    if not BOT_TOKEN:
        logger.error("请设置 BOT_TOKEN 环境变量")
        return

    logger.info(f"机器人启动，ADMIN_ID={ADMIN_ID}")

    app = Application.builder().token(BOT_TOKEN).build()

    # 注册命令
    app.add_handler(CommandHandler("start",    cmd_start))
    app.add_handler(CommandHandler("news",     cmd_news))
    app.add_handler(CommandHandler("summary",  cmd_summary))
    app.add_handler(CommandHandler("sources",  cmd_sources))
    app.add_handler(CommandHandler("keywords", cmd_keywords))
    app.add_handler(CommandHandler("settings", cmd_settings))

    # 注册回调
    app.add_handler(CallbackQueryHandler(handle_callback))

    # 注册文本消息（用于添加关键词）
    app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_text))

    # 启动调度器（在 post_init 中启动，确保 event loop 已就绪）
    scheduler = Scheduler(app)

    async def post_init(application: Application):
        scheduler.start()
        logger.info("调度器已在 post_init 中启动")

    app.post_init = post_init

    logger.info("开始 polling...")
    app.run_polling(drop_pending_updates=True)


if __name__ == "__main__":
    main()
