# -*- coding: utf-8 -*-
"""CL CronTaskService durable 定时任务（Phase 8 Tier 2）

借鉴 zhikuncode CronTaskService.java：
- 内存 + durable 文件持久化（默认 ~/.abyss/scheduled_tasks.json）
- 原子写（.tmp + os.replace），避免写入中断导致文件损坏
- 上限 50 条（超出自动淘汰最老）
- 30 天过期自动清理（按 created_at 判断）
- 线程安全（所有公开方法加锁）

与 curator.py 集成：CuratorDaemon 每 60s 调 `cron_service.trigger_due()`，
返回到期的命令列表交由主循环执行。
"""
from __future__ import annotations

import json
import logging
import os
import threading
import time
import uuid
from dataclasses import dataclass, field
from pathlib import Path
from typing import List, Optional

logger = logging.getLogger(__name__)

# 默认参数
DEFAULT_PERSIST_PATH = "~/.abyss/scheduled_tasks.json"
DEFAULT_MAX_TASKS = 50
DEFAULT_EXPIRE_DAYS = 30


@dataclass
class DurableCronTask:
    """单条 durable 定时任务记录。"""

    id: str
    interval_seconds: float
    command: str  # slash 命令或 shell 命令
    next_run: float
    created_at: float
    last_run: Optional[float] = None
    last_result: Optional[str] = None

    def to_dict(self) -> dict:
        """序列化为 dict（用于 JSON 持久化）。"""
        return {
            "id": self.id,
            "interval_seconds": self.interval_seconds,
            "command": self.command,
            "next_run": self.next_run,
            "created_at": self.created_at,
            "last_run": self.last_run,
            "last_result": self.last_result,
        }

    @classmethod
    def from_dict(cls, data: dict) -> "DurableCronTask":
        """从 dict 反序列化。缺失字段使用默认值，保证向前兼容。"""
        return cls(
            id=data["id"],
            interval_seconds=float(data.get("interval_seconds", 0)),
            command=data.get("command", ""),
            next_run=float(data.get("next_run", 0)),
            created_at=float(data.get("created_at", 0)),
            last_run=data.get("last_run"),
            last_result=data.get("last_result"),
        )


class CronTaskService:
    """durable 定时任务管理器。

    所有公开方法线程安全。
    持久化路径默认 `~/.abyss/scheduled_tasks.json`，原子写避免损坏。
    """

    def __init__(
        self,
        persist_path: str = DEFAULT_PERSIST_PATH,
        max_tasks: int = DEFAULT_MAX_TASKS,
        expire_days: int = DEFAULT_EXPIRE_DAYS,
    ):
        """初始化 CronTaskService。

        Args:
            persist_path: 持久化文件路径，~ 会被展开
            max_tasks: 任务上限，超出时淘汰 created_at 最老的
            expire_days: 过期天数，超过则被 _purge_expired 清理
        """
        self._persist_path = str(Path(persist_path).expanduser())
        self._max_tasks = max_tasks
        self._expire_days = expire_days
        self._expire_seconds = expire_days * 86400
        # 复用锁：所有公开方法持有此锁
        self._lock = threading.RLock()
        # 内存任务列表
        self._tasks: List[DurableCronTask] = []
        # 启动时加载持久化数据
        self._load()

    # -- 公开接口 --

    def create(self, interval: int, command: str) -> str:
        """创建定时任务。

        Args:
            interval: 触发间隔（秒）
            command: 触发时执行的命令（slash 或 shell）

        Returns:
            新任务 id（uuid4 hex）
        """
        now = time.time()
        task = DurableCronTask(
            id=uuid.uuid4().hex,
            interval_seconds=float(interval),
            command=command,
            next_run=now + float(interval),
            created_at=now,
        )
        with self._lock:
            self._tasks.append(task)
            # 超出上限淘汰最老的（按 created_at 升序）
            self._enforce_max()
            self._persist()
        logger.debug(f"CronTaskService: created task {task.id} cmd={command}")
        return task.id

    def list(self) -> List[DurableCronTask]:
        """返回所有任务（浅拷贝列表，调用方可安全修改）。"""
        with self._lock:
            return list(self._tasks)

    def delete(self, task_id: str) -> bool:
        """删除指定 id 的任务。

        Returns:
            True=已删除，False=未找到
        """
        with self._lock:
            before = len(self._tasks)
            self._tasks = [t for t in self._tasks if t.id != task_id]
            if len(self._tasks) == before:
                return False
            self._persist()
            return True

    def trigger_due(self) -> List[str]:
        """触发所有到期任务（next_run <= now）。

        - 执行任务时调用 executor（如有）
        - 更新 last_run / last_result
        - 重新计算 next_run = now + interval
        - 持久化变更

        Returns:
            被触发的命令列表（用于主循环执行）
        """
        now = time.time()
        triggered_commands: List[str] = []
        with self._lock:
            for task in self._tasks:
                if task.next_run <= now:
                    # 触发
                    triggered_commands.append(task.command)
                    task.last_run = now
                    task.last_result = ""  # 实际结果由执行方回填
                    # 重新计算下次运行时间
                    task.next_run = now + task.interval_seconds
            if triggered_commands:
                self._persist()
        return triggered_commands

    # -- 内部方法（均在 _lock 内调用） --

    def _enforce_max(self) -> None:
        """超出上限时淘汰 created_at 最老的任务。

        必须在 _lock 内调用。
        """
        if len(self._tasks) <= self._max_tasks:
            return
        # 按 created_at 升序排序，淘汰最老的
        self._tasks.sort(key=lambda t: t.created_at)
        overflow = len(self._tasks) - self._max_tasks
        # 保留最新的 max_tasks 条
        self._tasks = self._tasks[overflow:]

    def _purge_expired(self) -> None:
        """清理过期任务（created_at 超过 expire_days）。

        必须在 _lock 内调用。
        """
        now = time.time()
        cutoff = now - self._expire_seconds
        before = len(self._tasks)
        self._tasks = [t for t in self._tasks if t.created_at >= cutoff]
        purged = before - len(self._tasks)
        if purged > 0:
            logger.info(f"CronTaskService: purged {purged} expired tasks")
            self._persist()

    def _persist(self) -> None:
        """原子写入持久化文件（.tmp + os.replace）。

        必须在 _lock 内调用。
        """
        path = self._persist_path
        tmp_path = path + ".tmp"
        try:
            # 确保父目录存在
            parent = os.path.dirname(path)
            if parent:
                os.makedirs(parent, exist_ok=True)
            # 序列化
            data = [t.to_dict() for t in self._tasks]
            content = json.dumps(data, indent=2, ensure_ascii=False)
            # 写临时文件
            with open(tmp_path, "w", encoding="utf-8") as f:
                f.write(content)
                f.flush()
                os.fsync(f.fileno())
            # 原子替换
            os.replace(tmp_path, path)
        except Exception as exc:
            logger.error(f"CronTaskService: persist failed: {exc}")
            # 清理临时文件
            try:
                if os.path.exists(tmp_path):
                    os.unlink(tmp_path)
            except Exception:
                pass

    def _load(self) -> None:
        """从持久化文件加载任务列表。

        文件不存在或损坏时从空开始，不抛异常。
        启动时调用一次。
        """
        path = self._persist_path
        if not os.path.exists(path):
            self._tasks = []
            return
        try:
            with open(path, "r", encoding="utf-8-sig") as f:
                data = json.load(f)
            if not isinstance(data, list):
                self._tasks = []
                return
            tasks: List[DurableCronTask] = []
            for item in data:
                if not isinstance(item, dict):
                    continue
                try:
                    tasks.append(DurableCronTask.from_dict(item))
                except (KeyError, ValueError, TypeError) as exc:
                    logger.warning(
                        f"CronTaskService: skip malformed task entry: {exc}"
                    )
                    continue
            self._tasks = tasks
        except (json.JSONDecodeError, OSError) as exc:
            logger.warning(
                f"CronTaskService: load failed, starting fresh: {exc}"
            )
            self._tasks = []

    # -- 维护接口 --

    def cleanup(self) -> int:
        """主动清理过期任务。

        Returns:
            清理的任务数
        """
        with self._lock:
            before = len(self._tasks)
            self._purge_expired()
            return before - len(self._tasks)

    def count(self) -> int:
        """返回当前任务总数。"""
        with self._lock:
            return len(self._tasks)
