"""Agent skills manager / 智能体技能管理器。

负责扫描全局 skills 库、生成可用清单，并同步到工作区 skills/。
"""

from __future__ import annotations

import re
import shutil
import threading
from pathlib import Path
from typing import Dict, List, Optional, Sequence

from config import AGENT_SKILLS_DIR, CUSTOM_SKILLS_DIR, IS_HOST_MODE, WORKSPACE_SKILLS_DIRNAME
from utils.logger import setup_logger

logger = setup_logger(__name__)

SKILL_FILE_NAME = "SKILL.md"
SKILL_ID_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_-]*$")
_SYNC_LOCK_GUARD = threading.Lock()
_SYNC_LOCKS: Dict[str, threading.Lock] = {}


def _get_sync_lock(skills_dir: Path) -> threading.Lock:
    key = str(skills_dir.resolve())
    with _SYNC_LOCK_GUARD:
        lock = _SYNC_LOCKS.get(key)
        if lock is None:
            lock = threading.Lock()
            _SYNC_LOCKS[key] = lock
        return lock


def ensure_agent_skills_dir(base_dir: Optional[str] = None) -> Path:
    """Ensure the global skills directory exists / 确保全局技能目录存在。"""
    root = Path(base_dir or AGENT_SKILLS_DIR).expanduser().resolve()
    root.mkdir(parents=True, exist_ok=True)
    return root


def ensure_workspace_skills_dir(project_path: str | Path) -> Path:
    """Ensure workspace skills directory exists / 确保工作区 skills 目录存在。"""
    root = Path(project_path).expanduser().resolve()
    skills_dir = (root / WORKSPACE_SKILLS_DIRNAME).resolve()
    skills_dir.mkdir(parents=True, exist_ok=True)
    return skills_dir


def infer_private_skills_dir(data_dir: str | Path | None) -> Optional[Path]:
    """Infer per-user private skills dir from workspace data_dir.

    host 模式下统一使用运行态根下的 ``agentskills``，不再按用户拆分。
    """
    if not data_dir:
        return None
    try:
        if IS_HOST_MODE:
            return Path(CUSTOM_SKILLS_DIR).expanduser().resolve()
        data_path = Path(data_dir).expanduser().resolve()
        if data_path.name == "data" and data_path.parent.parent.name == "workspaces":
            return (data_path.parent.parent.parent / "agentskills").resolve()
        # Docker Web 项目化布局：
        # users/<user>/projects/<project_id>/data -> users/<user>/personal/agentskills
        if data_path.name == "data" and data_path.parent.parent.name == "projects":
            user_root = data_path.parent.parent.parent
            shared_root = (user_root / "personal" / "agentskills").resolve()
            legacy_root = (data_path.parent / "agentskills").resolve()
            if legacy_root.exists() and legacy_root.is_dir() and legacy_root != shared_root:
                shared_root.mkdir(parents=True, exist_ok=True)
                for item in list(legacy_root.iterdir()):
                    dest = shared_root / item.name
                    if not dest.exists():
                        shutil.move(str(item), str(dest))
                try:
                    legacy_root.rmdir()
                except Exception:
                    pass
            return shared_root
        # 兼容上一轮临时结构：
        # users/<user>/project/<project_id>/data -> users/<user>/personal/agentskills
        if data_path.name == "data" and data_path.parent.parent.name == "project":
            user_root = data_path.parent.parent.parent
            shared_root = (user_root / "personal" / "agentskills").resolve()
            temp_shared_root = (data_path.parent.parent / "shared" / "agentskills").resolve()
            for legacy_root in (temp_shared_root, data_path.parent / "agentskills"):
                if legacy_root.exists() and legacy_root.is_dir() and legacy_root != shared_root:
                    shared_root.mkdir(parents=True, exist_ok=True)
                    for item in list(legacy_root.iterdir()):
                        dest = shared_root / item.name
                        if not dest.exists():
                            shutil.move(str(item), str(dest))
                    try:
                        legacy_root.rmdir()
                    except Exception:
                        pass
            return shared_root
        return (data_path.parent / "agentskills").resolve()
    except Exception:
        return None


def validate_skill_directory(source_dir: str | Path) -> Dict[str, object]:
    """Validate a skill folder with minimal required SKILL.md frontmatter."""
    source = Path(source_dir).expanduser().resolve()
    if not source.exists() or not source.is_dir():
        return {"success": False, "error": "source_dir 不是目录", "source_dir": str(source)}

    skill_id = source.name
    if not _is_valid_skill_id(skill_id):
        return {"success": False, "error": "skill 文件夹名称不合法", "skill_name": skill_id}

    skill_file = source / SKILL_FILE_NAME
    if not skill_file.exists() or not skill_file.is_file():
        return {"success": False, "error": "缺少 SKILL.md", "skill_name": skill_id}

    try:
        content = skill_file.read_text(encoding="utf-8")
    except Exception as exc:
        return {"success": False, "error": f"读取 SKILL.md 失败: {exc}", "skill_name": skill_id}

    if not re.search(r"(?m)^\s*name\s*:", content):
        return {"success": False, "error": "缺少 name:", "skill_name": skill_id}
    if not re.search(r"(?m)^\s*description\s*:", content):
        return {"success": False, "error": "缺少 description:", "skill_name": skill_id}

    return {
        "success": True,
        "skill_name": skill_id,
        "source_dir": str(source),
        "skill_file": str(skill_file),
    }


def archive_skill_directory(source_dir: str | Path, target_root: str | Path) -> Dict[str, object]:
    """Move a validated skill folder into a skills library root."""
    validation = validate_skill_directory(source_dir)
    if not validation.get("success"):
        return validation

    source = Path(str(validation["source_dir"])).expanduser().resolve()
    target_base = Path(target_root).expanduser().resolve()
    target_base.mkdir(parents=True, exist_ok=True)
    target = (target_base / source.name).resolve()

    try:
        target.relative_to(target_base)
    except ValueError:
        return {"success": False, "error": "目标路径不合法", "skill_name": source.name}

    if target.exists():
        return {
            "success": False,
            "error": "目标 skill 已存在",
            "skill_name": source.name,
            "target_dir": str(target),
        }

    try:
        shutil.move(str(source), str(target))
    except Exception as exc:
        return {
            "success": False,
            "error": f"移动 skill 失败: {exc}",
            "skill_name": source.name,
            "source_dir": str(source),
            "target_dir": str(target),
        }

    return {
        "success": True,
        "skill_name": source.name,
        "source_dir": str(source),
        "target_dir": str(target),
    }


def _parse_frontmatter(text: str) -> Dict[str, str]:
    """Parse simple YAML frontmatter / 解析简单 YAML 头信息。"""
    lines = text.splitlines()
    if not lines or lines[0].strip() != "---":
        return {}
    end_idx = None
    for idx in range(1, len(lines)):
        if lines[idx].strip() == "---":
            end_idx = idx
            break
    if end_idx is None:
        return {}
    meta: Dict[str, str] = {}
    for line in lines[1:end_idx]:
        if ":" not in line:
            continue
        key, value = line.split(":", 1)
        key = key.strip()
        value = value.strip().strip('"').strip("'")
        if key and value:
            meta[key] = value
    return meta


def _is_valid_skill_id(name: str) -> bool:
    """Validate skill id / 校验技能目录名。"""
    return bool(name and SKILL_ID_PATTERN.match(name))


def _scan_skills_catalog(root: Path) -> List[Dict[str, str]]:
    """Scan one skills root."""
    if not root.exists() or not root.is_dir():
        return []
    catalog: List[Dict[str, str]] = []
    for child in sorted(root.iterdir()):
        if not child.is_dir():
            continue
        if not _is_valid_skill_id(child.name):
            continue
        skill_file = child / SKILL_FILE_NAME
        if not skill_file.exists():
            continue
        meta: Dict[str, str] = {}
        try:
            meta = _parse_frontmatter(skill_file.read_text(encoding="utf-8"))
        except Exception:
            meta = {}
        label = meta.get("name") or child.name
        description = meta.get("description") or ""
        catalog.append({
            "id": child.name,
            "label": label,
            "description": description,
        })
    return catalog


def get_skills_catalog(
    base_dir: Optional[str] = None,
    private_dir: Optional[str | Path] = None,
) -> List[Dict[str, str]]:
    """List available skills from global library plus optional private library."""
    roots: List[Path] = [Path(base_dir or AGENT_SKILLS_DIR).expanduser().resolve()]
    if private_dir:
        private_root = Path(private_dir).expanduser().resolve()
        if private_root not in roots:
            roots.append(private_root)

    merged: Dict[str, Dict[str, str]] = {}
    order: List[str] = []
    for root in roots:
        for item in _scan_skills_catalog(root):
            skill_id = item.get("id")
            if not skill_id:
                continue
            if skill_id not in merged:
                order.append(skill_id)
            # Later roots (private) override metadata for same id.
            merged[skill_id] = item
    return [merged[skill_id] for skill_id in order if skill_id in merged]


def _get_skill_source_map(
    base_dir: Optional[str] = None,
    private_dir: Optional[str | Path] = None,
) -> Dict[str, Path]:
    roots: List[Path] = [Path(base_dir or AGENT_SKILLS_DIR).expanduser().resolve()]
    if private_dir:
        private_root = Path(private_dir).expanduser().resolve()
        if private_root not in roots:
            roots.append(private_root)

    sources: Dict[str, Path] = {}
    for root in roots:
        if not root.exists() or not root.is_dir():
            continue
        for item in _scan_skills_catalog(root):
            skill_id = item.get("id")
            if skill_id:
                sources[skill_id] = root / skill_id
    return sources


def resolve_enabled_skills(
    enabled_skills: Optional[Sequence[str]],
    catalog: Sequence[Dict[str, str]],
) -> List[str]:
    """Resolve enabled skills list / 解析启用技能列表。"""
    catalog_ids = [item.get("id", "") for item in catalog if item.get("id")]
    if enabled_skills is None:
        return catalog_ids
    if not isinstance(enabled_skills, (list, tuple)):
        return catalog_ids
    allowed = set(catalog_ids)
    seen = set()
    resolved: List[str] = []
    for item in enabled_skills:
        if not isinstance(item, str):
            continue
        skill_id = item.strip()
        if not skill_id or skill_id in seen or skill_id not in allowed:
            continue
        resolved.append(skill_id)
        seen.add(skill_id)
    return resolved


def build_skills_list(
    catalog: Sequence[Dict[str, str]],
    enabled_skill_ids: Sequence[str],
) -> List[str]:
    """Build skills list lines / 生成 skills 列表行。"""
    if not enabled_skill_ids:
        return []
    lookup = {item.get("id"): item for item in catalog if item.get("id")}
    lines: List[str] = []
    for skill_id in enabled_skill_ids:
        meta = lookup.get(skill_id)
        if not meta:
            continue
        description = (meta.get("description") or "").strip()
        if description:
            lines.append(f".astrion/skills/{skill_id}：{description}")
        else:
            lines.append(f".astrion/skills/{skill_id}")
    return lines


def merge_enabled_skills(
    enabled_skill_ids: Optional[Sequence[str]],
    catalog: Sequence[Dict[str, str]],
    catalog_snapshot: Optional[Sequence[str]] = None,
) -> List[str]:
    """Merge enabled skills with new catalog items / 合并启用列表并默认开启新增技能。"""
    catalog_ids = [item.get("id") for item in catalog if item.get("id")]
    if enabled_skill_ids is None:
        base = list(catalog_ids)
    else:
        base_set = {item for item in enabled_skill_ids if isinstance(item, str)}
        base = [item for item in catalog_ids if item in base_set]
    if catalog_snapshot:
        snapshot_set = {item for item in catalog_snapshot if isinstance(item, str)}
        new_items = [item for item in catalog_ids if item not in snapshot_set]
        for item in new_items:
            if item not in base:
                base.append(item)
    return base


def build_skills_prompt(template: str, skills_list: Sequence[str]) -> str:
    """Build skills prompt from template / 根据模板生成 skills 提示。"""
    if not template:
        return ""
    list_block = "\n".join(skills_list) if skills_list else ""
    content = template
    if list_block:
        content = content.replace("{skills_list}", list_block)
        content = re.sub(r"\[skills_empty\].*?\[/skills_empty\]\n?", "", content, flags=re.S)
    else:
        content = content.replace("{skills_list}", "")
        content = content.replace("[skills_empty]", "").replace("[/skills_empty]", "")
    return content.strip()


def sync_workspace_skills(
    project_path: str | Path,
    enabled_skills: Optional[Sequence[str]] = None,
    base_dir: Optional[str] = None,
    private_dir: Optional[str | Path] = None,
) -> Dict[str, object]:
    """Sync global skills into workspace / 将全局 skills 同步到工作区。"""
    root = Path(project_path).expanduser().resolve()
    skills_dir = (root / WORKSPACE_SKILLS_DIRNAME).resolve()
    try:
        skills_dir.relative_to(root)
    except Exception:
        return {"success": False, "error": "skills 目录不在项目路径内"}

    ensure_agent_skills_dir(base_dir)
    catalog = get_skills_catalog(base_dir, private_dir=private_dir)
    resolved = resolve_enabled_skills(enabled_skills, catalog)
    sources = _get_skill_source_map(base_dir, private_dir=private_dir)

    lock = _get_sync_lock(skills_dir)
    try:
        with lock:
            if skills_dir.exists():
                shutil.rmtree(skills_dir, ignore_errors=True)
            skills_dir.mkdir(parents=True, exist_ok=True)
            for skill_id in resolved:
                src = sources.get(skill_id)
                if src is None or not src.exists() or not src.is_dir():
                    continue
                dst = skills_dir / skill_id
                if dst.exists() and not dst.is_dir():
                    dst.unlink(missing_ok=True)
                shutil.copytree(src, dst, dirs_exist_ok=True)
            return {
                "success": True,
                "copied": list(resolved),
                "available": [item.get("id") for item in catalog],
                "target": str(skills_dir),
            }
    except Exception as exc:
        logger.error("同步 skills 失败: %s", exc, exc_info=True)
        return {"success": False, "error": str(exc), "target": str(skills_dir)}
