"""Tool installation utilities."""

import re
import shutil
import subprocess
import sys
import tomllib

from enum import Enum, auto
from pathlib import Path
from typing import TYPE_CHECKING
from urllib.parse import urlparse

if TYPE_CHECKING:
    from ai_rules.bootstrap.updater import ToolSpec


class ToolSource(Enum):
    """Source from which a tool was installed."""

    PYPI = auto()
    GITHUB = auto()
    LOCAL = auto()


def make_github_install_url(repo: str) -> str:
    """Construct GitHub install URL for uv tool install.

    Args:
        repo: GitHub repository in format "owner/repo"

    Returns:
        Full git+ssh URL for uv tool install
    """
    return f"git+ssh://git@github.com/{repo}.git"


UV_NOT_FOUND_ERROR = "uv not found in PATH. Install from https://docs.astral.sh/uv/"


def _validate_package_name(package_name: str) -> bool:
    """Validate package name matches PyPI naming convention (PEP 508)."""
    return bool(re.match(r"^[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?$", package_name))


def _is_github_git_reference(git_ref: str) -> bool:
    """Return True if git_ref points to github.com using supported git URL formats."""
    parsed = urlparse(git_ref)
    if parsed.hostname:
        return parsed.hostname.lower() == "github.com"

    # Handle SCP-like SSH syntax, e.g. git@github.com:owner/repo.git
    if "@" in git_ref and ":" in git_ref:
        host_part = git_ref.split("@", 1)[1].split(":", 1)[0]
        return host_part.lower() == "github.com"

    return False


def get_tool_config_dir(package_name: str = "ai-agent-rules") -> Path:
    """Get config directory for a uv tool installation.

    Discovers the actual Python version directory via glob rather than
    assuming it matches the running interpreter.

    Args:
        package_name: Name of the uv tool package

    Returns:
        Path to the config directory in the uv tools location
    """

    from ai_rules.platform import get_lib_path_fragment, get_uv_tools_dir

    tools_base = get_uv_tools_dir() / package_name

    candidates = sorted(
        tools_base.glob("lib/python*/site-packages/ai_rules/config"),
        reverse=True,
    )
    if not candidates:
        candidates = list(tools_base.glob("Lib/site-packages/ai_rules/config"))
    if candidates:
        return candidates[0]

    python_version = f"python{sys.version_info.major}.{sys.version_info.minor}"
    return tools_base / get_lib_path_fragment(python_version) / "ai_rules" / "config"


def get_tool_source(package_name: str) -> ToolSource | None:
    """Detect how a uv tool was installed.

    Args:
        package_name: Name of the uv tool package

    Returns:
        ToolSource.PYPI if installed from PyPI
        ToolSource.GITHUB if installed from GitHub
        ToolSource.LOCAL if installed from a local path
        None if tool not installed or receipt file not found
    """
    from ai_rules.platform import get_uv_tools_dir

    receipt_path = get_uv_tools_dir() / package_name / "uv-receipt.toml"

    if not receipt_path.exists():
        return None

    try:
        with open(receipt_path, "rb") as f:
            receipt = tomllib.load(f)

        requirements = receipt.get("tool", {}).get("requirements", [])
        if not requirements:
            return None

        first_req = requirements[0]
        if isinstance(first_req, dict):
            if "git" in first_req and _is_github_git_reference(str(first_req["git"])):
                return ToolSource.GITHUB
            if (
                "path" in first_req
                or "directory" in first_req
                or "editable" in first_req
            ):
                return ToolSource.LOCAL

        return ToolSource.PYPI

    except OSError, tomllib.TOMLDecodeError, KeyError, IndexError:
        return None


def is_command_available(command: str) -> bool:
    """Check if a command is available in PATH.

    Args:
        command: Command name to check

    Returns:
        True if command is available, False otherwise
    """
    return shutil.which(command) is not None


def install_tool(
    package_name: str = "ai-agent-rules",
    from_github: bool = False,
    github_url: str | None = None,
    local_path: str | None = None,
    force: bool = False,
    dry_run: bool = False,
) -> tuple[bool, str]:
    """Install package as a uv tool.

    Args:
        package_name: Name of package to install (ignored if from_github or local_path)
        from_github: Install from GitHub instead of PyPI
        github_url: GitHub URL to install from (only used if from_github=True)
        local_path: Local filesystem path to install from (takes priority over from_github)
        force: Force reinstall if already installed
        dry_run: Show what would be done without executing

    Returns:
        Tuple of (success, message)
    """
    if not local_path and not from_github and not _validate_package_name(package_name):
        return False, f"Invalid package name: {package_name}"

    if not is_command_available("uv"):
        return False, UV_NOT_FOUND_ERROR

    if local_path:
        source = str(Path(local_path).expanduser().resolve())
    elif from_github:
        if not github_url:
            raise ValueError("github_url is required when from_github=True")
        source = github_url
    else:
        source = package_name
    cmd = ["uv", "tool", "install", source]

    if force:
        cmd.insert(3, "--force")
        if from_github or local_path:
            cmd.insert(4, "--reinstall")

    if dry_run:
        return True, f"Would run: {' '.join(cmd)}"

    try:
        result = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=60,
        )

        if result.returncode == 0:
            return True, "Installation successful"

        error_msg = result.stderr.strip()
        if not error_msg:
            error_msg = "Installation failed with no error message"

        return False, error_msg

    except subprocess.TimeoutExpired:
        return False, "Installation timed out after 60 seconds"
    except Exception as e:
        return False, f"Unexpected error: {e}"


def uninstall_tool(package_name: str = "ai-agent-rules") -> tuple[bool, str]:
    """Uninstall package from uv tools.

    Args:
        package_name: Name of package to uninstall

    Returns:
        Tuple of (success, message)
    """
    if not _validate_package_name(package_name):
        return False, f"Invalid package name: {package_name}"

    if not is_command_available("uv"):
        return False, UV_NOT_FOUND_ERROR

    cmd = ["uv", "tool", "uninstall", package_name]

    try:
        result = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=30,
        )

        if result.returncode == 0:
            return True, "Uninstallation successful"

        error_msg = result.stderr.strip()
        if not error_msg:
            error_msg = "Uninstallation failed with no error message"

        return False, error_msg

    except subprocess.TimeoutExpired:
        return False, "Uninstallation timed out after 30 seconds"
    except Exception as e:
        return False, f"Unexpected error: {e}"


def get_tool_version(tool_name: str) -> str | None:
    """Get installed version of a uv tool by parsing `uv tool list`.

    Args:
        tool_name: Name of the tool package (e.g., "claude-code-statusline")

    Returns:
        Version string (e.g., "0.7.1") or None if not installed
    """
    if not _validate_package_name(tool_name):
        return None

    if not is_command_available("uv"):
        return None

    try:
        result = subprocess.run(
            ["uv", "tool", "list"],
            capture_output=True,
            text=True,
            timeout=10,
        )

        if result.returncode != 0:
            return None

        for line in result.stdout.splitlines():
            if line.startswith(tool_name + " "):
                match = re.search(r"v?(\d+\.\d+\.\d+)", line)
                if match:
                    return match.group(1)

        return None

    except subprocess.TimeoutExpired, Exception:
        return None


def get_effective_install_source(
    tool_id: str, cli_github_flag: bool = False, config: object | None = None
) -> tuple[ToolSource, str | None]:
    """Resolve the install source for a tool.

    Priority (highest first):
    1. cli_github_flag — explicit session override (--github flag)
    2. Provided config or merged active config managed_tools.install_sources
    3. Default: (ToolSource.PYPI, None)

    Args:
        tool_id: Tool identifier (e.g., "statusline", "ai-agent-rules")
        cli_github_flag: True if --github was passed on the CLI
        config: Optional already-loaded Config to avoid active-profile lookups

    Returns:
        Tuple of (ToolSource, local_path). local_path is set only for LOCAL source.
    """
    if cli_github_flag:
        return ToolSource.GITHUB, None
    try:
        if config is None:
            from ai_rules.config import Config

            config = Config.load()

        source_getter = getattr(config, "get_tool_install_source", None)
        if not callable(source_getter):
            return ToolSource.PYPI, None

        source = source_getter(tool_id)
        if source == "github":
            return ToolSource.GITHUB, None
        if source == "pypi":
            return ToolSource.PYPI, None
        if source and source.startswith("local:"):
            local_path = source[len("local:") :]
            return ToolSource.LOCAL, local_path
    except Exception as e:
        import logging

        logging.getLogger(__name__).debug(
            f"Config load failed in install source resolver: {e}"
        )
    return ToolSource.PYPI, None


def _switch_tool_source(
    spec: ToolSpec,
    source: ToolSource,
    local_path: str | None,
    dry_run: bool,
) -> tuple[str, str | None] | None:
    """Reinstall an installed tool from the desired source, if it differs.

    Returns None when the current source already matches (or is unknown).
    """
    current_source = get_tool_source(spec.package_name)
    if current_source is None or current_source == source:
        return None

    if dry_run:
        return (
            "source_switch_needed",
            f"Would switch {spec.display_name} from {current_source.name} to {source.name}",
        )

    uninstall_success, _ = uninstall_tool(spec.package_name)
    if uninstall_success:
        from_github = source == ToolSource.GITHUB
        github_url = spec.github_install_url if from_github else None
        success, _ = install_tool(
            spec.package_name,
            from_github=from_github,
            github_url=github_url,
            local_path=local_path,
            force=True,
        )
        if success:
            return "source_switched", f"{current_source.name} → {source.name}"
    return "failed", None


def _reinstall_local(
    spec: ToolSpec, local_path: str | None, dry_run: bool
) -> tuple[str, str | None]:
    """LOCAL source: always reinstall to pick up latest local changes."""
    success, message = install_tool(
        spec.package_name,
        local_path=local_path,
        force=True,
        dry_run=dry_run,
    )
    if dry_run:
        return "upgrade_available", message
    if success:
        return "upgraded", "reinstalled from local path"
    return "failed", None


def _check_and_apply_upgrade(
    spec: ToolSpec, dry_run: bool
) -> tuple[str, str | None] | None:
    """Upgrade an installed tool when an update is available. Fails open.

    Returns None when nothing was (or would be) upgraded.
    """
    try:
        from ai_rules.bootstrap.updater import (
            check_tool_updates,
            perform_tool_upgrade,
        )

        update_info = check_tool_updates(spec, timeout=10)
        if update_info and update_info.check_failed:
            import logging

            logging.getLogger(__name__).debug(
                f"Update check failed for {spec.display_name}, skipping upgrade"
            )
        elif update_info and update_info.has_update:
            if dry_run:
                return (
                    "upgrade_available",
                    f"Would upgrade {spec.display_name} {update_info.current_version} → {update_info.latest_version}",
                )
            success, msg, _ = perform_tool_upgrade(
                spec, is_self=(spec.tool_id == "ai-agent-rules")
            )
            if success:
                return (
                    "upgraded",
                    f"{update_info.current_version} → {update_info.latest_version}",
                )
    except Exception:
        pass
    return None


def ensure_tool_installed(
    spec: ToolSpec,
    dry_run: bool = False,
    source: ToolSource = ToolSource.PYPI,
    local_path: str | None = None,
    allow_source_switch: bool = False,
    skip_update_check: bool = False,
) -> tuple[str, str | None]:
    """Install or upgrade a tool if needed. Fails open.

    Args:
        spec: Tool specification describing how to install the tool
        dry_run: If True, show what would be done without executing
        source: Desired install source (PYPI, GITHUB, or LOCAL)
        local_path: Local filesystem path to install from (required when source=LOCAL)
        allow_source_switch: If True and the installed source differs from desired,
            uninstall and reinstall from the correct source
        skip_update_check: If True, skip the PyPI/GitHub upgrade check for already-installed tools

    Returns:
        Tuple of (status, message) where status is one of:
        "already_installed", "installed", "upgraded", "upgrade_available",
        "source_switched", "source_switch_needed", "failed"
        Message is only provided in dry_run mode or when upgraded/switched
    """
    if spec.is_installed():
        if allow_source_switch:
            switched = _switch_tool_source(spec, source, local_path, dry_run)
            if switched is not None:
                return switched

        if source == ToolSource.LOCAL:
            return _reinstall_local(spec, local_path, dry_run)

        if not skip_update_check:
            upgraded = _check_and_apply_upgrade(spec, dry_run)
            if upgraded is not None:
                return upgraded
        return "already_installed", None

    try:
        from_github = source == ToolSource.GITHUB
        github_url = spec.github_install_url if from_github else None
        success, message = install_tool(
            spec.package_name,
            from_github=from_github,
            github_url=github_url,
            local_path=local_path,
            force=False,
            dry_run=dry_run,
        )
        if success:
            return "installed", message if dry_run else None
        else:
            return "failed", None
    except Exception:
        return "failed", None


def ensure_tool_uninstalled(
    command_name: str,
    package_name: str,
    dry_run: bool = False,
) -> tuple[str, str | None]:
    """Remove a tool installed via uv if present.

    Args:
        command_name: Command to check for presence (e.g. "recall").
        package_name: The uv package name to uninstall (e.g. "recall-mcp-server").
        dry_run: If True, return what would happen without executing.

    Returns:
        Tuple of (status, message) where status is one of:
        "not_installed", "would_uninstall", "uninstalled", "failed"
    """
    uv_managed = get_tool_source(package_name) is not None
    cmd_present = is_command_available(command_name)
    if not uv_managed and not cmd_present:
        return "not_installed", None
    if dry_run:
        return "would_uninstall", f"Would uninstall {package_name}"
    success, message = uninstall_tool(package_name)
    return ("uninstalled", None) if success else ("failed", message)
