# -*- coding: utf-8 -*-
"""
schedule_service.py — 定时任务管理（CRUD + APScheduler 集成）

数据存储：service_monitor/data/schedules.json
调度器：APScheduler BackgroundScheduler
"""

from __future__ import annotations

import json
import logging
import threading
import uuid
from datetime import datetime
from typing import Optional

from ..utils.paths import SCHEDULES_FILE, ensure_dirs
from utils.logger import get_logger

# 使用 troubleshoot 命名空间的 logger，确保日志正确输出
logger = get_logger("service_monitor.schedule")

# 北京时区常量（定时任务统一使用此时区，不受服务器系统时区影响）
from pytz import timezone as _pytz_timezone
SHANGHAI_TZ = _pytz_timezone("Asia/Shanghai")

# 全局调度器（延迟初始化）
_scheduler = None
_scheduler_lock = threading.Lock()

# schedules.json 读写锁（防止多线程并发读-改-写导致状态丢失）
# 使用 RLock（可重入锁）：_update_current_status 持有锁后调用 _save_schedules，
# _save_schedules 也需要获取锁，普通 Lock 会死锁，RLock 允许同一线程重入
_schedules_lock = threading.RLock()

# 星期映射：数字 → 中文名
WEEKDAY_NAMES = {1: "一", 2: "二", 3: "三", 4: "四", 5: "五", 6: "六", 7: "日"}


def _build_cron(repeat_mode: str, hour: int, minute: int, weekdays: list | None = None) -> str:
    """将友好调度字段转换为 Cron 表达式。

    Args:
        repeat_mode: daily / weekday / weekly
        hour: 0-23
        minute: 0-59
        weekdays: 星期几列表（1=周一…7=周日），仅 weekly 模式

    Returns:
        标准 5 段 Cron 表达式
    """
    h = max(0, min(23, int(hour)))
    m = max(0, min(59, int(minute)))

    if repeat_mode == "daily":
        return f"{m} {h} * * *"
    elif repeat_mode == "weekday":
        return f"{m} {h} * * 1-5"
    elif repeat_mode == "weekly":
        if not weekdays:
            raise ValueError("每周模式请选择至少一个星期")
        # 排序去重，确保合法
        days = sorted(set(int(d) for d in weekdays if 1 <= int(d) <= 7))
        if not days:
            raise ValueError("每周模式请选择至少一个星期")
        dow = ",".join(str(d) for d in days)
        return f"{m} {h} * * {dow}"
    else:
        raise ValueError(f"不支持的重复周期: {repeat_mode}")


def _describe_schedule(sched: dict) -> str:
    """生成定时任务的自然语言描述。"""
    mode = sched.get("repeat_mode", "")
    hour = sched.get("hour")
    minute = sched.get("minute", 0)
    weekdays = sched.get("weekdays", [])
    start_date = sched.get("start_date")
    end_date = sched.get("end_date")

    # 兼容旧数据：无 repeat_mode 时从 cron 反推
    if not mode:
        return _describe_cron(sched.get("cron", ""))

    time_str = f"{int(hour):02d}:{int(minute):02d}" if hour is not None else "???"

    if mode == "daily":
        desc = f"每天 {time_str}"
    elif mode == "weekday":
        desc = f"工作日 {time_str}"
    elif mode == "weekly":
        if weekdays:
            day_names = "、".join(f"周{WEEKDAY_NAMES.get(d, str(d))}" for d in sorted(weekdays))
            desc = f"每{day_names} {time_str}"
        else:
            desc = f"每周 {time_str}"
    else:
        desc = sched.get("cron", "")

    # 追加起止日期
    date_parts = []
    if start_date:
        date_parts.append(start_date)
    if end_date:
        date_parts.append(end_date)
    if date_parts:
        desc += f"（{' 至 '.join(date_parts)}）"

    return desc


def _describe_cron(cron_expr: str) -> str:
    """Best-effort 从 Cron 表达式生成描述（兼容旧数据）。"""
    if not cron_expr:
        return "-"
    parts = cron_expr.split()
    if len(parts) != 5:
        return cron_expr
    minute, hour, _, _, dow = parts
    time_str = f"{int(hour):02d}:{int(minute):02d}" if hour.isdigit() and minute.isdigit() else f"{hour}:{minute}"
    if dow == "*":
        return f"每天 {time_str}"
    elif dow == "1-5":
        return f"工作日 {time_str}"
    else:
        return cron_expr


def _load_schedules() -> list:
    """加载定时任务列表。"""
    if not SCHEDULES_FILE.exists():
        return []
    try:
        data = json.loads(SCHEDULES_FILE.read_text(encoding="utf-8"))
        return data if isinstance(data, list) else []
    except (json.JSONDecodeError, OSError) as e:
        logger.error("加载 schedules.json 失败: %s", e)
        return []


def _save_schedules(schedules: list) -> None:
    """保存定时任务列表。"""
    with _schedules_lock:
        ensure_dirs()
        SCHEDULES_FILE.write_text(
            json.dumps(schedules, ensure_ascii=False, indent=2),
            encoding="utf-8",
        )


def _now() -> str:
    """返回当前北京时间字符串（所有时间戳统一使用北京时区）。"""
    return datetime.now(SHANGHAI_TZ).strftime("%Y-%m-%dT%H:%M:%S")


def _calc_next_run(cron_expr: str) -> Optional[str]:
    """计算下次执行时间（北京时间）。"""
    try:
        from croniter import croniter
        base = datetime.now(SHANGHAI_TZ)
        iter = croniter(cron_expr, base)
        next_dt = iter.get_next(datetime)
        # 确保返回北京时间字符串
        if next_dt.tzinfo:
            next_dt = next_dt.astimezone(SHANGHAI_TZ)
        return next_dt.strftime("%Y-%m-%dT%H:%M:%S")
    except Exception as e:
        logger.warning("计算下次执行时间失败: %s", e)
        return None


# ============================================================
# CRUD
# ============================================================

def list_schedules() -> list:
    """列出所有定时任务。"""
    return _load_schedules()


def get_schedule(schedule_id: str) -> Optional[dict]:
    """获取单个定时任务。"""
    for s in _load_schedules():
        if s.get("id") == schedule_id:
            return s
    return None


def create_schedule(data: dict, created_by: str) -> dict:
    """创建定时任务。"""
    # 校验必填字段
    name = data.get("name", "").strip()
    target_id = data.get("target_id", "").strip()
    suite = data.get("suite", "quick")
    repeat_mode = data.get("repeat_mode", "daily")
    hour = data.get("hour", 8)
    minute = data.get("minute", 0)
    weekdays = data.get("weekdays", [])
    start_date = data.get("start_date") or None
    end_date = data.get("end_date") or None

    if not name:
        raise ValueError("任务名称不能为空")
    if not target_id:
        raise ValueError("请选择监测目标")

    # 校验 repeat_mode
    if repeat_mode not in ("daily", "weekday", "weekly"):
        raise ValueError("重复周期仅支持：每天、工作日、每周")

    # 校验时间
    try:
        hour = int(hour)
        minute = int(minute)
        if not (0 <= hour <= 23 and 0 <= minute <= 59):
            raise ValueError
    except (ValueError, TypeError):
        raise ValueError("时间格式无效，小时 0-23，分钟 0-59")

    # 校验星期（weekly 模式）
    if repeat_mode == "weekly":
        if not weekdays:
            raise ValueError("每周模式请选择至少一个星期")
        weekdays = [int(d) for d in weekdays if 1 <= int(d) <= 7]
        if not weekdays:
            raise ValueError("每周模式请选择至少一个星期")

    # 校验起止日期
    if end_date and start_date and end_date < start_date:
        raise ValueError("失效日期必须晚于生效日期")

    # 生成 Cron 表达式
    cron_expr = _build_cron(repeat_mode, hour, minute, weekdays)

    # 生成 ID
    schedule_id = "sched_" + str(uuid.uuid4())[:12]

    # 获取目标名称
    from . import target_service
    target = target_service.get_target(target_id)
    target_name = target.get("name") if target else target_id

    schedule = {
        "id": schedule_id,
        "name": name,
        "target_id": target_id,
        "target_name": target_name,
        "suite": suite,
        "cron": cron_expr,
        "repeat_mode": repeat_mode,
        "hour": hour,
        "minute": minute,
        "weekdays": weekdays,
        "start_date": start_date,
        "end_date": end_date,
        "enabled": True,
        "current_status": "idle",  # 初始化执行状态
        "last_run_at": None,
        "last_run_status": None,
        "last_report_id": None,
        "next_run_at": _calc_next_run(cron_expr),
        "created_at": _now(),
        "created_by": created_by,
    }

    schedules = _load_schedules()
    schedules.append(schedule)
    _save_schedules(schedules)

    logger.info("创建定时任务: %s (%s)", name, cron_expr)
    return schedule


def update_schedule(schedule_id: str, data: dict) -> dict:
    """更新定时任务。"""
    schedules = _load_schedules()
    for i, s in enumerate(schedules):
        if s.get("id") == schedule_id:
            # 可更新字段
            if "name" in data:
                name = data["name"].strip()
                if not name:
                    raise ValueError("任务名称不能为空")
                schedules[i]["name"] = name

            # 调度字段更新：只要传了 repeat_mode 就重算 cron
            repeat_mode = data.get("repeat_mode", s.get("repeat_mode", "daily"))
            hour = data.get("hour", s.get("hour", 8))
            minute = data.get("minute", s.get("minute", 0))
            weekdays = data.get("weekdays", s.get("weekdays", []))

            if "repeat_mode" in data or "hour" in data or "minute" in data or "weekdays" in data:
                # 校验
                if repeat_mode not in ("daily", "weekday", "weekly"):
                    raise ValueError("重复周期仅支持：每天、工作日、每周")
                try:
                    hour = int(hour)
                    minute = int(minute)
                    if not (0 <= hour <= 23 and 0 <= minute <= 59):
                        raise ValueError
                except (ValueError, TypeError):
                    raise ValueError("时间格式无效")
                if repeat_mode == "weekly":
                    weekdays = [int(d) for d in weekdays if 1 <= int(d) <= 7]
                    if not weekdays:
                        raise ValueError("每周模式请选择至少一个星期")

                cron_expr = _build_cron(repeat_mode, hour, minute, weekdays)
                schedules[i]["repeat_mode"] = repeat_mode
                schedules[i]["hour"] = hour
                schedules[i]["minute"] = minute
                schedules[i]["weekdays"] = weekdays
                schedules[i]["cron"] = cron_expr
                schedules[i]["next_run_at"] = _calc_next_run(cron_expr)

            # 起止日期
            if "start_date" in data:
                schedules[i]["start_date"] = data["start_date"] or None
            if "end_date" in data:
                schedules[i]["end_date"] = data["end_date"] or None

            # 校验起止日期
            sd = schedules[i].get("start_date")
            ed = schedules[i].get("end_date")
            if ed and sd and ed < sd:
                raise ValueError("失效日期必须晚于生效日期")

            if "suite" in data:
                schedules[i]["suite"] = data["suite"]

            _save_schedules(schedules)
            logger.info("更新定时任务: %s", schedule_id)
            return schedules[i]

    raise ValueError("定时任务不存在")


def delete_schedule(schedule_id: str) -> bool:
    """删除定时任务。"""
    schedules = _load_schedules()
    new_schedules = [s for s in schedules if s.get("id") != schedule_id]
    if len(new_schedules) == len(schedules):
        return False
    _save_schedules(new_schedules)
    logger.info("删除定时任务: %s", schedule_id)
    return True


def toggle_enabled(schedule_id: str, enabled: Optional[bool] = None) -> dict:
    """切换定时任务启用/禁用状态。"""
    schedules = _load_schedules()
    for i, s in enumerate(schedules):
        if s.get("id") == schedule_id:
            new_enabled = not s.get("enabled", True) if enabled is None else enabled
            schedules[i]["enabled"] = new_enabled
            if new_enabled:
                schedules[i]["next_run_at"] = _calc_next_run(s["cron"])
            _save_schedules(schedules)
            logger.info("切换定时任务状态: %s -> %s", schedule_id, new_enabled)
            return schedules[i]
    raise ValueError("定时任务不存在")


def update_run_status(schedule_id: str, status: str, report_id: str, run_at: str) -> dict:
    """更新定时任务执行状态。"""
    with _schedules_lock:
        schedules = _load_schedules()
        for i, s in enumerate(schedules):
            if s.get("id") == schedule_id:
                schedules[i]["last_run_at"] = run_at
                schedules[i]["last_run_status"] = status
                schedules[i]["last_report_id"] = report_id
                schedules[i]["next_run_at"] = _calc_next_run(s["cron"])
                _save_schedules(schedules)
                return schedules[i]
    raise ValueError("定时任务不存在")


# ============================================================
# APScheduler 集成
# ============================================================

def init_scheduler(app) -> None:
    """初始化 APScheduler，在 Flask app 创建后调用。

    Werkzeug reloader 防护：
    Flask debug 模式下，reloader 会 fork 子进程。父进程（监控进程）中
    WERKZEUG_RUN_MAIN 未设置，子进程（实际服务进程）中为 'true'。
    我们只在子进程中初始化 APScheduler，避免在即将被丢弃的父进程中启动。
    非 debug 模式下没有 reloader，此判断不影响。
    """
    global _scheduler
    with _scheduler_lock:
        if _scheduler is not None:
            logger.info("APScheduler 已初始化，跳过重复初始化")
            return

        import os
        is_reloader_parent = os.environ.get('WERKZEUG_RUN_MAIN') != 'true'
        # 注意：默认值必须为 '0'，与 server.py 保持一致
        # FLASK_DEBUG 未设置时 = 生产模式，应直接初始化 scheduler
        is_debug = os.environ.get('FLASK_DEBUG', '0') == '1'
        if is_debug and is_reloader_parent:
            logger.info("APScheduler 跳过 reloader 父进程初始化（将在子进程中初始化）")
            return

        try:
            from apscheduler.schedulers.background import BackgroundScheduler
            from apscheduler.events import EVENT_JOB_EXECUTED, EVENT_JOB_ERROR, EVENT_JOB_MISSED

            _scheduler = BackgroundScheduler()

            # 添加事件监听器，用于调试
            def job_executed(event):
                logger.info("定时任务执行成功: job_id=%s", event.job_id)

            def job_error(event):
                logger.error("定时任务执行失败: job_id=%s, exception=%s", event.job_id, event.exception)

            def job_missed(event):
                logger.warning("定时任务错过执行: job_id=%s", event.job_id)

            _scheduler.add_listener(job_executed, EVENT_JOB_EXECUTED)
            _scheduler.add_listener(job_error, EVENT_JOB_ERROR)
            _scheduler.add_listener(job_missed, EVENT_JOB_MISSED)

            _scheduler.start()
            logger.info("=" * 50)
            logger.info("APScheduler 已启动")
            logger.info("调度器状态: running=%s", _scheduler.running)
            logger.info("=" * 50)

            # 加载已启用的定时任务
            _reload_jobs()

            # 注册每日报告缺失检测任务
            _register_missing_report_job()

            # 验证任务注册情况
            jobs = _scheduler.get_jobs()
            logger.info("已注册任务数: %d", len(jobs))
            for job in jobs:
                logger.info("  - %s: next_run=%s", job.id, job.next_run_time)

        except ImportError as e:
            logger.warning("APScheduler 未安装，定时任务功能不可用: %s", e)
        except Exception as e:
            logger.exception("APScheduler 初始化失败: %s", e)


def _reload_jobs() -> None:
    """从 schedules.json 重新加载所有任务。"""
    if _scheduler is None:
        return

    schedules = list_schedules()
    for s in schedules:
        if s.get("enabled"):
            _add_job(s)
        else:
            _remove_job(s["id"])


def _add_job(sched: dict) -> None:
    """添加或更新单个 APScheduler job。"""
    if _scheduler is None:
        logger.warning("调度器未初始化，无法注册任务: %s", sched.get('id'))
        return

    from apscheduler.triggers.cron import CronTrigger

    job_id = f"schedule_{sched['id']}"
    try:
        # 使用北京时区解析 cron 表达式（不受服务器系统时区影响）
        trigger = CronTrigger.from_crontab(sched["cron"], timezone=SHANGHAI_TZ)

        # 支持起止日期（使用北京时区）
        start_date = sched.get("start_date")
        end_date = sched.get("end_date")
        if start_date:
            # 转为北京时区的 datetime
            trigger.start_date = SHANGHAI_TZ.localize(
                datetime.strptime(start_date, "%Y-%m-%d")
            )
        if end_date:
            # end_date 当天也要执行，设为当天 23:59:59 北京时间
            trigger.end_date = SHANGHAI_TZ.localize(
                datetime.strptime(end_date, "%Y-%m-%d").replace(
                    hour=23, minute=59, second=59
                )
            )

        job = _scheduler.add_job(
            func=_execute_scheduled_job,
            trigger=trigger,
            id=job_id,
            args=[sched["id"]],
            replace_existing=True,
            misfire_grace_time=3600,  # 允许 1 小时内的错过执行（默认 1 秒太短）
            coalesce=True,            # 错过多次只执行一次
            max_instances=1,          # 同一 job 不并发（显式声明）
        )
        logger.info("定时任务已注册: %s (%s) [Asia/Shanghai]", sched["name"], sched["cron"])
        logger.info("  - job_id: %s", job_id)
        logger.info("  - next_run_time: %s", job.next_run_time)
        logger.info("  - trigger: %s", job.trigger)
        logger.info("  - start_date: %s, end_date: %s", start_date, end_date)
    except Exception as e:
        logger.exception("注册定时任务失败: %s - %s", sched["id"], e)


def _remove_job(schedule_id: str) -> None:
    """移除 APScheduler job。"""
    if _scheduler is None:
        return

    job_id = f"schedule_{schedule_id}"
    try:
        _scheduler.remove_job(job_id)
        logger.info("定时任务已移除: %s", schedule_id)
    except Exception:
        pass  # job 可能不存在


def _run_job_body(schedule_id: str) -> None:
    """定时任务执行体（不含 enabled 检查，供 APScheduler 和 run_now 共用）。"""
    logger.info("=" * 50)
    logger.info("定时任务执行开始: %s", schedule_id)
    logger.info("=" * 50)

    sched = get_schedule(schedule_id)
    if not sched:
        logger.warning("定时任务不存在: %s", schedule_id)
        return

    logger.info("任务详情: name=%s, target_id=%s, suite=%s",
                sched.get("name"), sched.get("target_id"), sched.get("suite"))

    # 标记执行中
    _update_current_status(schedule_id, "running")
    logger.info("已标记状态: running")

    try:
        from . import runner_service
        logger.info("开始执行巡检...")
        result = runner_service.run_inspection_sync(
            sched["target_id"], sched["suite"]
        )
        logger.info("巡检执行完成: success=%s, report_id=%s",
                    result.get("success"), result.get("report_id"))

        update_run_status(
            schedule_id,
            status="success" if result.get("success") else "failed",
            report_id=result.get("report_id", ""),
            run_at=_now(),
        )
        _update_current_status(schedule_id, "success" if result.get("success") else "failed")
        logger.info("定时任务执行完成: %s, report_id=%s", schedule_id, result.get("report_id"))
    except Exception as e:
        logger.exception("定时任务执行异常: %s - %s", schedule_id, e)
        update_run_status(
            schedule_id,
            status="error",
            report_id="",
            run_at=_now(),
        )
        _update_current_status(schedule_id, "error")  # 与 last_run_status 一致
        logger.error("已标记状态: error")


def _execute_scheduled_job(schedule_id: str) -> None:
    """定时任务执行入口（APScheduler 线程中调用），含 enabled 检查。"""
    logger.info("=" * 50)
    logger.info("APScheduler 触发定时任务: %s", schedule_id)
    logger.info("当前时间(北京): %s", _now())
    logger.info("=" * 50)

    sched = get_schedule(schedule_id)
    if not sched:
        logger.warning("定时任务不存在: %s", schedule_id)
        return

    if not sched.get("enabled"):
        logger.warning("定时任务已禁用，跳过执行: %s", schedule_id)
        return

    # 检查是否已在执行中（防止定时触发与手动触发并发）
    if sched.get("current_status") == "running":
        logger.warning("定时任务已在执行中，跳过: %s", schedule_id)
        return

    logger.info("任务信息: name=%s, cron=%s, target_id=%s",
                sched.get("name"), sched.get("cron"), sched.get("target_id"))

    # 使用后台线程执行，不阻塞 APScheduler 调度线程
    logger.info("启动后台线程执行任务...")
    t = threading.Thread(target=_run_job_body, args=(schedule_id,), daemon=True)
    t.start()
    logger.info("后台线程已启动: %s", t.name)


def run_now(schedule_id: str) -> dict:
    """手动立即执行一次定时任务（后台线程，立即返回，不阻塞 HTTP 请求）。

    巡检可能耗时数分钟（单模块 90s 超时），故用后台线程执行。
    不检查 enabled —— 手动执行是显式动作，禁用状态也应能跑。
    页面通过 current_status 轮询感知 running → 完成。
    """
    logger.info("手动触发定时任务请求: %s", schedule_id)

    sched = get_schedule(schedule_id)
    if not sched:
        logger.warning("定时任务不存在: %s", schedule_id)
        raise ValueError("定时任务不存在")

    # 避免重复触发：已在执行中则跳过
    if sched.get("current_status") == "running":
        logger.warning("任务正在执行中，跳过: %s", schedule_id)
        raise ValueError("该任务正在执行中，请稍后刷新查看")

    logger.info("启动后台线程执行任务: name=%s", sched.get("name"))
    t = threading.Thread(target=_run_job_body, args=(schedule_id,), daemon=True)
    t.start()
    logger.info("后台线程已启动: %s", t.name)

    return {"schedule_id": schedule_id, "triggered": True, "thread": t.name}


def reload_job(schedule_id: str) -> None:
    """重新加载单个定时任务（CRUD 后调用）。"""
    if _scheduler is None:
        return

    sched = get_schedule(schedule_id)
    if sched and sched.get("enabled"):
        _add_job(sched)
    else:
        _remove_job(schedule_id)


def get_scheduler_status() -> dict:
    """获取调度器状态。"""
    if _scheduler is None:
        return {"running": False, "reason": "APScheduler 未初始化"}
    return {
        "running": _scheduler.running,
        "job_count": len(_scheduler.get_jobs()),
    }


def _update_current_status(schedule_id: str, status: str) -> None:
    """更新定时任务当前执行状态（running/success/failed）。"""
    with _schedules_lock:
        schedules = _load_schedules()
        for i, s in enumerate(schedules):
            if s.get("id") == schedule_id:
                schedules[i]["current_status"] = status
                _save_schedules(schedules)
                return
    raise ValueError("定时任务不存在")


def _check_missing_reports_job() -> None:
    """每日报告缺失检测任务（APScheduler 调用）。

    遍历所有目标，检查是否长时间未产生巡检报告。
    若配置了 report_missing 告警且存在缺失目标，发送通知。
    """
    try:
        from . import notification_service, report_service

        config = notification_service.get_config()
        rm_cfg = config.get("report_missing", {})
        if not rm_cfg.get("enabled"):
            logger.info("报告缺失告警未启用，跳过检测")
            return

        missing_days = int(rm_cfg.get("missing_days", 2) or 2)
        missing = report_service.check_missing_reports(missing_days=missing_days)

        if not missing:
            logger.info("报告缺失检测：无目标缺失报告")
            return

        logger.warning("报告缺失检测：%d 个目标无报告", len(missing))
        notification_service.send_missing_report_notification(missing, rm_cfg)
    except Exception as e:
        logger.exception("报告缺失检测异常: %s", e)


MISSING_REPORT_CHECK_CRON = "30 9 * * *"  # 每天 09:30（北京时区）


def _register_missing_report_job() -> None:
    """注册每日报告缺失检测定时任务。"""
    if _scheduler is None:
        return
    try:
        from apscheduler.triggers.cron import CronTrigger
        _scheduler.add_job(
            func=_check_missing_reports_job,
            trigger=CronTrigger.from_crontab(MISSING_REPORT_CHECK_CRON, timezone=SHANGHAI_TZ),
            id="missing_report_check",
            replace_existing=True,
            coalesce=True,
            max_instances=1,
            misfire_grace_time=3600,
        )
        logger.info("报告缺失检测任务已注册: %s [Asia/Shanghai]", MISSING_REPORT_CHECK_CRON)
    except Exception as e:
        logger.exception("注册报告缺失检测任务失败: %s", e)
