# -*- coding: utf-8 -*-
"""
Knowledge Curator 模块
按知识类型差异化执行生命周期状态过渡：
  active   — 默认状态
  stale    — 超过 stale_days 未使用（仅 experience/tool）
  archived — 超过 archive_days 未使用，移至 .archive/（仅 experience/tool）

严格不变式：
- preference / bugfix 永不过期
- pinned 豁免所有自动过渡
- 从不删除，最大操作是归档（可恢复）
- 使用侧边文件 .usage.json 跟踪使用记录（每个级别独立）

设计借鉴 abyss/curator.py（skill 生命周期管理）。
"""

from __future__ import annotations

import json
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

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

# 分类子目录名 → category（与 KnowledgeLoader 保持一致）
CATEGORY_SUBDIRS = {
    "preferences": "preference",
    "bugfixes": "bugfix",
    "experiences": "experience",
    "tools": "tool",
}


class KnowledgeCurator:
    """知识库生命周期管理者，按类型差异化执行状态过渡。

    通过构造函数接收 knowledge_dirs 字典（与 KnowledgeLoader 相同格式），
    在每个级别目录下维护独立的 .usage.json 跟踪使用记录。
    """

    EXPIRY_RULES = {
        "preference": None,      # 永不过期
        "bugfix": None,          # 永不过期
        "experience": {"stale_days": 90, "archive_days": 180},
        "tool": {"stale_days": 90, "archive_days": 180},
    }

    def __init__(self, knowledge_dirs: Dict[str, Path]):
        """接收 {"global": <path>, "project": <path>} 字典。"""
        self.knowledge_dirs = knowledge_dirs

    # --- 使用记录读写 ---

    def _usage_file_for_level(self, level: str) -> Optional[Path]:
        """返回某级别的 .usage.json 路径，级别不存在返回 None。"""
        base = self.knowledge_dirs.get(level)
        if base is None:
            return None
        return Path(base) / ".usage.json"

    def load_usage_for_level(self, level: str) -> Dict[str, Any]:
        """加载某级别的 .usage.json，文件不存在或损坏返回空 dict。"""
        path = self._usage_file_for_level(level)
        if path is None or not path.exists():
            return {}
        try:
            return json.loads(path.read_text(encoding="utf-8"))
        except (json.JSONDecodeError, OSError):
            return {}

    def save_usage_for_level(self, level: str, data: Dict[str, Any]) -> None:
        """原子写入某级别的 .usage.json。"""
        path = self._usage_file_for_level(level)
        if path is None:
            return
        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

    @staticmethod
    def _ensure_record(usage: Dict[str, Any], name: str) -> Dict[str, Any]:
        """确保 usage 中有 name 的记录，没有则创建默认记录。返回该记录。"""
        rec = usage.get(name)
        if rec is None:
            rec = {
                "use_count": 0,
                "last_used_at": None,
                "state": STATE_ACTIVE,
                "pinned": False,
            }
            usage[name] = rec
        return rec

    # --- 知识文件发现 ---

    @staticmethod
    def _parse_name_from_md(md_path: Path) -> Optional[str]:
        """从 .md frontmatter 读取 name 字段，失败返回 None。"""
        try:
            raw = md_path.read_text(encoding="utf-8")
        except OSError:
            return None
        if not raw.startswith("---"):
            return None
        end_idx = raw.find("\n---", 3)
        if end_idx == -1:
            return None
        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 None

    def _scan_level_knowledge(self, level: str) -> List[Tuple[str, Path, str]]:
        """扫描某级别下所有分类子目录的 .md 文件。
        返回 [(name, md_path, category), ...]。
        """
        base = self.knowledge_dirs.get(level)
        if base is None or not Path(base).exists():
            return []
        base = Path(base)
        results: List[Tuple[str, Path, str]] = []
        for subdir_name, category in CATEGORY_SUBDIRS.items():
            cat_dir = base / subdir_name
            if not cat_dir.exists():
                continue
            for md_file in sorted(cat_dir.glob("*.md")):
                name = self._parse_name_from_md(md_file)
                if name:
                    results.append((name, md_file, category))
        return results

    def _find_knowledge(self, name: str) -> Optional[Tuple[str, Path, str]]:
        """按 name 查找知识文件，项目级优先。
        返回 (level, md_path, category) 或 None。
        """
        for level in ("project", "global"):
            for kname, md_path, category in self._scan_level_knowledge(level):
                if kname == name:
                    return (level, md_path, category)
        return None

    def _resolve_level_for_name(self, name: str) -> str:
        """确定 name 所属级别（项目级优先），未找到默认 project。"""
        found = self._find_knowledge(name)
        if found is not None:
            return found[0]
        return "project"

    # --- 公共 API ---

    def bump_use(self, name: str) -> None:
        """记录一次知识使用：递增 use_count，更新 last_used_at，stale→active。"""
        if not name:
            return
        level = self._resolve_level_for_name(name)
        usage = self.load_usage_for_level(level)
        rec = self._ensure_record(usage, name)
        rec["use_count"] = rec.get("use_count", 0) + 1
        rec["last_used_at"] = datetime.now(timezone.utc).isoformat()
        if rec.get("state") == STATE_STALE:
            rec["state"] = STATE_ACTIVE
        self.save_usage_for_level(level, usage)

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

        返回计数 dict: {marked_stale, archived, reactivated, checked}。
        """
        if now is None:
            now = datetime.now(timezone.utc)
        counts = {
            "marked_stale": 0,
            "archived": 0,
            "reactivated": 0,
            "checked": 0,
        }
        for level in ("global", "project"):
            self._apply_transitions_for_level(level, now, counts)
        return counts

    def _apply_transitions_for_level(
        self, level: str, now: datetime, counts: Dict[str, int]
    ) -> None:
        """对单个级别执行状态过渡，原地更新 counts 与对应级别的 usage。

        对每个知识：pinned 跳过；永不过期类型跳过；否则按 stale/archive 阈值过渡。
        """
        usage = self.load_usage_for_level(level)
        dirty = False
        for name, md_path, category in self._scan_level_knowledge(level):
            counts["checked"] += 1
            rec = usage.get(name, {})
            if rec.get("pinned"):
                continue
            rule = self.EXPIRY_RULES.get(category)
            if rule is None:
                continue  # 永不过期类型跳过
            last_used = self._resolve_last_used(rec, md_path, now)
            state = rec.get("state", STATE_ACTIVE)
            stale_cutoff = now - timedelta(days=rule["stale_days"])
            archive_cutoff = now - timedelta(days=rule["archive_days"])
            if last_used <= archive_cutoff and state != STATE_ARCHIVED:
                self._archive_file(md_path, name, usage)
                counts["archived"] += 1
                dirty = True
            elif last_used <= stale_cutoff and state == STATE_ACTIVE:
                rec["state"] = STATE_STALE
                usage[name] = rec
                counts["marked_stale"] += 1
                dirty = True
            elif last_used > stale_cutoff and state == STATE_STALE:
                rec["state"] = STATE_ACTIVE
                usage[name] = rec
                counts["reactivated"] += 1
                dirty = True
        if dirty:
            self.save_usage_for_level(level, usage)

    @staticmethod
    def _resolve_last_used(
        rec: Dict[str, Any], md_path: Path, now: datetime
    ) -> datetime:
        """从 rec 解析 last_used_at；缺失则用文件 mtime；再缺失用 now。"""
        last_used: Optional[datetime] = None
        ts = rec.get("last_used_at")
        if ts:
            try:
                last_used = datetime.fromisoformat(ts)
            except (TypeError, ValueError):
                last_used = None
        if last_used is None:
            try:
                mtime = md_path.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)
        return last_used

    def _archive_file(
        self, md_path: Path, name: str, usage: Dict[str, Any]
    ) -> None:
        """将 .md 文件移动到所在级别的 .archive/ 子目录，并更新 usage 记录。"""
        level = self._level_of_path(md_path)
        if level is None:
            return
        base = Path(self.knowledge_dirs[level])
        archive_dir = base / ".archive"
        archive_dir.mkdir(parents=True, exist_ok=True)
        dest = self._resolve_archive_dest(archive_dir, md_path)
        try:
            md_path.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 _level_of_path(self, md_path: Path) -> Optional[str]:
        """根据 md_path 反查所属级别。"""
        for level, base in self.knowledge_dirs.items():
            if base is None:
                continue
            try:
                md_path.relative_to(Path(base))
                return level
            except ValueError:
                continue
        return None

    @staticmethod
    def _resolve_archive_dest(archive_dir: Path, md_path: Path) -> Path:
        """计算归档目标路径，重名时加数字后缀。"""
        dest = archive_dir / md_path.name
        if not dest.exists():
            return dest
        i = 1
        while True:
            candidate = archive_dir / f"{md_path.stem}_{i}{md_path.suffix}"
            if not candidate.exists():
                return candidate
            i += 1

    def pin_knowledge(self, name: str) -> bool:
        """固定知识，使其免于自动过渡。"""
        if not name:
            return False
        level = self._resolve_level_for_name(name)
        usage = self.load_usage_for_level(level)
        rec = self._ensure_record(usage, name)
        rec["pinned"] = True
        self.save_usage_for_level(level, usage)
        return True

    def unpin_knowledge(self, name: str) -> bool:
        """取消固定。"""
        if not name:
            return False
        level = self._resolve_level_for_name(name)
        usage = self.load_usage_for_level(level)
        rec = self._ensure_record(usage, name)
        rec["pinned"] = False
        self.save_usage_for_level(level, usage)
        return True

    def archive_knowledge(self, name: str) -> bool:
        """手动归档：将 .md 移动到 .archive/ 并标记状态为 archived。"""
        found = self._find_knowledge(name)
        if found is None:
            return False
        level, md_path, _category = found
        usage = self.load_usage_for_level(level)
        self._archive_file(md_path, name, usage)
        self.save_usage_for_level(level, usage)
        return True
