import asyncio
import json
import logging
import os
import shutil
import zipfile
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

import yaml

from nexus_agent.models.skill import SkillInfo, SkillDetail

logger = logging.getLogger(__name__)

SKILL_TOOL_NAMES = {
    "read_skill", "run_skill_script", "read_skill_reference",
    "create_skill", "add_skill_script", "update_skill",
}


class SkillManager:
    def __init__(self):
        self._skills: Dict[str, SkillInfo] = {}
        self._config_path: Optional[Path] = None
        self._disabled: set[str] = set()
        self._base_dirs: List[Path] = []

    # --- Config persistence ---

    def load_config(self, config_path: str) -> None:
        path = Path(config_path)
        if not path.is_absolute():
            from nexus_agent.config import get_config_path
            path = get_config_path(config_path)
        self._config_path = path

        if not path.exists():
            logger.info(f"Skills config not found: {path}, starting fresh")
            self._disabled = set()
            return

        try:
            data = json.loads(path.read_text(encoding="utf-8"))
            self._disabled = set(data.get("disabled", []))
            logger.info(f"Loaded skills config from {path} ({len(self._disabled)} disabled)")
        except Exception as e:
            logger.warning(f"Failed to load skills config: {e}")
            self._disabled = set()

    def _save_config(self) -> None:
        if not self._config_path:
            return
        data = {"disabled": sorted(self._disabled)}
        self._config_path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")

    # --- Discovery ---

    def _parse_frontmatter(self, skill_md_path: Path) -> Dict[str, Any]:
        content = skill_md_path.read_text(encoding="utf-8")
        if not content.startswith("---"):
            raise ValueError(f"No YAML frontmatter: {skill_md_path}")
        parts = content.split("---", 2)
        if len(parts) < 3:
            raise ValueError(f"Invalid frontmatter format: {skill_md_path}")
        return yaml.safe_load(parts[1]) or {}

    def _list_dir_files(self, base: Path, subdirs: List[str], extensions: Tuple[str, ...]) -> List[str]:
        """여러 대안 디렉토리명을 탐색하고 하위 디렉토리까지 재귀 검색"""
        for subdir in subdirs:
            d = base / subdir
            if d.is_dir():
                return sorted(
                    str(f.relative_to(d))
                    for f in d.rglob("*")
                    if f.is_file() and f.suffix in extensions
                )
        return []

    def discover_skills(self, dirs: List[str]) -> None:
        self._base_dirs = []

        for d in dirs:
            p = Path(d)
            if not p.is_absolute():
                from nexus_agent.config import get_skills_dir
                p = get_skills_dir()
            self._base_dirs.append(p)

        discovered: Dict[str, SkillInfo] = {}
        for base_dir in self._base_dirs:
            if not base_dir.is_dir():
                logger.info(f"Skills directory not found: {base_dir}, skipping")
                continue

            for skill_dir in sorted(base_dir.iterdir()):
                skill_md = skill_dir / "SKILL.md"
                if not skill_md.is_file():
                    continue

                try:
                    fm = self._parse_frontmatter(skill_md)
                    name = fm.get("name", skill_dir.name)

                    if name in discovered:
                        continue  # 먼저 발견된 스킬(사용자 디렉토리)이 우선

                    scripts = self._list_dir_files(skill_dir, ["scripts", "templates"], (".py", ".sh", ".js", ".ts"))
                    references = self._list_dir_files(skill_dir, ["references", "reference"], (".md", ".txt", ".json", ".yaml", ".yml"))

                    discovered[name] = SkillInfo(
                        name=name,
                        description=fm.get("description", ""),
                        path=str(skill_dir.resolve()),
                        scripts=scripts,
                        references=references,
                        enabled=name not in self._disabled,
                        license=fm.get("license"),
                        compatibility=fm.get("compatibility"),
                        metadata=fm.get("metadata"),
                    )
                    logger.info(f"Discovered skill: {name}")
                except Exception as e:
                    logger.warning(f"Failed to load skill ({skill_dir}): {e}")

        self._skills = discovered
        logger.info(f"Total skills discovered: {len(self._skills)}")

    # --- CRUD ---

    def get_all_skills(self) -> List[SkillInfo]:
        return list(self._skills.values())

    def get_skill(self, name: str) -> Optional[SkillInfo]:
        return self._skills.get(name)

    def load_skill_content(self, name: str) -> Optional[SkillDetail]:
        skill = self._skills.get(name)
        if not skill:
            return None
        skill_md = Path(skill.path) / "SKILL.md"
        content = skill_md.read_text(encoding="utf-8") if skill_md.exists() else ""
        return SkillDetail(**skill.model_dump(), content=content)

    def _resolve_subdir(self, skill_base: Path, candidates: List[str], rel_path: str) -> Optional[Path]:
        """대안 디렉토리 후보 중 파일이 존재하는 경로를 반환 (path traversal 검증 포함)"""
        for subdir in candidates:
            full_path = (skill_base / subdir / rel_path).resolve()
            if not full_path.is_relative_to(skill_base):
                raise ValueError(f"Path traversal detected: {rel_path}")
            if full_path.is_file():
                return full_path
        return None

    def load_skill_reference(self, name: str, ref_path: str) -> Optional[str]:
        skill = self._skills.get(name)
        if not skill:
            return None

        skill_base = Path(skill.path).resolve()
        full_path = self._resolve_subdir(skill_base, ["references", "reference"], ref_path)
        if not full_path:
            return None
        return full_path.read_text(encoding="utf-8")

    async def execute_script(self, name: str, script: str, args: List[str] | None = None) -> Dict[str, Any]:
        skill = self._skills.get(name)
        if not skill:
            return {"success": False, "error": f"Skill not found: {name}"}

        skill_base = Path(skill.path).resolve()
        try:
            script_path = self._resolve_subdir(skill_base, ["scripts", "templates"], script)
        except ValueError as e:
            return {"success": False, "error": str(e)}
        if not script_path:
            return {"success": False, "error": f"Script not found: {script}"}

        # Determine runner based on extension
        import sys
        ext = script_path.suffix
        if ext == ".py":
            python_cmd = "python" if sys.platform == "win32" else "python3"
            cmd = [python_cmd, str(script_path)] + (args or [])
        elif ext == ".sh":
            bash = shutil.which("bash")
            if not bash:
                return {"success": False, "error": "bash not found. .sh scripts require bash (install Git Bash or WSL on Windows)."}
            cmd = [bash, str(script_path)] + (args or [])
        elif ext == ".js":
            cmd = ["node", str(script_path)] + (args or [])
        elif ext in (".bat", ".cmd", ".ps1"):
            if ext == ".ps1":
                cmd = ["powershell", "-ExecutionPolicy", "Bypass", "-File", str(script_path)] + (args or [])
            else:
                cmd = ["cmd", "/c", str(script_path)] + (args or [])
        else:
            return {"success": False, "error": f"Unsupported script type: {ext}"}

        try:
            proc = await asyncio.create_subprocess_exec(
                *cmd,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
                cwd=str(skill_base),
            )
            stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=60)
            return {
                "success": proc.returncode == 0,
                "stdout": stdout.decode(errors="replace"),
                "stderr": stderr.decode(errors="replace"),
                "returncode": proc.returncode,
            }
        except asyncio.TimeoutError:
            proc.kill()
            return {"success": False, "error": "Script execution timed out (60s)"}
        except Exception as e:
            return {"success": False, "error": str(e)}

    def create_skill(self, name: str, description: str, instructions: str = "") -> SkillInfo:
        if not self._base_dirs:
            raise ValueError("No skill directories configured")

        skill_dir = self._base_dirs[0] / name
        skill_dir.mkdir(parents=True, exist_ok=True)

        frontmatter = f"---\nname: {name}\ndescription: >\n  {description}\n---\n"
        body = instructions if instructions else f"# {name}\n\nSkill instructions go here.\n"
        (skill_dir / "SKILL.md").write_text(frontmatter + "\n" + body, encoding="utf-8")

        self._rediscover()
        return self._skills[name]

    def import_from_path(self, source_path: str) -> SkillInfo:
        """기존 스킬 폴더를 skills 디렉토리로 복사하여 등록"""
        if not self._base_dirs:
            raise ValueError("No skill directories configured")

        src = Path(source_path).resolve()
        if not src.is_dir():
            raise ValueError(f"Source is not a directory: {source_path}")
        if not (src / "SKILL.md").is_file():
            raise ValueError(f"SKILL.md not found in: {source_path}")

        fm = self._parse_frontmatter(src / "SKILL.md")
        name = fm.get("name", src.name)

        dest = self._base_dirs[0] / name
        if dest.exists():
            raise ValueError(f"Skill '{name}' already exists")

        shutil.copytree(str(src), str(dest))
        self._rediscover()
        return self._skills[name]

    def import_from_zip(self, zip_path: str) -> SkillInfo:
        """zip 파일을 풀어서 스킬로 등록"""
        if not self._base_dirs:
            raise ValueError("No skill directories configured")

        zp = Path(zip_path)
        if not zp.is_file():
            raise ValueError(f"Zip file not found: {zip_path}")

        import tempfile
        with tempfile.TemporaryDirectory() as tmpdir:
            with zipfile.ZipFile(str(zp), "r") as zf:
                zf.extractall(tmpdir)

            # zip 내부 구조 탐색: SKILL.md를 가진 폴더 찾기
            tmp = Path(tmpdir)
            skill_root = self._find_skill_root(tmp)
            if not skill_root:
                raise ValueError("SKILL.md not found in zip archive")

            return self.import_from_path(str(skill_root))

    def import_from_zip_bytes(self, data: bytes, filename: str) -> SkillInfo:
        """업로드된 zip 바이트를 임시 저장 후 등록"""
        import tempfile
        with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as f:
            f.write(data)
            tmp_path = f.name
        try:
            return self.import_from_zip(tmp_path)
        finally:
            os.unlink(tmp_path)

    def _find_skill_root(self, base: Path) -> Optional[Path]:
        """디렉토리 트리에서 SKILL.md가 있는 최상위 폴더를 찾음"""
        if (base / "SKILL.md").is_file():
            return base
        for child in sorted(base.iterdir()):
            if child.is_dir() and not child.name.startswith("__"):
                if (child / "SKILL.md").is_file():
                    return child
        return None

    def _rediscover(self) -> None:
        self.discover_skills([str(d) for d in self._base_dirs])

    def delete_skill(self, name: str) -> bool:
        skill = self._skills.get(name)
        if not skill:
            return False

        shutil.rmtree(skill.path, ignore_errors=True)
        self._skills.pop(name, None)
        self._disabled.discard(name)
        self._save_config()
        return True

    def toggle_skill(self, name: str, enabled: bool) -> Optional[SkillInfo]:
        skill = self._skills.get(name)
        if not skill:
            return None

        skill.enabled = enabled
        if enabled:
            self._disabled.discard(name)
        else:
            self._disabled.add(name)
        self._save_config()
        return skill

    def update_skill(self, name: str, description: str | None = None, instructions: str | None = None, enabled: bool | None = None) -> Optional[SkillInfo]:
        skill = self._skills.get(name)
        if not skill:
            return None

        if enabled is not None:
            self.toggle_skill(name, enabled)

        if description is not None or instructions is not None:
            skill_md_path = Path(skill.path) / "SKILL.md"
            content = skill_md_path.read_text(encoding="utf-8") if skill_md_path.exists() else ""

            if content.startswith("---"):
                parts = content.split("---", 2)
                if len(parts) >= 3:
                    fm = yaml.safe_load(parts[1]) or {}
                    if description is not None:
                        fm["description"] = description
                        skill.description = description
                    body = instructions if instructions is not None else parts[2].lstrip("\n")
                    new_content = "---\n" + yaml.dump(fm, allow_unicode=True, default_flow_style=False) + "---\n\n" + body
                    skill_md_path.write_text(new_content, encoding="utf-8")

        return skill

    # --- LLM Integration ---

    def generate_skills_xml(self) -> str:
        enabled = [s for s in self._skills.values() if s.enabled]
        if not enabled:
            return ""

        lines = ["<available_skills>"]
        for skill in enabled:
            lines.append("  <skill>")
            lines.append(f"    <name>{skill.name}</name>")
            lines.append(f"    <description>{skill.description}</description>")
            if skill.scripts:
                lines.append(f"    <scripts>{', '.join(skill.scripts)}</scripts>")
            if skill.references:
                lines.append(f"    <references>{', '.join(skill.references)}</references>")
            lines.append("  </skill>")
        lines.append("</available_skills>")
        return "\n".join(lines)

    def get_skill_tools(self) -> List[Dict[str, Any]]:
        tools = [
            {
                "type": "function",
                "function": {
                    "name": "create_skill",
                    "description": "새로운 스킬을 생성하여 등록합니다. 반복적으로 사용할 수 있는 워크플로우, 도구, 변환 로직 등을 스킬로 저장하세요.",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "name": {"type": "string", "description": "스킬 이름 (kebab-case, 예: document-reader)"},
                            "description": {"type": "string", "description": "스킬 설명 — 언제 사용하는지 포함"},
                            "instructions": {"type": "string", "description": "SKILL.md 본문 — 상세 지시사항, 예시, 워크플로우"},
                        },
                        "required": ["name", "description", "instructions"],
                    },
                },
            },
            {
                "type": "function",
                "function": {
                    "name": "add_skill_script",
                    "description": "기존 스킬에 실행 가능한 스크립트 파일을 추가합니다. 스크립트가 외부 파일을 인자로 받을 경우 절대 경로를 전제로 작성하세요.",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "skill_name": {"type": "string", "description": "스킬 이름"},
                            "filename": {"type": "string", "description": "스크립트 파일명 (예: extract_pptx.py)"},
                            "content": {"type": "string", "description": "스크립트 내용"},
                        },
                        "required": ["skill_name", "filename", "content"],
                    },
                },
            },
        ]

        enabled = [s for s in self._skills.values() if s.enabled]
        if not enabled:
            return tools

        tools.append({
            "type": "function",
            "function": {
                "name": "update_skill",
                "description": "기존 스킬의 설명이나 지시사항을 수정합니다. 스킬을 개선하거나 보완할 때 사용하세요.",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "name": {"type": "string", "description": "수정할 스킬 이름"},
                        "description": {"type": "string", "description": "새로운 스킬 설명 (생략 시 기존 유지)"},
                        "instructions": {"type": "string", "description": "새로운 SKILL.md 본문 (생략 시 기존 유지)"},
                    },
                    "required": ["name"],
                },
            },
        })

        tools.extend([
            {
                "type": "function",
                "function": {
                    "name": "read_skill",
                    "description": "등록된 스킬의 전체 지시사항(SKILL.md)을 읽습니다. 스킬의 사용법과 워크플로우를 파악할 때 호출하세요.",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "skill_name": {
                                "type": "string",
                                "description": "읽을 스킬 이름",
                            }
                        },
                        "required": ["skill_name"],
                    },
                },
            },
            {
                "type": "function",
                "function": {
                    "name": "run_skill_script",
                    "description": "스킬에 포함된 스크립트를 실행합니다. 스크립트는 스킬 디렉토리에서 실행되므로, 워크스페이스 파일을 참조할 때는 반드시 절대 경로를 args로 전달하세요.",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "skill_name": {"type": "string", "description": "스킬 이름"},
                            "script_name": {"type": "string", "description": "실행할 스크립트 파일명 (예: analyze.py)"},
                            "args": {
                                "type": "array",
                                "items": {"type": "string"},
                                "description": "스크립트 인자 (워크스페이스 파일은 절대 경로 사용, 예: /Users/.../file.pptx)",
                            },
                        },
                        "required": ["skill_name", "script_name"],
                    },
                },
            },
            {
                "type": "function",
                "function": {
                    "name": "read_skill_reference",
                    "description": "스킬의 참조 문서를 읽습니다.",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "skill_name": {"type": "string", "description": "스킬 이름"},
                            "reference_path": {"type": "string", "description": "참조 파일 경로 (예: api-guide.md)"},
                        },
                        "required": ["skill_name", "reference_path"],
                    },
                },
            },
        ])
        return tools

    async def handle_tool_call(self, tool_name: str, args: Dict[str, Any]) -> str:
        if tool_name == "read_skill":
            detail = self.load_skill_content(args["skill_name"])
            if not detail:
                return f"Error: Skill '{args['skill_name']}' not found"
            return detail.content

        elif tool_name == "run_skill_script":
            result = await self.execute_script(
                args["skill_name"],
                args["script_name"],
                args.get("args"),
            )
            if result["success"]:
                return result.get("stdout", "Script executed successfully")
            return f"Error: {result.get('error', result.get('stderr', 'Unknown error'))}"

        elif tool_name == "read_skill_reference":
            content = self.load_skill_reference(args["skill_name"], args["reference_path"])
            if content is None:
                return f"Error: Reference '{args['reference_path']}' not found in skill '{args['skill_name']}'"
            return content

        elif tool_name == "create_skill":
            try:
                info = self.create_skill(args["name"], args["description"], args["instructions"])
                return f"스킬 '{info.name}' 생성 완료. 경로: {info.path}"
            except Exception as e:
                return f"Error: 스킬 생성 실패 — {e}"

        elif tool_name == "update_skill":
            try:
                result = self.update_skill(
                    args["name"],
                    description=args.get("description"),
                    instructions=args.get("instructions"),
                )
                if not result:
                    return f"Error: 스킬 '{args['name']}' 을(를) 찾을 수 없습니다."
                return f"스킬 '{result.name}' 수정 완료."
            except Exception as e:
                return f"Error: 스킬 수정 실패 — {e}"

        elif tool_name == "add_skill_script":
            skill = self.get_skill(args["skill_name"])
            if not skill:
                return f"Error: 스킬 '{args['skill_name']}' 을(를) 찾을 수 없습니다."
            scripts_dir = Path(skill.path) / "scripts"
            scripts_dir.mkdir(exist_ok=True)
            script_path = scripts_dir / args["filename"]
            script_path.write_text(args["content"], encoding="utf-8")
            self._rediscover()
            return f"스크립트 '{args['filename']}' 추가 완료. 경로: {script_path}"

        return f"Error: Unknown skill tool '{tool_name}'"


skill_manager = SkillManager()
