from __future__ import annotations

import logging
import re
import shutil
import subprocess
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Any

from witty_agent_server.application.services.skill.errors import (
    AgentSkillServiceError,
)
from witty_agent_server.application.services.skill.skill_client_port import (
    SkillClientPort,
)
from witty_service.config import get_settings


class AgentSkillServiceBase(ABC):
    runtime_type: str
    _ALLOWED_SOURCE_BASES: list[Path] = []

    def __init__(
        self,
        *,
        skill_client: SkillClientPort | None = None,
    ) -> None:
        self._skill_client = skill_client

    def _require_skill_client(self) -> SkillClientPort:
        """返回 skill_client,若子类未提供则抛出清晰错误。

        需要调用 skill_client 的子类应在 __init__中覆写并传入有效实现
        """
        if self._skill_client is None:
            raise RuntimeError(
                f"{type(self).__name__}: skill_client is required but was not "
                f"provided. Override __init__ and pass a SkillClientPort "
                f"implementation."
            )
        return self._skill_client

    @abstractmethod
    def list_skills(self, *, agent_id: str | None = None) -> dict[str, Any]:
        """查询当前 runtime 可用的技能列表。"""

    @abstractmethod
    def install_skill(
        self,
        *,
        agent_id: str | None = None,
        skill_name: str,
        source_type: str | None = None,
        source_path: str | None = None,
        skill_source: str | None = None,
    ) -> dict[str, Any]:
        """安装技能到当前 runtime。source_path 非空时为本地技能目录。"""

    @abstractmethod
    def uninstall_skill(
        self,
        *,
        agent_id: str | None = None,
        skill_name: str,
        source_type: str | None = None,
        source_path: str | None = None,
        runtime_source: str | None = None,
    ) -> dict[str, Any]:
        """卸载当前 runtime 中的技能。"""

    @staticmethod
    def _is_local_path(path: str) -> bool:
        path = path.strip()
        return (
            path.startswith("/") 
            or path.startswith("~") 
            or path.startswith(".")
            or "\\" in path
            or ("/" in path and not path.startswith("http://") and not path.startswith("https://"))
        )

    @staticmethod
    def _remove_path(target: Path) -> None:
        """安全删除文件或目录。"""
        if target.is_symlink() or target.is_file():
            target.unlink(missing_ok=True)
            return
        if target.exists():
            shutil.rmtree(target)

    def _normalize_skill_name(
        self,
        skill_name: str,
        error_cls: type[AgentSkillServiceError],
    ) -> str:
        """校验并规范化 skill 名称,防止路径穿越。"""
        if not isinstance(skill_name, str) or not skill_name.strip():
            raise error_cls(
                runtime_type=self.runtime_type,
                skill_name=skill_name,
                reason="skill_name is empty",
            )
        normalized = skill_name.strip()
        if normalized in (".", ".."):
            raise error_cls(
                runtime_type=self.runtime_type,
                skill_name=normalized,
                reason="skill_name must not be '.' or '..'",
            )
        if re.search(r"[\\/]", normalized):
            raise error_cls(
                runtime_type=self.runtime_type,
                skill_name=normalized,
                reason="skill_name contains path separator",
            )
        return normalized

    @classmethod
    def _validate_source_path(cls, target: Path) -> Path:
        """校验 source_path 是否在允许的目录下,防止路径穿越。"""
        resolved = target.expanduser().resolve()
        bases = cls._ALLOWED_SOURCE_BASES or [
            get_settings().workspace.root_path() / "skill-repositories"
        ]
        for base in bases:
            base_resolved = base.resolve()
            try:
                resolved.relative_to(base_resolved)
                return resolved
            except ValueError:
                continue
        raise ValueError(
            f"Source path {resolved} is outside allowed directories: "
            f"{[str(b) for b in bases]}"
        )

    _logger = logging.getLogger(__name__)

    def _run_wittyhub_command(
        self,
        command: list[str],
        cwd: Path,
        *,
        skill_name: str,
        error_cls: type[AgentSkillServiceError],
        timeout: int | None = None,
        raise_on_error: bool = True,
    ) -> subprocess.CompletedProcess | None:
        """执行 wittyhub 命令并统一转换常见错误。

        子类通过此方法调用 wittyhub,避免重复的 try/except 样板代码。
        ``raise_on_error=False`` 时,CalledProcessError 仅记录警告而不抛出。
        """
        try:
            result = subprocess.run(
                command,
                check=True,
                capture_output=True,
                text=True,
                cwd=cwd,
                timeout=timeout,
            )
            return result
        except subprocess.TimeoutExpired as exc:
            reason = f"wittyhub command timed out: {exc}"
            if not raise_on_error:
                self._logger.warning(
                    "wittyhub command timed out, runtime_type=%s skill_name=%s",
                    self.runtime_type,
                    skill_name,
                )
                return None
            raise error_cls(
                runtime_type=self.runtime_type,
                skill_name=skill_name,
                reason=reason,
            ) from exc
        except FileNotFoundError:
            raise error_cls(
                runtime_type=self.runtime_type,
                skill_name=skill_name,
                reason="npx command not found",
            )
        except subprocess.CalledProcessError as exc:
            stderr = (exc.stderr or "").strip()
            stdout = (exc.stdout or "").strip()
            reason = stderr or stdout or f"wittyhub exited with code {exc.returncode}"
            if not raise_on_error:
                self._logger.warning(
                    "wittyhub command failed, runtime_type=%s skill_name=%s reason=%s",
                    self.runtime_type,
                    skill_name,
                    reason,
                )
                return None
            raise error_cls(
                runtime_type=self.runtime_type,
                skill_name=skill_name,
                reason=reason,
            ) from exc