#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称：scheduler_service.py
模块描述：UI 自动化定时任务调度引擎

作者：czj
创建日期：2026-08-19
最后修改：2026-08-19

调度策略：
- 应用启动时（lifespan）调用 start_scheduler() 启动一个 asyncio 后台循环，
  每 30 秒轮询数据库中的启用定时任务，到期则触发执行；
- 触发通过 asyncio.create_task 异步执行，不阻塞调度循环；
- 复用 ExecutionService.create_execution()（trigger_type=scheduled,
  trigger_by=任务名）与 run_execution()（全局串行锁），不新增执行路径；
- 到期时若已有 UI 执行在运行，跳过本次（不产生失败记录），下次周期再触发；
- 执行完成后若任务开启 auto_report，自动生成 HTML 报告到报告中心。
"""

import asyncio
import logging
from datetime import datetime, timedelta
from typing import Optional, List

from sqlalchemy import select

from app.database import async_session_maker
from app.models.scheduled_task import ScheduledTask
from app.models.test_case import TestCase
from app.models.execution import Execution
from app.services.execution_service import ExecutionService, is_execution_running
from app.services.report_service import ReportService
from app.utils.id_generator import generate_id

logger = logging.getLogger(__name__)

# 调度轮询间隔（秒）
POLL_INTERVAL_SECONDS = 30

# 正在触发/执行中的任务ID集合，防止同一任务重复触发
_running_tasks: set = set()


# ==================== 周期计算 ====================


def compute_next_run(
    task: ScheduledTask,
    now: Optional[datetime] = None,
) -> Optional[datetime]:
    """
    计算任务的下次触发时间

    Args:
        task (ScheduledTask): 定时任务
        now (Optional[datetime]): 参考时间，默认当前时间

    Returns:
        Optional[datetime]: 下次触发时间；周期配置非法时返回 None
    """
    now = now or datetime.now()

    if task.schedule_type == "interval":
        unit_seconds = {
            "minutes": 60,
            "hours": 3600,
            "days": 86400,
        }.get(task.interval_unit, 60)
        value = int(task.interval_value or 0)
        if value <= 0:
            value = 1  # 兜底：非法间隔按 1 处理
        return now + timedelta(seconds=value * unit_seconds)

    if task.schedule_type == "daily":
        try:
            hour, minute = map(int, (task.run_time or "00:00").split(":"))
        except ValueError:
            return None
        candidate = now.replace(hour=hour, minute=minute, second=0, microsecond=0)
        if candidate <= now:
            candidate += timedelta(days=1)
        return candidate

    if task.schedule_type == "weekly":
        try:
            hour, minute = map(int, (task.run_time or "00:00").split(":"))
        except ValueError:
            return None
        weekdays = [int(w) for w in (task.weekdays or []) if isinstance(w, int) or str(w).isdigit()]
        weekdays = [w for w in weekdays if 0 <= w <= 6]
        if not weekdays:
            return None
        today = now.replace(hour=hour, minute=minute, second=0, microsecond=0)
        for offset in range(8):  # 未来 7 天内必然包含全部 7 个 weekday
            candidate = today + timedelta(days=offset)
            if candidate.weekday() in weekdays and candidate > now:
                return candidate
        return None

    return None


# ==================== 触发流程 ====================


async def collect_module_cases(
    db,
    module_ids: List[str],
    case_type: str = "ui",
) -> List[TestCase]:
    """
    收集选中模块下所有启用的测试用例（按模块分组 + order 排序）

    Args:
        db: 异步数据库会话
        module_ids (List[str]): 模块 ID 列表
        case_type (str): 用例类型（ui/security）

    Returns:
        List[TestCase]: 用例列表
    """
    if not module_ids:
        return []
    query = (
        select(TestCase)
        .where(
            TestCase.module_id.in_(module_ids),
            TestCase.status == "active",
            TestCase.case_type == case_type,
        )
        .order_by(TestCase.module_id, TestCase.order, TestCase.created_at)
    )
    result = await db.execute(query)
    return list(result.scalars().all())


async def _has_running_security_execution(db) -> bool:
    """检查是否已有安全测试执行在运行，避免定时任务并发压垮目标系统。"""
    result = await db.execute(
        select(Execution.id).where(
            Execution.case_type == "security",
            Execution.status.in_(["pending", "running"]),
        ).limit(1)
    )
    return result.scalar_one_or_none() is not None


async def _run_security_task_once(
    task: ScheduledTask,
    db,
) -> Optional[str]:
    """
    安全测试定时任务触发流程

    收集安全测试用例 → 创建执行记录（SecurityService）→ 运行 → 自动生成报告 → 推进调度

    Args:
        task (ScheduledTask): 定时任务对象（已从数据库加载）
        db: 异步数据库会话

    Returns:
        Optional[str]: 本次创建的执行记录 ID；跳过/失败时返回 None
    """
    from app.services.security_service import SecurityService
    from app.services.security_report_service import SecurityReportService

    # 1. 收集安全测试用例
    cases = await collect_module_cases(db, task.module_ids or [], "security")
    if not cases:
        logger.warning(
            f"[定时任务] 安全任务「{task.name}」选中模块下无启用安全用例，跳过本次"
        )
        task.next_run_at = compute_next_run(task)
        await db.flush()
        return None

    # 2. 执行锁预检：已有安全测试执行在运行 → 跳过
    if await _has_running_security_execution(db):
        logger.info(
            f"[定时任务] 安全任务「{task.name}」到点但已有安全执行在运行，"
            f"跳过本次，下次周期再触发"
        )
        task.next_run_at = compute_next_run(task)
        await db.flush()
        return None

    # 3. 创建执行记录（通过 SecurityService）
    case_ids = [c.id for c in cases]
    security_service = SecurityService(db)
    run_name = f"{task.name}-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
    execution = await security_service.create_execution(
        config_id=task.config_id,
        module_ids=task.module_ids,
        case_ids=case_ids,
        name=run_name,
    )
    # 覆盖 trigger_type / trigger_by（SecurityService 默认 manual）
    execution.trigger_type = "scheduled"
    execution.trigger_by = task.name
    await db.commit()
    logger.info(
        f"[定时任务] 安全任务「{task.name}」触发执行: {execution.id}, "
        f"用例 {len(case_ids)} 个"
    )

    # 4. 运行执行（阻塞等待完成）
    run_ok = True
    try:
        await security_service.run_execution(execution.id)
    except Exception as e:
        run_ok = False
        logger.error(f"[定时任务] 安全任务「{task.name}」执行异常: {e}")

    # 5. 自动生成报告（执行成功后才生成）
    if task.auto_report and run_ok:
        try:
            report_service = SecurityReportService()
            await report_service.generate_report(execution.id, db)
            logger.info(
                f"[定时任务] 安全任务「{task.name}」报告已生成: {execution.id}"
            )
        except Exception as e:
            logger.error(
                f"[定时任务] 安全任务「{task.name}」生成报告失败: {e}"
            )

    # 6. 推进调度状态
    task.last_run_at = datetime.now()
    task.run_count = (task.run_count or 0) + 1
    task.next_run_at = compute_next_run(task)
    await db.flush()
    await db.commit()

    return execution.id


async def run_task_once(task_id: str) -> Optional[str]:
    """
    执行一次定时任务（创建执行记录 → 运行 → 自动生成报告 → 推进下次调度）

    供后台调度循环与「立即执行」接口共用。
    按任务 case_type 分流：ui 走原 Playwright 执行路径，security 走安全测试执行器路径。

    Args:
        task_id (str): 定时任务 ID

    Returns:
        Optional[str]: 本次创建的执行记录 ID；跳过/失败时返回 None
    """
    if task_id in _running_tasks:
        logger.info(f"[定时任务] 任务 {task_id} 正在执行中，忽略重复触发")
        return None

    _running_tasks.add(task_id)
    try:
        async with async_session_maker() as db:
            result = await db.execute(
                select(ScheduledTask).where(ScheduledTask.id == task_id)
            )
            task = result.scalar_one_or_none()
            if not task:
                logger.warning(f"[定时任务] 任务不存在: {task_id}")
                return None

            # 按 case_type 分流
            if task.case_type == "security":
                return await _run_security_task_once(task, db)

            # ===== 以下为 UI 自动化定时任务原有逻辑 =====
            cases = await collect_module_cases(db, task.module_ids or [])
            if not cases:
                logger.warning(
                    f"[定时任务] 任务「{task.name}」选中模块下无启用 UI 用例，跳过本次"
                )
                task.next_run_at = compute_next_run(task)
                await db.flush()
                return None

            # 2. 执行锁预检：已有 UI 执行在运行 → 跳过（不创建注定失败的记录）
            is_running, running_id = is_execution_running()
            if is_running:
                logger.info(
                    f"[定时任务] 任务「{task.name}」到点但已有执行 {running_id} 在运行，"
                    f"跳过本次，下次周期再触发"
                )
                task.next_run_at = compute_next_run(task)
                await db.flush()
                return None

            # 3. 创建执行记录（trigger_type=scheduled, trigger_by=任务名）
            case_ids = [c.id for c in cases]
            exec_service = ExecutionService(db)
            run_name = f"{task.name}-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
            execution = await exec_service.create_execution(
                case_ids=case_ids,
                name=run_name,
                trigger_type="scheduled",
                trigger_by=task.name,
                environment="default",
                config={"auto_report": task.auto_report},
            )
            await db.commit()
            logger.info(
                f"[定时任务] 任务「{task.name}」触发执行: {execution.id}, "
                f"用例 {len(case_ids)} 个"
            )

            # 4. 运行执行（阻塞等待完成；内部全局锁保证串行）
            run_ok = True
            try:
                await exec_service.run_execution(execution.id)
            except Exception as e:
                run_ok = False
                logger.error(f"[定时任务] 任务「{task.name}」执行异常: {e}")

            # 5. 自动生成报告（执行成功后才生成，避免冲突失败记录产出空报告）
            if task.auto_report and run_ok:
                try:
                    report_service = ReportService()
                    html = await report_service.generate_html(execution.id, db)
                    await report_service.save_report(execution.id, html)
                    logger.info(
                        f"[定时任务] 任务「{task.name}」报告已生成: {execution.id}"
                    )
                except Exception as e:
                    logger.error(
                        f"[定时任务] 任务「{task.name}」生成报告失败: {e}"
                    )

            # 6. 推进调度状态
            task.last_run_at = datetime.now()
            task.run_count = (task.run_count or 0) + 1
            task.next_run_at = compute_next_run(task)
            await db.flush()
            await db.commit()

            return execution.id
    except Exception as e:
        logger.error(f"[定时任务] 触发任务 {task_id} 失败: {e}")
        return None
    finally:
        _running_tasks.discard(task_id)


# ==================== 后台调度循环 ====================


async def check_and_trigger() -> None:
    """
    轮询一轮：找出到期任务并异步触发

    每轮独立会话，异常不影响调度循环。
    """
    try:
        due_ids: List[str] = []
        async with async_session_maker() as db:
            result = await db.execute(
                select(ScheduledTask).where(ScheduledTask.enabled == True)  # noqa: E712
            )
            tasks = list(result.scalars().all())

            now = datetime.now()
            for task in tasks:
                # 首次启用但未设置下次执行时间 → 立即计算（避免立刻触发）
                if task.next_run_at is None:
                    task.next_run_at = compute_next_run(task)
                elif task.next_run_at <= now:
                    due_ids.append(task.id)
            await db.commit()

        # 到期任务异步触发（不阻塞轮询循环）
        for task_id in due_ids:
            asyncio.create_task(run_task_once(task_id))
    except Exception as e:
        logger.error(f"[定时任务] 调度轮询异常: {e}")


async def scheduler_loop() -> None:
    """
    后台调度主循环（应用启动时由 lifespan 创建）

    每 POLL_INTERVAL_SECONDS 秒轮询一次到期任务。
    """
    logger.info(
        f"[定时任务] 调度引擎已启动，轮询间隔 {POLL_INTERVAL_SECONDS}s"
    )
    while True:
        try:
            await check_and_trigger()
        except asyncio.CancelledError:
            logger.info("[定时任务] 调度引擎已停止")
            raise
        except Exception as e:
            logger.error(f"[定时任务] 调度循环异常: {e}")
        await asyncio.sleep(POLL_INTERVAL_SECONDS)


def start_scheduler() -> asyncio.Task:
    """
    启动调度引擎（供 lifespan 调用）

    Returns:
        asyncio.Task: 调度任务句柄（用于关闭时取消）
    """
    return asyncio.create_task(scheduler_loop())
