"""GitHub CLI (gh) wrapper for local-first skill fetching."""

from __future__ import annotations

import hashlib
import io
import subprocess
import tarfile
import tempfile
from pathlib import Path

import yaml


class GhCliError(Exception):
    """Error from gh CLI operations."""


def check_gh_available() -> bool:
    """Check if the gh CLI is installed."""
    try:
        subprocess.run(
            ["gh", "--version"],
            capture_output=True,
            check=True,
        )
        return True
    except FileNotFoundError:
        return False
    except subprocess.CalledProcessError:
        return False


def check_gh_auth() -> bool:
    """Check if the user is authenticated with gh."""
    try:
        subprocess.run(
            ["gh", "auth", "status"],
            capture_output=True,
            check=True,
        )
        return True
    except (FileNotFoundError, subprocess.CalledProcessError):
        return False


def resolve_ref(repo: str) -> str:
    """Resolve the default branch of a repo.

    Args:
        repo: GitHub repo in 'owner/repo' format

    Returns:
        Default branch name (e.g. 'main', 'master')
    """
    try:
        result = subprocess.run(
            ["gh", "api", f"repos/{repo}", "--jq", ".default_branch"],
            capture_output=True,
            check=True,
            text=True,
        )
        branch = result.stdout.strip()
        if branch:
            return branch
    except subprocess.CalledProcessError as e:
        raise GhCliError(f"Failed to resolve ref for {repo}: {e.stderr.strip()}") from e

    return "main"


def get_commit_sha(repo: str, ref: str) -> str:
    """Get the short commit SHA for a given ref.

    Args:
        repo: GitHub repo in 'owner/repo' format
        ref: Git ref (branch name, tag, or SHA)

    Returns:
        Short commit SHA (7 chars)
    """
    try:
        result = subprocess.run(
            ["gh", "api", f"repos/{repo}/commits/{ref}", "--jq", ".sha"],
            capture_output=True,
            check=True,
            text=True,
        )
        sha = result.stdout.strip()
        if sha:
            return sha[:7]
    except subprocess.CalledProcessError:
        pass
    return ref


def download_repo_tarball(repo: str, ref: str) -> bytes:
    """Download a repo tarball using gh CLI.

    Args:
        repo: GitHub repo in 'owner/repo' format
        ref: Git ref (tag or branch)

    Returns:
        Raw tarball bytes
    """
    with tempfile.NamedTemporaryFile(suffix=".tar.gz", delete=False) as tmp:
        tmp_path = tmp.name

    try:
        # gh api follows redirects and writes binary to file
        subprocess.run(
            ["gh", "api", f"repos/{repo}/tarball/{ref}"],
            stdout=open(tmp_path, "wb"),
            stderr=subprocess.PIPE,
            check=True,
        )
        return Path(tmp_path).read_bytes()
    except subprocess.CalledProcessError as e:
        raise GhCliError(f"Failed to download {repo}@{ref}: {e.stderr.decode().strip()}") from e
    finally:
        Path(tmp_path).unlink(missing_ok=True)


def discover_skills_in_tarball(tarball_bytes: bytes, source_path: str) -> list[str]:
    """Discover skill directories under source_path in a tarball.

    If source_path itself contains SKILL.md, returns [source_path].
    Otherwise looks for immediate subdirectories with SKILL.md.

    Returns:
        List of skill paths relative to repo root, sorted alphabetically.
    """
    with tarfile.open(fileobj=io.BytesIO(tarball_bytes), mode="r:gz") as tar:
        members = tar.getmembers()
        if not members:
            return [source_path]

        prefix = members[0].name.split("/")[0]
        names = {m.name for m in members}

        # Check if source_path itself is a skill
        if f"{prefix}/{source_path}/SKILL.md" in names:
            return [source_path]

        # Look for sub-skills: source_path/*/SKILL.md
        sub_skills = []
        target = f"{prefix}/{source_path}/"
        for name in sorted(names):
            if not name.startswith(target):
                continue
            # Get the relative path after source_path/
            rest = name[len(target) :]
            # Match pattern: <subdir>/SKILL.md (exactly one level deep)
            if rest.count("/") == 1 and rest.endswith("/SKILL.md"):
                subdir = rest.split("/")[0]
                sub_skills.append(f"{source_path}/{subdir}")

        return sub_skills if sub_skills else [source_path]


def extract_and_package_skill(tarball_bytes: bytes, source_path: str) -> tuple[bytes, dict]:
    """Extract a skill subdirectory from a repo tarball and re-package as tar.gz.

    Args:
        tarball_bytes: Raw repo tarball bytes
        source_path: Path within the repo (e.g. 'agent-skills/milvus-deploy')

    Returns:
        Tuple of (clean_tar_gz_bytes, metadata_dict).
        metadata_dict keys: name, description, category, slug, archive_hash
    """
    metadata: dict = {}
    output_buf = io.BytesIO()
    found_files = False

    with tarfile.open(fileobj=io.BytesIO(tarball_bytes), mode="r:gz") as src_tar:
        members = src_tar.getmembers()
        if not members:
            raise GhCliError("Empty tarball")

        # GitHub tarballs have a top-level prefix like "owner-repo-hash/"
        prefix = members[0].name.split("/")[0]
        target_prefix = f"{prefix}/{source_path}"

        with tarfile.open(fileobj=output_buf, mode="w:gz") as dst_tar:
            for member in members:
                if not member.name.startswith(target_prefix):
                    continue

                # Strip the repo prefix, keep only skill-relative path
                relative = member.name[len(target_prefix) :].lstrip("/")
                if not relative and member.isdir():
                    continue  # Skip the skill root dir itself

                found_files = True

                # Parse SKILL.md frontmatter
                if relative == "SKILL.md" and member.isfile():
                    f = src_tar.extractfile(member)
                    if f:
                        content = f.read().decode("utf-8")
                        metadata = _parse_skill_md(content)

                # Clone member with rewritten name
                new_member = tarfile.TarInfo(name=relative)
                new_member.size = member.size
                new_member.mode = member.mode
                new_member.type = member.type

                if member.isfile():
                    f = src_tar.extractfile(member)
                    if f:
                        dst_tar.addfile(new_member, f)
                elif member.isdir():
                    new_member.type = tarfile.DIRTYPE
                    dst_tar.addfile(new_member)

    if not found_files:
        raise GhCliError(f"No files found at path '{source_path}' in tarball")

    archive_bytes = output_buf.getvalue()

    if not metadata:
        slug = source_path.rsplit("/", 1)[-1]
        metadata = {"name": slug, "slug": slug}

    if "slug" not in metadata:
        metadata["slug"] = source_path.rsplit("/", 1)[-1]

    metadata["archive_hash"] = hashlib.sha256(archive_bytes).hexdigest()

    return archive_bytes, metadata


def _parse_skill_md(content: str) -> dict:
    """Parse YAML frontmatter from SKILL.md content.

    Expected format:
        ---
        name: Milvus Deploy
        description: Deploy Milvus on K8s
        category: deployment
        ---
        ... markdown body ...
    """
    if not content.startswith("---"):
        return {}

    parts = content.split("---", 2)
    if len(parts) < 3:
        return {}

    try:
        fm = yaml.safe_load(parts[1])
        if not isinstance(fm, dict):
            return {}
        return {
            k: v
            for k, v in fm.items()
            if k in ("name", "description", "category") and isinstance(v, str)
        }
    except yaml.YAMLError:
        return {}
