# -*- coding: utf-8 -*-
"""
Skill 生命周期管理（Curator）

自动管理 ~/.abyss/skills/ 中 skill 的生命周期状态：
  active   — 默认状态
  stale    — 超过 stale_after_days 未使用
  archived — 超过 archive_after_days 未使用，移至 .archive/

严格不变式：
- 只管理 agent 创建或用户手工安装的 skill（内置 skill 不受影响）
- 从不删除，最大操作是归档（可恢复）
- Pinned skill 豁免所有自动过渡
- 使用侧边文件 .usage.json 跟踪使用记录

设计借鉴 Hermes Agent 的 agent/curator.py + tools/skill_usage.py。
"""

from __future__ import annotations

import json
import threading
import time
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional

from .config import CONFIG_DIR

# 状态常量
STATE_ACTIVE = "active"
STATE_STALE = "stale"
STATE_ARCHIVED = "archived"

# 默认参数
DEFAULT_INTERVAL_HOURS = 24  # 每 24 小时检查一次
DEFAULT_MIN_IDLE_MINUTES = 30  # agent 空闲 30 分钟后才运行
DEFAULT_STALE_AFTER_DAYS = 14  # 14 天未用 → stale
DEFAULT_ARCHIVE_AFTER_DAYS = 60  # 60 天未用 → archive


def _skills_dir() -> Path:
    """Skill 根目录。"""
    return CONFIG_DIR / "skills"


def _archive_dir() -> Path:
    """归档目录。"""
    d = _skills_dir() / ".archive"
    d.mkdir(parents=True, exist_ok=True)
    return d


def _usage_file() -> Path:
    """使用记录侧边文件路径。"""
    return _skills_dir() / ".usage.json"


def _state_file() -> Path:
    """Curator 状态文件路径。"""
    return _skills_dir() / ".curator_state"


def load_usage() -> Dict[str, Any]:
    """加载 .usage.json，文件不存在返回空 dict。"""
    path = _usage_file()
    if not path.exists():
        return {}
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except (json.JSONDecodeError, OSError):
        return {}


def save_usage(data: Dict[str, Any]) -> None:
    """原子写入 .usage.json。"""
    path = _usage_file()
    path.parent.mkdir(parents=True, exist_ok=True)
    tmp = path.with_suffix(".tmp")
    try:
        tmp.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
        tmp.replace(path)
    except Exception:
        try:
            tmp.unlink(missing_ok=True)
        except Exception:
            pass


def load_state() -> Dict[str, Any]:
    """加载 curator 状态。"""
    defaults = {
        "last_run_at": None,
        "paused": False,
        "run_count": 0,
    }
    path = _state_file()
    if not path.exists():
        return defaults
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
        if isinstance(data, dict):
            return {**defaults, **data}
    except (json.JSONDecodeError, OSError):
        pass
    return defaults


def save_state(data: Dict[str, Any]) -> None:
    """保存 curator 状态。"""
    path = _state_file()
    path.parent.mkdir(parents=True, exist_ok=True)
    tmp = path.with_suffix(".tmp")
    try:
        tmp.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
        tmp.replace(path)
    except Exception:
        try:
            tmp.unlink(missing_ok=True)
        except Exception:
            pass


def bump_use(skill_name: str) -> None:
    """记录一次 skill 使用。调用方无需关心返回值，静默失败。"""
    if not skill_name:
        return
    usage = load_usage()
    rec = usage.get(skill_name)
    if rec is None:
        rec = {
            "use_count": 0,
            "last_used_at": None,
            "state": STATE_ACTIVE,
            "pinned": False,
        }
        usage[skill_name] = rec
    rec["use_count"] = rec.get("use_count", 0) + 1
    rec["last_used_at"] = datetime.now(timezone.utc).isoformat()
    # 使用时激活（如果之前是 stale）
    if rec.get("state") == STATE_STALE:
        rec["state"] = STATE_ACTIVE
    save_usage(usage)


def _parse_iso(ts: Optional[str]) -> Optional[datetime]:
    """解析 ISO 时间字符串为 datetime。"""
    if not ts:
        return None
    try:
        return datetime.fromisoformat(ts)
    except (TypeError, ValueError):
        return None


def _discover_skills() -> List[Path]:
    """扫描 skills 目录下所有 SKILL.md，返回 skill 目录路径列表。"""
    base = _skills_dir()
    if not base.exists():
        return []
    results = []
    for md in sorted(base.rglob("SKILL.md")):
        # 跳过 .archive 目录
        if ".archive" in md.parts:
            continue
        results.append(md.parent)
    return results


def _parse_skill_name(skill_dir: Path) -> Optional[str]:
    """从 SKILL.md frontmatter 读取 name 字段。"""
    md = skill_dir / "SKILL.md"
    if not md.exists():
        return None
    try:
        raw = md.read_text(encoding="utf-8")
    except OSError:
        return None

    if not raw.startswith("---"):
        return skill_dir.name

    end_idx = raw.find("\n---", 3)
    if end_idx == -1:
        return skill_dir.name

    fm_text = raw[3:end_idx]
    for line in fm_text.splitlines():
        if line.startswith("name:"):
            name = line.split(":", 1)[1].strip()
            if name:
                return name
    return skill_dir.name


def apply_automatic_transitions(now: Optional[datetime] = None) -> Dict[str, int]:
    """遍历所有 skill，按最后使用时间自动过渡状态。

    返回计数 dict: {marked_stale, archived, reactivated, checked}。
    """
    if now is None:
        now = datetime.now(timezone.utc)

    stale_cutoff = now - timedelta(days=DEFAULT_STALE_AFTER_DAYS)
    archive_cutoff = now - timedelta(days=DEFAULT_ARCHIVE_AFTER_DAYS)
    usage = load_usage()
    counts = {"marked_stale": 0, "archived": 0, "reactivated": 0, "checked": 0}

    for skill_dir in _discover_skills():
        name = _parse_skill_name(skill_dir)
        if not name:
            continue
        counts["checked"] += 1

        rec = usage.get(name, {})
        if rec.get("pinned"):
            continue

        last_used = _parse_iso(rec.get("last_used_at"))
        state = rec.get("state", STATE_ACTIVE)

        if last_used is None:
            # 从未使用过的 skill：用目录修改时间作为锚点
            try:
                mtime = skill_dir.stat().st_mtime
                last_used = datetime.fromtimestamp(mtime, tz=timezone.utc)
            except OSError:
                last_used = now

        if last_used.tzinfo is None:
            last_used = last_used.replace(tzinfo=timezone.utc)

        if last_used <= archive_cutoff and state != STATE_ARCHIVED:
            # 归档：移动到 .archive/
            _archive_skill(skill_dir, name, usage)
            counts["archived"] += 1
        elif last_used <= stale_cutoff and state == STATE_ACTIVE:
            # 标记为过期
            rec["state"] = STATE_STALE
            usage[name] = rec
            counts["marked_stale"] += 1
        elif last_used > stale_cutoff and state == STATE_STALE:
            # 重新激活
            rec["state"] = STATE_ACTIVE
            usage[name] = rec
            counts["reactivated"] += 1

    if counts["marked_stale"] or counts["archived"] or counts["reactivated"]:
        save_usage(usage)

    return counts


def _archive_skill(skill_dir: Path, name: str, usage: Dict[str, Any]) -> None:
    """将 skill 目录移动到 .archive/。"""
    archive_root = _archive_dir()
    dest = archive_root / skill_dir.name
    # 如果目标已存在，加后缀
    if dest.exists():
        i = 1
        while (archive_root / f"{skill_dir.name}_{i}").exists():
            i += 1
        dest = archive_root / f"{skill_dir.name}_{i}"
    try:
        skill_dir.rename(dest)
        rec = usage.get(name, {})
        rec["state"] = STATE_ARCHIVED
        rec["archived_at"] = datetime.now(timezone.utc).isoformat()
        rec["archived_path"] = str(dest)
        usage[name] = rec
    except OSError:
        pass  # 静默失败，下次还会尝试


def pin_skill(name: str) -> bool:
    """固定 skill，使其免于自动过渡。"""
    usage = load_usage()
    rec = usage.get(name, {})
    rec["pinned"] = True
    usage[name] = rec
    save_usage(usage)
    return True


def unpin_skill(name: str) -> bool:
    """取消固定。"""
    usage = load_usage()
    rec = usage.get(name, {})
    rec["pinned"] = False
    usage[name] = rec
    save_usage(usage)
    return True


def restore_skill(name: str) -> bool:
    """从 .archive/ 恢复 skill。"""
    usage = load_usage()
    rec = usage.get(name, {})
    archived_path = rec.get("archived_path")
    if not archived_path:
        return False
    src = Path(archived_path)
    if not src.exists():
        return False
    dest = _skills_dir() / src.name
    try:
        src.rename(dest)
        rec["state"] = STATE_ACTIVE
        rec.pop("archived_at", None)
        rec.pop("archived_path", None)
        usage[name] = rec
        save_usage(usage)
        return True
    except OSError:
        return False


def list_archived() -> List[Dict[str, Any]]:
    """列出所有已归档的 skill。"""
    usage = load_usage()
    result = []
    for name, rec in usage.items():
        if rec.get("state") == STATE_ARCHIVED:
            result.append({
                "name": name,
                "archived_at": rec.get("archived_at", ""),
                "archived_path": rec.get("archived_path", ""),
            })
    return result


def get_report() -> List[Dict[str, Any]]:
    """返回所有 skill 的状态报告。"""
    usage = load_usage()
    report = []
    for skill_dir in _discover_skills():
        name = _parse_skill_name(skill_dir)
        if not name:
            continue
        rec = usage.get(name, {})
        report.append({
            "name": name,
            "state": rec.get("state", STATE_ACTIVE),
            "pinned": rec.get("pinned", False),
            "use_count": rec.get("use_count", 0),
            "last_used_at": rec.get("last_used_at") or "never",
        })
    # 加上已归档的
    for name, rec in usage.items():
        if rec.get("state") == STATE_ARCHIVED and not any(r["name"] == name for r in report):
            report.append({
                "name": name,
                "state": STATE_ARCHIVED,
                "pinned": rec.get("pinned", False),
                "use_count": rec.get("use_count", 0),
                "last_used_at": rec.get("last_used_at") or "never",
                "archived_at": rec.get("archived_at", ""),
            })
    return report


def should_run_now(now: Optional[datetime] = None) -> bool:
    """检查是否应该运行 curator。"""
    state = load_state()
    if state.get("paused"):
        return False

    if now is None:
        now = datetime.now(timezone.utc)

    last = _parse_iso(state.get("last_run_at"))
    if last is None:
        # 首次运行：记录时间，等一个 interval 后再执行
        state["last_run_at"] = now.isoformat()
        save_state(state)
        return False

    if last.tzinfo is None:
        last = last.replace(tzinfo=timezone.utc)

    return (now - last) >= timedelta(hours=DEFAULT_INTERVAL_HOURS)


def run_curator(on_summary: Optional[Callable[[str], None]] = None) -> Dict[str, Any]:
    """执行一次 curator 检查。

    Args:
        on_summary: 可选回调，接收一行摘要文本。

    Returns:
        {"started_at": str, "counts": dict, "summary": str}
    """
    now = datetime.now(timezone.utc)
    counts = apply_automatic_transitions(now=now)

    parts = []
    if counts["marked_stale"]:
        parts.append(f"{counts['marked_stale']} 标记为过期")
    if counts["archived"]:
        parts.append(f"{counts['archived']} 归档")
    if counts["reactivated"]:
        parts.append(f"{counts['reactivated']} 重新激活")
    summary = ", ".join(parts) if parts else "无变更"

    state = load_state()
    state["last_run_at"] = now.isoformat()
    state["run_count"] = state.get("run_count", 0) + 1
    save_state(state)

    if on_summary:
        try:
            on_summary(f"curator: {summary}")
        except Exception:
            pass

    return {
        "started_at": now.isoformat(),
        "counts": counts,
        "summary": summary,
    }


def maybe_run_curator(
    agent_idle_seconds: float = None,
    on_summary: Optional[Callable[[str], None]] = None,
) -> Optional[Dict[str, Any]]:
    """最佳努力：在满足条件时运行 curator。返回结果 dict 或 None。"""
    try:
        if not should_run_now():
            return None
        if agent_idle_seconds is not None:
            min_idle = DEFAULT_MIN_IDLE_MINUTES * 60.0
            if agent_idle_seconds < min_idle:
                return None
        return run_curator(on_summary=on_summary)
    except Exception:
        return None


class CuratorDaemon:
    """后台守护线程，定期检查并执行 curator。

    同时管理 skill（通过 maybe_run_curator）和 knowledge（通过 KnowledgeCurator）
    生命周期。两者独立运行，任一失败不影响另一个。
    """

    def __init__(self, check_interval_seconds: int = 3600,
                 knowledge_curator: Any = None):
        self._interval = check_interval_seconds
        self._stop = threading.Event()
        self._thread: Optional[threading.Thread] = None
        # 外部可设置：当前是否有 agent 活跃（非活跃时才运行 curator）
        self.agent_busy = False
        # 可选的知识库管理者（KnowledgeCurator 实例）
        self._knowledge_curator = knowledge_curator

    def start(self):
        """启动守护线程。"""
        if self._thread is not None:
            return
        self._stop.clear()
        self._thread = threading.Thread(target=self._loop, daemon=True, name="curator-daemon")
        self._thread.start()

    def stop(self):
        """停止守护线程。"""
        self._stop.set()
        if self._thread:
            self._thread.join(timeout=2)
            self._thread = None

    def _run_one_cycle(self) -> None:
        """执行单轮检查：skill + knowledge 生命周期过渡。

        供测试直接调用，避免触发 _loop 的无限等待。
        knowledge_curator 异常静默失败，不影响 skill curator。
        """
        if self.agent_busy:
            return
        # skill 生命周期
        try:
            maybe_run_curator()
        except Exception:
            pass
        # knowledge 生命周期
        if self._knowledge_curator is not None:
            try:
                self._knowledge_curator.apply_automatic_transitions()
            except Exception:
                pass  # 静默失败，下次还会尝试

    def _loop(self):
        while not self._stop.is_set():
            self._stop.wait(self._interval)
            if self._stop.is_set():
                break
            self._run_one_cycle()
