"""Skill Importer - Install skills from OSS repositories.

Supports importing skills from:
- GitHub repos (anthropics/skills, etc.)
- Local directories
- URLs

Usage:
    from core.skills.importer import install_skill, list_installed

    # Install from GitHub
    await install_skill("anthropics/skills/pdf")
    await install_skill("anthropics/skills/notion")

    # Install from local path
    await install_skill("/path/to/my-skill", source="local")

    # List installed
    installed = list_installed()
"""

import json
import logging
import shutil
from pathlib import Path

import httpx
from pydantic import BaseModel, Field

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Result models
# ---------------------------------------------------------------------------


class SkillMetadata(BaseModel):
    """Parsed metadata from SKILL.md frontmatter."""

    name: str | None = None
    description: str | None = None
    author: str | None = None
    version: str | None = None

    model_config = {"extra": "allow"}


class SkillInstallResult(BaseModel):
    """Result of a skill install operation."""

    success: bool
    error: str | None = None
    skill_name: str | None = None
    path: str | None = None
    metadata: SkillMetadata | None = None
    tools: list[str] = Field(default_factory=list)
    pathways_downloaded: list[str] = Field(default_factory=list)
    agents_downloaded: list[str] = Field(default_factory=list)
    tried: list[str] = Field(default_factory=list)


class InstalledSkillInfo(BaseModel):
    """Info about an installed skill (from .install_info.json or directory scan)."""

    name: str
    path: str
    source: str | None = None
    metadata: SkillMetadata | None = None
    tools_downloaded: list[str] = Field(default_factory=list)

    model_config = {"extra": "allow"}


# Default install location
SKILLS_DIR = Path(".albus/skills")

# Known skill registries
REGISTRIES = {
    "anthropic": "https://raw.githubusercontent.com/anthropics/skills/main/skills",
    "albus": "https://raw.githubusercontent.com/albusstudio/albusOS/main/src/stdlib/primitives",
}


def _ensure_skills_dir() -> Path:
    """Ensure the skills directory exists."""
    SKILLS_DIR.mkdir(parents=True, exist_ok=True)
    return SKILLS_DIR


def _parse_skill_path(path: str) -> tuple[str, str, str]:
    """Parse a skill path into (registry/owner, repo, skill_name).

    Examples
    --------
        "anthropics/skills/pdf" -> ("anthropics", "skills", "pdf")
        "my-org/my-repo/custom" -> ("my-org", "my-repo", "custom")
        "pdf" -> ("anthropic", "skills", "pdf")  # shorthand for anthropic registry
    """
    parts = path.strip("/").split("/")

    if len(parts) == 1:
        # Shorthand: assume anthropic registry
        return ("anthropics", "skills", parts[0])
    if len(parts) == 2:
        # owner/skill - assume 'skills' repo
        return (parts[0], "skills", parts[1])
    if len(parts) >= 3:
        return (parts[0], parts[1], "/".join(parts[2:]))
    msg = f"Invalid skill path: {path}"
    raise ValueError(msg)


async def _download_file(url: str, dest: Path) -> bool:
    """Download a file from URL to destination."""
    try:
        logger.debug(f"Downloading {url} -> {dest}")
        async with httpx.AsyncClient(timeout=30) as client:
            resp = await client.get(url)
            if resp.status_code == 404:
                logger.debug(f"Not found: {url}")
                return False
            resp.raise_for_status()
            dest.parent.mkdir(parents=True, exist_ok=True)
            dest.write_bytes(resp.content)
        return True
    except httpx.HTTPStatusError as e:
        logger.warning(f"HTTP error downloading {url}: {e}")
        return False
    except Exception as e:
        logger.warning(f"Error downloading {url}: {e}")
        return False


async def install_skill(
    path: str,
    *,
    source: str = "github",
    dest_dir: Path | None = None,
    force: bool = False,
) -> SkillInstallResult:
    """Install a skill from a repository or local path.

    Args:
        path: Skill path (e.g., "anthropics/skills/pdf" or "/local/path")
        source: "github" or "local"
        dest_dir: Custom destination directory
        force: Overwrite if exists

    Returns
    -------
        SkillInstallResult with skill info and status
    """
    dest_dir = dest_dir or _ensure_skills_dir()

    if source == "local":
        return await _install_from_local(Path(path), dest_dir, force)
    return await _install_from_github(path, dest_dir, force)


async def _list_github_dir(
    owner: str,
    repo: str,
    skill_path: str,
    subdir: str,
) -> list[dict[str, str]]:
    """List files in a GitHub directory via the Contents API.

    Returns
    -------
    list[dict[str, str]]
        Each dict has ``name``, ``download_url``, and ``type``
        (``"file"`` or ``"dir"``).
    """
    api_url = f"https://api.github.com/repos/{owner}/{repo}/contents/{skill_path}/{subdir}"
    try:
        async with httpx.AsyncClient(timeout=15) as client:
            resp = await client.get(
                api_url,
                headers={"Accept": "application/vnd.github.v3+json"},
            )
            if resp.status_code != 200:
                return []
            entries = resp.json()
            if not isinstance(entries, list):
                return []
            return [
                {
                    "name": e.get("name", ""),
                    "download_url": e.get("download_url", ""),
                    "type": e.get("type", ""),
                }
                for e in entries
                if isinstance(e, dict)
            ]
    except Exception as e:
        logger.debug("Failed to list GitHub dir %s/%s: %s", skill_path, subdir, e)
        return []


async def _download_github_subdir(
    owner: str,
    repo: str,
    skill_path: str,
    subdir: str,
    dest: Path,
) -> list[str]:
    """Download all files in a GitHub sub-directory to *dest*.

    Returns list of filenames successfully downloaded.
    """
    entries = await _list_github_dir(owner, repo, skill_path, subdir)
    downloaded: list[str] = []
    for entry in entries:
        if entry["type"] != "file" or not entry["download_url"]:
            continue
        file_dest = dest / subdir / entry["name"]
        if await _download_file(entry["download_url"], file_dest):
            downloaded.append(entry["name"])
    return downloaded


async def _install_from_github(
    path: str,
    dest_dir: Path,
    force: bool,
) -> SkillInstallResult:
    """Install a skill from GitHub."""
    owner, repo, skill_name = _parse_skill_path(path)

    # Determine the base URL
    base_url = f"https://raw.githubusercontent.com/{owner}/{repo}/main"

    # Try different skill locations
    skill_paths = [
        f"skills/{skill_name}",  # anthropics/skills structure
        f"{skill_name}",  # root level
    ]

    skill_md_url = None
    resolved_skill_path: str | None = None

    async with httpx.AsyncClient(timeout=10) as client:
        for sp in skill_paths:
            test_url = f"{base_url}/{sp}/SKILL.md"
            try:
                resp = await client.head(test_url)
                if resp.status_code == 200:
                    skill_md_url = test_url
                    resolved_skill_path = sp
                    break
            except (httpx.HTTPError, OSError, ValueError):
                continue

    if not skill_md_url or resolved_skill_path is None:
        return SkillInstallResult(
            success=False,
            error=f"SKILL.md not found for {path}",
            tried=[f"{base_url}/{sp}/SKILL.md" for sp in skill_paths],
        )

    # Create destination
    skill_dest = dest_dir / skill_name
    if skill_dest.exists() and not force:
        return SkillInstallResult(
            success=False,
            error=f"Skill already installed: {skill_name}. Use force=True to overwrite.",
            path=str(skill_dest),
        )

    if skill_dest.exists():
        shutil.rmtree(skill_dest)

    skill_dest.mkdir(parents=True, exist_ok=True)

    # Download SKILL.md
    skill_md_path = skill_dest / "SKILL.md"
    if not await _download_file(skill_md_url, skill_md_path):
        return SkillInstallResult(success=False, error="Failed to download SKILL.md")

    # Download skill.yaml (optional manifest)
    skill_yaml_url = f"{base_url}/{resolved_skill_path}/skill.yaml"
    await _download_file(skill_yaml_url, skill_dest / "skill.yaml")

    # Download tools/, pathways/, agents/ via GitHub Contents API
    tools_downloaded = await _download_github_subdir(
        owner, repo, resolved_skill_path, "tools", skill_dest
    )
    pathways_downloaded = await _download_github_subdir(
        owner, repo, resolved_skill_path, "pathways", skill_dest
    )
    agents_downloaded = await _download_github_subdir(
        owner, repo, resolved_skill_path, "agents", skill_dest
    )

    # Also try scripts/ (legacy external-skill format)
    scripts_downloaded = await _download_github_subdir(
        owner, repo, resolved_skill_path, "scripts", skill_dest
    )
    tools_downloaded.extend(scripts_downloaded)

    # Parse SKILL.md for metadata
    skill_content = skill_md_path.read_text()
    metadata = _parse_skill_metadata(skill_content)

    # Save install info
    install_info = InstalledSkillInfo(
        name=skill_name,
        source=f"github:{owner}/{repo}",
        path=str(skill_dest),
        metadata=metadata,
        tools_downloaded=tools_downloaded,
    )

    (skill_dest / ".install_info.json").write_text(install_info.model_dump_json(indent=2))

    logger.info(f"Installed skill: {skill_name} from {owner}/{repo}")

    return SkillInstallResult(
        success=True,
        skill_name=skill_name,
        path=str(skill_dest),
        metadata=metadata,
        tools=tools_downloaded,
        pathways_downloaded=pathways_downloaded,
        agents_downloaded=agents_downloaded,
    )


async def _install_from_local(
    source_path: Path,
    dest_dir: Path,
    force: bool,
) -> SkillInstallResult:
    """Install a skill from a local directory."""
    if not source_path.exists():
        return SkillInstallResult(success=False, error=f"Path not found: {source_path}")

    skill_md = source_path / "SKILL.md"
    if not skill_md.exists():
        return SkillInstallResult(success=False, error=f"SKILL.md not found in {source_path}")

    skill_name = source_path.name
    skill_dest = dest_dir / skill_name

    if skill_dest.exists() and not force:
        return SkillInstallResult(
            success=False,
            error=f"Skill already installed: {skill_name}",
        )

    if skill_dest.exists():
        shutil.rmtree(skill_dest)

    # Copy the entire skill directory
    shutil.copytree(source_path, skill_dest)

    # Parse metadata
    metadata = _parse_skill_metadata(skill_md.read_text())

    logger.info(f"Installed skill: {skill_name} from {source_path}")

    return SkillInstallResult(
        success=True,
        skill_name=skill_name,
        path=str(skill_dest),
        metadata=metadata,
    )


def _parse_skill_metadata(content: str) -> SkillMetadata:
    """Parse SKILL.md frontmatter for metadata."""
    fields: dict[str, str] = {}

    if content.startswith("---"):
        parts = content.split("---", 2)
        if len(parts) >= 3:
            # Parse YAML frontmatter
            frontmatter = parts[1].strip()
            for line in frontmatter.split("\n"):
                if ":" in line:
                    key, value = line.split(":", 1)
                    fields[key.strip()] = value.strip()

    # Also extract first heading as name if not in frontmatter
    if "name" not in fields:
        for line in content.split("\n"):
            if line.startswith("# "):
                fields["name"] = line[2:].strip()
                break

    return SkillMetadata.model_validate(fields)


def list_installed(skills_dir: Path | None = None) -> list[InstalledSkillInfo]:
    """List all installed skills."""
    skills_dir = skills_dir or SKILLS_DIR

    if not skills_dir.exists():
        return []

    installed: list[InstalledSkillInfo] = []
    for skill_path in skills_dir.iterdir():
        if not skill_path.is_dir():
            continue

        info_file = skill_path / ".install_info.json"
        if info_file.exists():
            try:
                raw = json.loads(info_file.read_text())
                installed.append(InstalledSkillInfo.model_validate(raw))
            except (json.JSONDecodeError, OSError):
                installed.append(InstalledSkillInfo(name=skill_path.name, path=str(skill_path)))
        else:
            # Check for SKILL.md
            skill_md = skill_path / "SKILL.md"
            if skill_md.exists():
                installed.append(
                    InstalledSkillInfo(
                        name=skill_path.name,
                        path=str(skill_path),
                        metadata=_parse_skill_metadata(skill_md.read_text()),
                    ),
                )

    return installed


def uninstall_skill(skill_name: str, skills_dir: Path | None = None) -> bool:
    """Uninstall a skill by name."""
    skills_dir = skills_dir or SKILLS_DIR
    skill_path = skills_dir / skill_name

    if skill_path.exists():
        shutil.rmtree(skill_path)
        logger.info(f"Uninstalled skill: {skill_name}")
        return True

    return False


__all__ = [
    "SKILLS_DIR",
    "InstalledSkillInfo",
    "SkillInstallResult",
    "SkillMetadata",
    "install_skill",
    "list_installed",
    "uninstall_skill",
]
