import json
import os
import shutil
import subprocess
from dataclasses import dataclass
from urllib.parse import urlparse

from .common import PROJECT_ROOT, REPO_ROOT

CONFIG_DIR: str = os.path.expanduser("~/.config/skget")
SETTINGS_PATH: str = os.path.join(CONFIG_DIR, "settings.json")
DEFAULT_SETTINGS_PATH: str = os.path.join(REPO_ROOT, "default.settings.json")

DEFAULT_AGENTS: dict[str, dict[str, str]] = {
    "claude": {"base_path": "~/.claude", "local_base_path": ".claude"},
    "codex": {"base_path": "~/.codex", "local_base_path": ".codex"},
}


@dataclass(frozen=True)
class AgentConfig:
    """Resolved agent paths from settings."""

    base_path: str
    local_base_path: str


def _run_git(args: list[str], cwd: str | None = None) -> None:
    try:
        subprocess.run(
            ["git", *args],
            cwd=cwd,
            check=True,
            capture_output=True,
            text=True,
        )
    except FileNotFoundError as exc:
        raise RuntimeError("git is required to clone skills from URL") from exc
    except subprocess.CalledProcessError as exc:
        detail = exc.stderr.strip() or exc.stdout.strip()
        msg = "git command failed"
        if detail:
            msg = f"{msg}: {detail}"
        raise RuntimeError(msg) from exc


def _parse_github_tree_url(url: str) -> tuple[str, str, str, str, str | None, str]:
    parsed = urlparse(url)
    parts = [part for part in parsed.path.split("/") if part]
    if len(parts) < 2:
        raise RuntimeError(f"invalid github skills url: {url}")
    owner = parts[0]
    repo = parts[1]
    repo_name = repo[:-4] if repo.endswith(".git") else repo
    repo_url = f"{parsed.scheme}://{parsed.netloc}/{owner}/{repo_name}"
    branch = None
    subpath = ""
    if len(parts) >= 4 and parts[2] == "tree":
        branch = parts[3]
        if len(parts) > 4:
            subpath = "/".join(parts[4:])
    return repo_url, parsed.netloc, owner, repo_name, branch, subpath


def _ensure_repo(
    repo_url: str,
    clone_root: str,
    branch: str | None,
    subpaths: list[str],
) -> None:
    cloned = False
    if not os.path.isdir(clone_root):
        os.makedirs(os.path.dirname(clone_root), exist_ok=True)
        _run_git(["clone", "--filter=blob:none", "--no-checkout", repo_url, clone_root])
        cloned = True
    elif not os.path.isdir(os.path.join(clone_root, ".git")):
        raise RuntimeError(f"existing path is not a git repo: {clone_root}")

    if subpaths:
        _run_git(["-C", clone_root, "sparse-checkout", "init", "--cone"])
        _run_git(["-C", clone_root, "sparse-checkout", "set", *subpaths])
    else:
        sparse_file = os.path.join(clone_root, ".git", "info", "sparse-checkout")
        if os.path.isfile(sparse_file):
            _run_git(["-C", clone_root, "sparse-checkout", "disable"])

    if branch:
        try:
            _run_git(["-C", clone_root, "checkout", branch])
        except RuntimeError:
            _run_git(["-C", clone_root, "fetch", "origin", branch])
            _run_git(["-C", clone_root, "checkout", branch])
    elif cloned or not os.path.isdir(os.path.join(clone_root, ".git", "refs")):
        _run_git(["-C", clone_root, "checkout"])


def resolve_skills_root(entry: str) -> str:
    parsed = urlparse(entry)
    if parsed.scheme in ("http", "https") and parsed.netloc:
        if parsed.netloc.lower() == "github.com":
            repo_url, host, owner, repo, branch, subpath = _parse_github_tree_url(entry)
            clone_root = (
                os.path.join(CONFIG_DIR, host, owner, repo, "tree", branch)
                if branch
                else os.path.join(CONFIG_DIR, host, owner, repo)
            )
            _ensure_repo(repo_url, clone_root, branch, [subpath] if subpath else [])
            return os.path.join(clone_root, subpath) if subpath else clone_root

        parts = [part for part in parsed.path.split("/") if part]
        if not parts:
            raise RuntimeError(f"invalid skills url: {entry}")
        repo = parts[-1]
        repo_name = repo[:-4] if repo.endswith(".git") else repo
        clone_root = os.path.join(CONFIG_DIR, parsed.netloc, *parts[:-1], repo_name)
        repo_url = (
            f"{parsed.scheme}://{parsed.netloc}/{'/'.join(parts[:-1] + [repo_name])}"
        )
        _ensure_repo(repo_url, clone_root, None, [])
        return clone_root

    expanded = os.path.expanduser(entry)
    return os.path.abspath(expanded)


def _load_settings() -> dict:
    if not os.path.isfile(SETTINGS_PATH):
        if not os.path.isfile(DEFAULT_SETTINGS_PATH):
            return {}
        os.makedirs(CONFIG_DIR, exist_ok=True)
        try:
            shutil.copyfile(DEFAULT_SETTINGS_PATH, SETTINGS_PATH)
        except OSError as exc:
            raise RuntimeError(
                f"unable to write settings file: {SETTINGS_PATH}"
            ) from exc
    try:
        with open(SETTINGS_PATH, "r", encoding="utf-8") as handle:
            data = json.load(handle)
    except (OSError, json.JSONDecodeError) as exc:
        raise RuntimeError(f"invalid settings file: {SETTINGS_PATH}") from exc
    if not isinstance(data, dict):
        raise RuntimeError(f"invalid settings file: {SETTINGS_PATH}")
    return data


def _resolve_agent_path(value: str, is_local: bool) -> str:
    expanded = os.path.expanduser(value)
    if is_local and not os.path.isabs(expanded):
        return os.path.abspath(os.path.join(PROJECT_ROOT, expanded))
    return os.path.abspath(expanded)


def get_agents() -> dict[str, AgentConfig]:
    settings = _load_settings()
    agents_value = settings.get("agents")
    if agents_value is None:
        agents_value = DEFAULT_AGENTS
    if not isinstance(agents_value, dict):
        raise RuntimeError("invalid settings: agents must be a mapping")

    agents: dict[str, AgentConfig] = {}
    for raw_name, raw_config in agents_value.items():
        if not isinstance(raw_name, str) or not raw_name:
            raise RuntimeError("invalid settings: agent names must be strings")
        if not isinstance(raw_config, dict):
            raise RuntimeError(f"invalid settings: agent config for {raw_name} must be a mapping")
        base_path = raw_config.get("base_path")
        if not isinstance(base_path, str) or not base_path:
            raise RuntimeError(f"invalid settings: agent {raw_name} is missing base_path")
        local_base_path = raw_config.get("local_base_path")
        if not isinstance(local_base_path, str) or not local_base_path:
            raise RuntimeError(
                f"invalid settings: agent {raw_name} local_base_path must be a string"
            )
        agents[raw_name] = AgentConfig(
            base_path=_resolve_agent_path(base_path, is_local=False),
            local_base_path=_resolve_agent_path(local_base_path, is_local=True),
        )
    return agents


def get_skills_roots() -> list[str]:
    settings = _load_settings()
    paths = settings.get("paths")
    if isinstance(paths, dict) and "skills" in paths:
        skills_value = paths.get("skills")
        if not isinstance(skills_value, list):
            raise RuntimeError("invalid settings: paths.skills must be a list")
        roots: list[str] = []
        github_entries: dict[tuple[str, str, str | None], dict[str, object]] = {}

        for entry in skills_value:
            if not isinstance(entry, str):
                continue
            parsed = urlparse(entry)
            if parsed.scheme in ("http", "https") and parsed.netloc.lower() == "github.com":
                repo_url, host, owner, repo, branch, subpath = _parse_github_tree_url(
                    entry
                )
                clone_root = (
                    os.path.join(CONFIG_DIR, host, owner, repo, "tree", branch)
                    if branch
                    else os.path.join(CONFIG_DIR, host, owner, repo)
                )
                key = (repo_url, clone_root, branch)
                if key not in github_entries:
                    github_entries[key] = {"full": False, "subpaths": []}
                if subpath:
                    subpaths = github_entries[key]["subpaths"]
                    if subpath not in subpaths:
                        subpaths.append(subpath)
                else:
                    github_entries[key]["full"] = True
                continue

            roots.append(resolve_skills_root(entry))

        for (repo_url, clone_root, branch), info in github_entries.items():
            full_checkout = bool(info["full"])
            subpaths = info["subpaths"]
            if full_checkout:
                _ensure_repo(repo_url, clone_root, branch, [])
                roots.append(clone_root)
            else:
                _ensure_repo(repo_url, clone_root, branch, subpaths)
                for subpath in subpaths:
                    roots.append(os.path.join(clone_root, subpath))

        return roots

    fallback = os.path.join(REPO_ROOT, "skills")
    if os.path.isdir(fallback):
        return [fallback]
    return []


def update_skills_repos() -> list[str]:
    """Pull configured remote skill repositories and return updated clone roots."""
    settings = _load_settings()
    paths = settings.get("paths")
    if not isinstance(paths, dict):
        return []
    skills_value = paths.get("skills")
    if not isinstance(skills_value, list):
        return []

    repos_to_update: dict[str, str | None] = {}
    for entry in skills_value:
        if not isinstance(entry, str):
            continue
        parsed = urlparse(entry)
        if parsed.scheme not in ("http", "https") or not parsed.netloc:
            continue

        if parsed.netloc.lower() == "github.com":
            repo_url, host, owner, repo, branch, subpath = _parse_github_tree_url(entry)
            clone_root = (
                os.path.join(CONFIG_DIR, host, owner, repo, "tree", branch)
                if branch
                else os.path.join(CONFIG_DIR, host, owner, repo)
            )
            _ensure_repo(repo_url, clone_root, branch, [subpath] if subpath else [])
            repos_to_update[clone_root] = branch
            continue

        parts = [part for part in parsed.path.split("/") if part]
        if not parts:
            raise RuntimeError(f"invalid skills url: {entry}")
        repo = parts[-1]
        repo_name = repo[:-4] if repo.endswith(".git") else repo
        clone_root = os.path.join(CONFIG_DIR, parsed.netloc, *parts[:-1], repo_name)
        repo_url = (
            f"{parsed.scheme}://{parsed.netloc}/{'/'.join(parts[:-1] + [repo_name])}"
        )
        _ensure_repo(repo_url, clone_root, None, [])
        repos_to_update[clone_root] = None

    updated_roots: list[str] = []
    for clone_root, branch in repos_to_update.items():
        if branch:
            _run_git(["-C", clone_root, "pull", "--ff-only", "origin", branch])
        else:
            _run_git(["-C", clone_root, "pull", "--ff-only"])
        updated_roots.append(clone_root)
    return sorted(updated_roots)
