"""M7 定时任务调度入口。

支持两种模式:
    --once            立刻执行一次全链路
    (默认)            启动 APScheduler,按 .env 中 SCHEDULE_TIMES 定时执行

用法:
    uv run python -m scripts.run_scheduler              # 启动定时服务
    uv run python -m scripts.run_scheduler --once        # 立即执行一次
    uv run python -m scripts.run_scheduler --once --date 20260616
    uv run python -m scripts.run_scheduler --once --steps crawler,extractor
"""

from __future__ import annotations

import argparse
import signal
import sys
from datetime import date, datetime
from pathlib import Path
from typing import Any

from dotenv import load_dotenv
from loguru import logger

from scheduler import STEP_COMMANDS, run_pipeline
from scheduler.stock_reporter import generate_all_stock_reports


def _setup_logger(level: str) -> None:
    logger.remove()
    logger.add(
        sys.stderr,
        level=level,
        format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name} | {message}",
    )
    log_path = Path("logs") / "scheduler.log"
    log_path.parent.mkdir(parents=True, exist_ok=True)
    logger.add(log_path, level="DEBUG", rotation="10 MB", retention=5, encoding="utf-8")


def _parse_schedule_times(raw: str) -> list[tuple[int, int]]:
    """解析 SCHEDULE_TIMES 环境变量。

    格式: "07:00,12:00,18:00,22:00"
    返回: [(7,0), (12,0), (18,0), (22,0)]
    """
    out: list[tuple[int, int]] = []
    for part in raw.split(","):
        part = part.strip()
        if not part:
            continue
        try:
            h, m = part.split(":")
            out.append((int(h), int(m)))
        except ValueError:
            logger.warning("SCHEDULE_TIMES 格式错误: {!r},跳过", part)
    return out


def _once(args: argparse.Namespace) -> int:
    """单次执行模式。"""
    steps = None
    if args.steps:
        steps = [s.strip() for s in args.steps.split(",")]
    run_pipeline(args.date, steps=steps)
    return 0


def _daemon(args: argparse.Namespace) -> int:
    """守护进程模式(APScheduler)。"""
    import os

    from apscheduler.schedulers.background import BackgroundScheduler  # noqa: E402
    from apscheduler.triggers.cron import CronTrigger  # noqa: E402

    times_raw = os.environ.get("SCHEDULE_TIMES", "07:00,12:00,18:00,22:00")
    times = _parse_schedule_times(times_raw)
    if not times:
        logger.error("SCHEDULE_TIMES 为空或全部非法,无法启动定时任务")
        return 2

    # 找出最早的时间(当天首次运行),仅该次追加日报步骤
    sorted_times = sorted(times)
    first_hour, first_minute = sorted_times[0] if sorted_times else (0, 0)

    scheduler = BackgroundScheduler()

    # 包装函数:每次触发时重新计算日期,避免 date.today() 在注册时冻结。
    def _scheduled_pipeline(steps: list[str] | None = None) -> None:
        run_pipeline(date.today().strftime("%Y%m%d"), steps=steps)

    for hour, minute in times:
        trigger = CronTrigger(hour=hour, minute=minute, timezone="Asia/Shanghai")
        is_first = (hour == first_hour and minute == first_minute)
        job_kwargs: dict | None = None
        if is_first:
            job_kwargs = {
                "steps": [k for k in STEP_COMMANDS if k not in ("report", "cninfo_crawl", "cninfo_extract", "cninfo_pdf")] + ["report"]
            }
        scheduler.add_job(
            _scheduled_pipeline,
            trigger=trigger,
            kwargs=job_kwargs,
            id=f"pipeline_{hour:02d}{minute:02d}",
            name=f"全链路 {'+日报' if is_first else ''} {hour:02d}:{minute:02d}",
        )
        logger.info("已注册定时任务: {}每天 {:02d}:{:02d}{}", trigger, hour, minute,
                     " (含日报)" if is_first else "")

    # cninfo 公告管道(可配置,默认 06:30)
    cninfo_raw = os.environ.get("CNINFO_SCHEDULE_TIME", "06:30")
    cninfo_parts = cninfo_raw.split(":")
    cninfo_h, cninfo_m = int(cninfo_parts[0]), int(cninfo_parts[1]) if len(cninfo_parts) > 1 else 0
    cninfo_trigger = CronTrigger(hour=cninfo_h, minute=cninfo_m, timezone="Asia/Shanghai")
    cninfo_steps = ["cninfo_crawl", "cninfo_extract", "cninfo_pdf",
                    "dedup", "llm", "embedding", "qdrant"]
    scheduler.add_job(
        _scheduled_pipeline,
        trigger=cninfo_trigger,
        kwargs={"steps": cninfo_steps},
        id="pipeline_cninfo",
        name=f"cninfo 公告管道 {cninfo_h:02d}:{cninfo_m:02d}",
    )
    logger.info("已注册定时任务: cninfo 公告管道 每天 {:02d}:{:02d}", cninfo_h, cninfo_m)

    # 个股日报(可配置,默认 07:30, 设为空可禁用)
    stock_raw = os.environ.get("STOCK_REPORT_TIME", "07:30")
    if stock_raw:
        stock_parts = stock_raw.split(":")
        stock_h, stock_m = int(stock_parts[0]), int(stock_parts[1]) if len(stock_parts) > 1 else 0
        stock_trigger = CronTrigger(hour=stock_h, minute=stock_m, timezone="Asia/Shanghai")
        scheduler.add_job(
            generate_all_stock_reports,
            trigger=stock_trigger,
            id="stock_report",
            name=f"个股日报 {stock_h:02d}:{stock_m:02d}",
        )
        logger.info("已注册定时任务: 个股日报 每天 {:02d}:{:02d}", stock_h, stock_m)
    else:
        logger.info("STOCK_REPORT_TIME 为空, 已禁用个股日报")

    # 优雅退出
    def _shutdown(signum: int, frame: Any) -> None:
        logger.info("收到信号 {}, 关闭调度器...", signum)
        scheduler.shutdown(wait=False)
        raise SystemExit(0)

    signal.signal(signal.SIGINT, _shutdown)
    signal.signal(signal.SIGTERM, _shutdown)

    scheduler.start()
    logger.info("调度器已启动,等待触发... (按 Ctrl+C 退出)")

    # 启动时检查是否有因重启/宕机错过的定时任务,30 分钟内补跑
    now = datetime.now()
    for hour, minute in times:
        scheduled = now.replace(hour=hour, minute=minute, second=0, microsecond=0)
        missed_minutes = (now - scheduled).total_seconds() / 60
        if 0 < missed_minutes < 30:
            logger.warning(
                "检测到错过的定时任务 {:02d}:{:02d} ({} 分钟前),立即补跑一次",
                hour, minute, int(missed_minutes),
            )
            steps = [k for k in STEP_COMMANDS if k != "report"]
            if (hour, minute) == sorted_times[0]:
                steps.append("report")
            run_pipeline(date.today().strftime("%Y%m%d"), steps=steps)

    import contextlib

    with contextlib.suppress(SystemExit, KeyboardInterrupt):
        # 保持主线程存活,直到收到退出信号
        signal.pause()

    return 0


def main() -> int:
    parser = argparse.ArgumentParser(description="A 股新闻定时任务 (M7)")
    parser.add_argument("--once", action="store_true", help="立即执行一次全链路")
    parser.add_argument(
        "--date", default=date.today().strftime("%Y%m%d"),
        help="日期 YYYYMMDD (仅 --once 模式)",
    )
    parser.add_argument("--steps", default=None,
                        help="仅执行指定步骤,逗号分隔 (如 crawler,extractor)")
    parser.add_argument("--log-level", default="INFO")
    args = parser.parse_args()

    _setup_logger(args.log_level)
    load_dotenv()

    if args.once:
        return _once(args)
    return _daemon(args)


if __name__ == "__main__":
    raise SystemExit(main())
