# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
Host Setup Skill

The agent installs software and sets up programs on the host machine —
not just advises on how to do it.

A novice user can say "install nmap" or "download and set up SpiderFoot"
and the agent will:
  1. Detect the right package manager / install method
  2. Show exactly what it's going to do before touching anything
  3. Ask for confirmation
  4. Execute with live output
  5. Verify success and tell the user what to do next

Design principles:
  - Read operations (check_installed, detect_system_info, list_installed)
    → no confirmation, never destructive
  - Install / download / setup operations
    → always preview → confirm → act → report
  - Package installs use --dry-run first so the user sees the dep tree
  - File downloads verify SHA-256 checksum when provided before confirming
  - Binaries go to ~/.local/bin by default (no root needed for most tools)
  - Root-requiring operations (system package installs) show the sudo
    command clearly and require explicit confirmation

Supported install methods (auto-detected):
  dnf         Fedora / RHEL / Magic Hat OS (primary)
  apt         Debian / Ubuntu
  pacman      Arch
  brew        macOS
  pip         Python packages (system or venv)
  flatpak     Sandboxed desktop apps
  snap        Ubuntu snap packages
  binary      Direct download → chmod +x → ~/.local/bin
  appimage    AppImage → chmod +x → ~/Applications
"""

import hashlib
import logging
import os
import platform
import shlex
import shutil
import subprocess
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

from familiar.core.confirmations import needs_confirmation

logger = logging.getLogger(__name__)

# Default install directory for user-local binaries (no sudo needed)
USER_BIN = Path.home() / ".local" / "bin"
# AppImages
USER_APPS = Path.home() / "Applications"

# Timeout for package manager operations (dnf/apt can be slow)
PKG_TIMEOUT = 300
# Timeout for downloads
DOWNLOAD_TIMEOUT = 600

# Package managers in detection priority order
_PM_PRIORITY = ["dnf", "apt-get", "pacman", "zypper", "brew", "apk"]

# Human-readable names
_PM_NAMES = {
    "dnf": "DNF (Fedora/RHEL)",
    "apt-get": "APT (Debian/Ubuntu)",
    "pacman": "Pacman (Arch)",
    "zypper": "Zypper (openSUSE)",
    "brew": "Homebrew (macOS)",
    "apk": "APK (Alpine)",
}


# ---------------------------------------------------------------------------
# System detection helpers
# ---------------------------------------------------------------------------


def _detect_package_manager() -> Optional[str]:
    """Return the best available system package manager, or None."""
    for pm in _PM_PRIORITY:
        if shutil.which(pm):
            return pm
    return None


def _pm_dry_run_cmd(pm: str, packages: List[str]) -> List[str]:
    """Return the dry-run command for a package manager."""
    _dry_run_map = {
        "dnf": ["dnf", "install", "--assumeno"] + packages,
        "apt-get": ["apt-get", "install", "--dry-run", "--no-install-recommends"] + packages,
        "pacman": ["pacman", "-S", "--print"] + packages,
        "brew": ["brew", "install", "--dry-run"] + packages,
        "zypper": ["zypper", "--dry-run", "install"] + packages,
        "apk": ["apk", "add", "--simulate"] + packages,
    }
    return _dry_run_map.get(pm, [])


def _pm_install_cmd(pm: str, packages: List[str], assume_yes: bool = True) -> List[str]:
    """Return the install command for a package manager."""
    yes_flag = {
        "dnf": ["-y"],
        "apt-get": ["-y"],
        "pacman": ["--noconfirm"],
        "brew": [],
        "zypper": ["-y"],
        "apk": [],
    }
    flags = yes_flag.get(pm, [])
    _install_map = {
        "dnf": ["dnf", "install"] + flags + packages,
        "apt-get": ["apt-get", "install"] + flags + packages,
        "pacman": ["pacman", "-S"] + flags + packages,
        "brew": ["brew", "install"] + packages,
        "zypper": ["zypper", "install"] + flags + packages,
        "apk": ["apk", "add"] + packages,
    }
    return _install_map.get(pm, [])


def _pm_remove_cmd(pm: str, packages: List[str]) -> List[str]:
    _map = {
        "dnf": ["dnf", "remove", "-y"] + packages,
        "apt-get": ["apt-get", "remove", "-y"] + packages,
        "pacman": ["pacman", "-R", "--noconfirm"] + packages,
        "brew": ["brew", "uninstall"] + packages,
        "zypper": ["zypper", "remove", "-y"] + packages,
        "apk": ["apk", "del"] + packages,
    }
    return _map.get(pm, [])


def _needs_sudo(pm: str) -> bool:
    """Return True if this package manager typically requires root."""
    return pm in ("dnf", "apt-get", "pacman", "zypper", "apk")


def _run(cmd: List[str], timeout: int = PKG_TIMEOUT, input_text: str = "") -> Tuple[int, str, str]:
    """Run a command safely (shell=False). Returns (returncode, stdout, stderr)."""
    try:
        result = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=timeout,
            input=input_text or None,
        )
        return result.returncode, result.stdout, result.stderr
    except subprocess.TimeoutExpired:
        return -1, "", f"Command timed out after {timeout}s"
    except FileNotFoundError as e:
        return -1, "", f"Command not found: {e}"
    except Exception as e:
        return -1, "", str(e)


def _sha256(path: Path) -> str:
    """Return hex SHA-256 of a file."""
    h = hashlib.sha256()
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(65536), b""):
            h.update(chunk)
    return h.hexdigest()


def _format_bytes(n: int) -> str:
    for unit in ("B", "KB", "MB", "GB"):
        if n < 1024:
            return f"{n:.1f} {unit}"
        n /= 1024
    return f"{n:.1f} TB"


# ---------------------------------------------------------------------------
# Tool handlers
# ---------------------------------------------------------------------------


def detect_system_info(data: dict) -> dict:
    """
    Detect the host system's OS, architecture, package manager, disk space,
    and available software installation methods.

    Read-only. No confirmation needed.

    Returns:
        Dict with os, arch, package_manager, python, pip, disk_free_gb,
        install_methods, and PATH.
    """
    info: Dict[str, Any] = {}

    # OS
    info["os"] = platform.system()
    info["os_release"] = platform.release()
    info["os_version"] = platform.version()[:100]
    info["arch"] = platform.machine()
    info["hostname"] = platform.node()

    # Try to read /etc/os-release for distro name
    try:
        with open("/etc/os-release") as f:
            for line in f:
                if line.startswith("PRETTY_NAME="):
                    info["distro"] = line.split("=", 1)[1].strip().strip('"')
                    break
    except (OSError, FileNotFoundError):
        pass

    # Package managers
    pm = _detect_package_manager()
    info["package_manager"] = pm
    info["package_manager_name"] = _PM_NAMES.get(pm, pm) if pm else "none detected"

    # Install methods
    methods = []
    if pm:
        methods.append(pm)
    if shutil.which("pip3") or shutil.which("pip"):
        methods.append("pip")
    if shutil.which("flatpak"):
        methods.append("flatpak")
    if shutil.which("snap"):
        methods.append("snap")
    methods.append("binary")  # always possible
    methods.append("appimage")  # always possible on Linux
    info["install_methods"] = methods

    # Python / pip
    info["python"] = sys.executable
    info["python_version"] = platform.python_version()
    pip = shutil.which("pip3") or shutil.which("pip")
    info["pip"] = pip

    # Disk space (root filesystem)
    try:
        usage = shutil.disk_usage("/")
        info["disk_total_gb"] = round(usage.total / 1e9, 1)
        info["disk_used_gb"] = round(usage.used / 1e9, 1)
        info["disk_free_gb"] = round(usage.free / 1e9, 1)
    except Exception:
        pass

    # PATH entries
    info["PATH"] = os.environ.get("PATH", "").split(":")

    # Systemd
    info["systemd"] = bool(shutil.which("systemctl"))

    return info


def check_installed(data: dict) -> dict:
    """
    Check whether a program is installed and get its version.

    Read-only. No confirmation needed.

    Args:
        name: Program name to check (e.g. 'nmap', 'python3', 'docker').
        version_flag: Flag to get version output (default '--version').

    Returns:
        Dict with installed (bool), path, version, name.
    """
    name = (data.get("name") or "").strip()
    if not name:
        return {"error": "name is required"}

    version_flag = data.get("version_flag", "--version")
    path = shutil.which(name)

    if not path:
        return {"installed": False, "name": name, "path": None, "version": None}

    # Try to get version
    version = None
    try:
        rc, stdout, stderr = _run([path, version_flag], timeout=10)
        output = (stdout + stderr).strip()
        # Take first non-empty line
        for line in output.splitlines():
            line = line.strip()
            if line:
                version = line[:120]
                break
    except Exception:
        pass

    return {"installed": True, "name": name, "path": path, "version": version}


def list_installed(data: dict) -> dict:
    """
    List installed packages matching a search term.

    Read-only. No confirmation needed.

    Args:
        filter: Search term to filter package names (e.g. 'python', 'net-tools').
        manager: Package manager to query. Auto-detected if not specified.

    Returns:
        Dict with packages (list of name+version), count, manager.
    """
    filter_term = (data.get("filter") or "").strip()
    pm = data.get("manager") or _detect_package_manager()

    if not pm:
        return {"error": "No package manager detected", "packages": [], "count": 0}

    _list_cmds = {
        "dnf": ["dnf", "list", "--installed"] + ([filter_term] if filter_term else []),
        "apt-get": ["dpkg", "--list"] + ([f"*{filter_term}*"] if filter_term else []),
        "pacman": ["pacman", "-Qs"] + ([filter_term] if filter_term else []),
        "brew": ["brew", "list", "--versions"] + ([filter_term] if filter_term else []),
    }
    cmd = _list_cmds.get(pm)
    if not cmd:
        return {"error": f"list not supported for {pm}", "packages": [], "count": 0}

    rc, stdout, stderr = _run(cmd, timeout=30)
    lines = [l.strip() for l in stdout.splitlines() if l.strip()]

    # Keep reasonable result set
    packages = lines[:100]
    return {
        "packages": packages,
        "count": len(packages),
        "manager": pm,
        "filter": filter_term,
        "truncated": len(lines) > 100,
    }


def install_package(data: dict):
    """
    Install one or more system or Python packages on the host machine.

    Runs a dry-run first to show the dependency tree, then asks for
    confirmation before installing anything.

    Args:
        packages: Package name(s) to install — string or list.
            Examples: 'nmap', ['nmap', 'iperf3'], 'requests', 'spiderfoot'
        manager: Installation method: 'auto' (default), 'dnf', 'apt', 'pip',
            'flatpak', 'snap'.  'auto' picks the system package manager first,
            falls back to pip for Python packages.
        reason: Why this package is needed (shown in confirmation).

    Returns:
        ConfirmationRequired if not yet confirmed.
        Dict with installed=True and output on success.
    """
    # Normalise packages to list
    raw_pkgs = data.get("packages") or data.get("package") or ""
    if isinstance(raw_pkgs, str):
        packages = [p.strip() for p in raw_pkgs.split(",") if p.strip()]
    else:
        packages = [str(p).strip() for p in raw_pkgs if str(p).strip()]

    if not packages:
        return {"error": "packages is required (e.g. 'nmap' or ['nmap', 'iperf3'])"}

    manager = (data.get("manager") or "auto").strip().lower()
    reason = (data.get("reason") or f"installing {', '.join(packages)}").strip()

    # Resolve manager
    if manager == "auto":
        pm = _detect_package_manager()
        manager = pm if pm else "pip"
    elif manager == "apt":
        manager = "apt-get"

    if data.get("_confirmed"):
        # Execute the actual install
        if manager == "pip":
            cmd = [sys.executable, "-m", "pip", "install"] + packages
        elif manager == "flatpak":
            cmd = ["flatpak", "install", "--noninteractive"] + packages
        elif manager == "snap":
            cmd = ["snap", "install"] + packages
        else:
            cmd = _pm_install_cmd(manager, packages)
            if _needs_sudo(manager):
                cmd = ["sudo"] + cmd

        rc, stdout, stderr = _run(cmd, timeout=PKG_TIMEOUT)
        output = (stdout + stderr).strip()
        success = rc == 0
        return {
            "installed": success,
            "packages": packages,
            "manager": manager,
            "output": output[:4000],
            "message": (
                f"{'Installed' if success else 'Installation failed for'}: {', '.join(packages)}"
            ),
        }

    # Check for prior installation knowledge
    prior_knowledge = ""
    try:
        from familiar.skills.host_setup.knowledge import search_install_knowledge

        matches = search_install_knowledge(packages[0] if packages else "")
        if matches:
            best = matches[0]
            prior_knowledge = (
                f"\n\nPrior install found: {best.get('topic', 'unknown')}\n"
                f"  Method: {best.get('install_method', 'N/A')}\n"
                f"  Platform: {best.get('platform', 'N/A')}\n"
                f"  Command: {best.get('command', 'N/A')}"
            )
    except Exception:
        pass

    # Dry-run preview
    if manager == "pip":
        dry_cmd = [sys.executable, "-m", "pip", "install", "--dry-run", "--quiet"] + packages
    elif manager in _PM_PRIORITY:
        dry_cmd = _pm_dry_run_cmd(manager, packages)
        if _needs_sudo(manager):
            dry_cmd = ["sudo"] + dry_cmd
    else:
        dry_cmd = []

    dry_output = ""
    if dry_cmd:
        rc, stdout, stderr = _run(dry_cmd, timeout=60)
        dry_output = (stdout + stderr).strip()[:800]

    needs_sudo = _needs_sudo(manager) and manager in _PM_PRIORITY
    sudo_note = "\n  (requires sudo — you may be prompted for your password)" if needs_sudo else ""

    preview_lines = [
        f"Install: {', '.join(packages)}",
        f"  Method  : {_PM_NAMES.get(manager, manager)}",
        f"  Reason  : {reason}" + sudo_note,
    ]
    if dry_output:
        preview_lines += ["", "What will be installed:", dry_output]
    if prior_knowledge:
        preview_lines.append(prior_knowledge)

    return needs_confirmation(
        tool_name="install_package",
        tool_input=data,
        preview="\n".join(preview_lines),
        risk="medium",
    )


def download_file(data: dict):
    """
    Download a file from a URL to the host machine.

    If a SHA-256 checksum is provided, the download is verified before
    asking the user to confirm.  Partial downloads are cleaned up on failure.

    Args:
        url: URL to download from.
        destination: Local path to save the file.
            Defaults to ~/Downloads/<filename>.
        checksum: Optional SHA-256 hex digest to verify the download.
        reason: Why this file is needed.

    Returns:
        ConfirmationRequired if not yet confirmed.
        Dict with downloaded=True, path, size_bytes, checksum_verified on success.
    """
    url = (data.get("url") or "").strip()
    if not url:
        return {"error": "url is required"}

    reason = (data.get("reason") or f"download {url.split('/')[-1]}").strip()

    # Resolve destination
    raw_dest = data.get("destination") or ""
    if raw_dest:
        dest = Path(raw_dest).expanduser().resolve()
    else:
        filename = url.rstrip("/").split("/")[-1].split("?")[0] or "downloaded_file"
        dest = Path.home() / "Downloads" / filename

    expected_checksum = (data.get("checksum") or "").strip().lower()

    if data.get("_confirmed"):
        dest.parent.mkdir(parents=True, exist_ok=True)

        # Download via httpx (already a familiar dep) with progress tracking
        try:
            import httpx

            with httpx.Client(follow_redirects=True, timeout=DOWNLOAD_TIMEOUT) as client:
                with client.stream("GET", url) as response:
                    response.raise_for_status()
                    total = int(response.headers.get("content-length", 0))
                    downloaded = 0
                    with open(dest, "wb") as f:
                        for chunk in response.iter_bytes(chunk_size=65536):
                            f.write(chunk)
                            downloaded += len(chunk)

        except ImportError:
            # Fall back to urllib
            import urllib.request

            urllib.request.urlretrieve(url, dest)
            downloaded = dest.stat().st_size

        except Exception as e:
            if dest.exists():
                dest.unlink(missing_ok=True)
            return {"downloaded": False, "error": str(e)}

        # Verify checksum if provided
        actual_checksum = _sha256(dest)
        checksum_ok = True
        if expected_checksum:
            checksum_ok = actual_checksum == expected_checksum
            if not checksum_ok:
                dest.unlink(missing_ok=True)
                return {
                    "downloaded": False,
                    "error": (
                        f"Checksum mismatch — file deleted for safety.\n"
                        f"  Expected : {expected_checksum}\n"
                        f"  Got      : {actual_checksum}"
                    ),
                }

        return {
            "downloaded": True,
            "path": str(dest),
            "size_bytes": downloaded,
            "size_human": _format_bytes(downloaded),
            "sha256": actual_checksum,
            "checksum_verified": bool(expected_checksum) and checksum_ok,
        }

    # Preview
    preview_lines = [
        "Download file",
        f"  From    : {url}",
        f"  To      : {dest}",
        f"  Reason  : {reason}",
    ]
    if expected_checksum:
        preview_lines.append("  Verify  : SHA-256 will be checked after download")

    return needs_confirmation(
        tool_name="download_file",
        tool_input=data,
        preview="\n".join(preview_lines),
        risk="medium",
    )


def setup_binary(data: dict):
    """
    Download a binary or AppImage, make it executable, and add it to
    ~/.local/bin (or ~/Applications for AppImages) so it's on the PATH.

    Use this for tools distributed as single-file binaries:
    - CLI tools (rclone, restic, mkcert, etc.)
    - AppImages (SpiderFoot, Obsidian, etc.)
    - Self-contained scripts

    Args:
        url: URL to download the binary from.
        name: Name for the installed command (e.g. 'rclone', 'spiderfoot').
        checksum: Optional SHA-256 hex digest for integrity verification.
        destination: Override install directory. Default: ~/.local/bin.
        reason: Why this program is needed.

    Returns:
        ConfirmationRequired if not yet confirmed.
        Dict with installed=True, path, on_path (bool) on success.
    """
    url = (data.get("url") or "").strip()
    name = (data.get("name") or url.rstrip("/").split("/")[-1].split("?")[0] or "binary").strip()
    reason = (data.get("reason") or f"install {name}").strip()
    checksum = (data.get("checksum") or "").strip().lower()

    if not url:
        return {"error": "url is required"}

    # Detect AppImage
    is_appimage = name.lower().endswith(".appimage") or url.lower().endswith(".appimage")
    if is_appimage:
        install_dir = USER_APPS
    else:
        raw_dest = data.get("destination") or ""
        install_dir = Path(raw_dest).expanduser() if raw_dest else USER_BIN

    dest_path = install_dir / name

    if data.get("_confirmed"):
        install_dir.mkdir(parents=True, exist_ok=True)

        # Download
        try:
            import httpx

            with httpx.Client(follow_redirects=True, timeout=DOWNLOAD_TIMEOUT) as client:
                with client.stream("GET", url) as response:
                    response.raise_for_status()
                    with open(dest_path, "wb") as f:
                        for chunk in response.iter_bytes(chunk_size=65536):
                            f.write(chunk)
        except ImportError:
            import urllib.request

            urllib.request.urlretrieve(url, dest_path)
        except Exception as e:
            if dest_path.exists():
                dest_path.unlink(missing_ok=True)
            return {"installed": False, "error": f"Download failed: {e}"}

        # Verify checksum
        actual = _sha256(dest_path)
        if checksum and actual != checksum:
            dest_path.unlink(missing_ok=True)
            return {
                "installed": False,
                "error": (
                    f"Checksum mismatch — binary removed.\n"
                    f"  Expected : {checksum}\n"
                    f"  Got      : {actual}"
                ),
            }

        # Make executable
        dest_path.chmod(dest_path.stat().st_mode | 0o755)

        # Check if install_dir is on PATH
        path_dirs = os.environ.get("PATH", "").split(":")
        on_path = str(install_dir) in path_dirs

        path_advice = ""
        if not on_path:
            path_advice = (
                f"\n{install_dir} is not on your PATH. "
                f"Add this to your ~/.bashrc or ~/.zshrc:\n"
                f'  export PATH="$HOME/.local/bin:$PATH"'
            )

        return {
            "installed": True,
            "name": name,
            "path": str(dest_path),
            "sha256": actual,
            "checksum_verified": bool(checksum),
            "on_path": on_path,
            "message": f"{name} installed to {dest_path}." + path_advice,
        }

    preview_lines = [
        f"Install binary: {name}",
        f"  Download from : {url}",
        f"  Install to    : {dest_path}",
        "  Method        : download → chmod +x",
        f"  Reason        : {reason}",
    ]
    if checksum:
        preview_lines.append("  Verify        : SHA-256 will be checked")
    if is_appimage:
        preview_lines.append("  Type          : AppImage")

    return needs_confirmation(
        tool_name="setup_binary",
        tool_input=data,
        preview="\n".join(preview_lines),
        risk="medium",
    )


def remove_package(data: dict):
    """
    Remove an installed package from the host machine.

    Args:
        packages: Package name(s) to remove — string or list.
        manager: Package manager to use. Auto-detected if not specified.
        reason: Why this package is being removed.

    Returns:
        ConfirmationRequired if not yet confirmed (risk: high).
        Dict with removed=True and output on success.
    """
    raw_pkgs = data.get("packages") or data.get("package") or ""
    if isinstance(raw_pkgs, str):
        packages = [p.strip() for p in raw_pkgs.split(",") if p.strip()]
    else:
        packages = [str(p).strip() for p in raw_pkgs if str(p).strip()]

    if not packages:
        return {"error": "packages is required"}

    manager = (data.get("manager") or "auto").strip().lower()
    reason = (data.get("reason") or f"removing {', '.join(packages)}").strip()

    if manager in ("auto", ""):
        pm = _detect_package_manager()
        manager = pm if pm else "pip"
    elif manager == "apt":
        manager = "apt-get"

    if data.get("_confirmed"):
        if manager == "pip":
            cmd = [sys.executable, "-m", "pip", "uninstall", "-y"] + packages
        else:
            cmd = _pm_remove_cmd(manager, packages)
            if _needs_sudo(manager):
                cmd = ["sudo"] + cmd

        rc, stdout, stderr = _run(cmd, timeout=PKG_TIMEOUT)
        output = (stdout + stderr).strip()
        success = rc == 0
        return {
            "removed": success,
            "packages": packages,
            "manager": manager,
            "output": output[:4000],
        }

    needs_sudo_note = ""
    if _needs_sudo(manager):
        needs_sudo_note = "\n  (requires sudo)"

    preview_lines = [
        f"Remove packages: {', '.join(packages)}",
        f"  Method  : {_PM_NAMES.get(manager, manager)}",
        f"  Reason  : {reason}" + needs_sudo_note,
        "",
        "This will uninstall the package(s) from your system.",
    ]

    return needs_confirmation(
        tool_name="remove_package",
        tool_input=data,
        preview="\n".join(preview_lines),
        risk="high",
    )


def run_setup_script(data: dict):
    """
    Run a local setup or install script with user approval.

    Use this for programs that provide their own installer script
    (e.g. a downloaded install.sh or configure.sh).
    The script must already exist on the local filesystem — use
    download_file() first if needed.

    Args:
        script_path: Absolute path to the script to run.
        args: Optional list of arguments to pass to the script.
        interpreter: Interpreter to use: 'bash', 'sh', or 'python3'.
            Defaults to 'bash'.
        reason: Why this script is being run.

    Returns:
        ConfirmationRequired if not yet confirmed (risk: high).
        Dict with success=True and output on success.
    """
    script_path = (data.get("script_path") or "").strip()
    if not script_path:
        return {"error": "script_path is required"}

    path = Path(script_path).expanduser().resolve()
    if not path.exists():
        return {"error": f"Script not found: {path}"}

    args = data.get("args") or []
    if isinstance(args, str):
        args = shlex.split(args)

    interpreter = (data.get("interpreter") or "bash").strip().lower()
    if interpreter not in ("bash", "sh", "python3", "python"):
        return {"error": "interpreter must be 'bash', 'sh', or 'python3'"}

    reason = (data.get("reason") or f"run {path.name}").strip()

    if data.get("_confirmed"):
        interp_path = shutil.which(interpreter)
        if not interp_path:
            return {"success": False, "error": f"Interpreter '{interpreter}' not found"}

        cmd = [interp_path, str(path)] + args
        rc, stdout, stderr = _run(cmd, timeout=PKG_TIMEOUT)
        output = (stdout + stderr).strip()
        success = rc == 0
        return {
            "success": success,
            "script": str(path),
            "returncode": rc,
            "output": output[:4000],
        }

    # Read first 20 lines of the script so the user can see what it does
    preview_contents = ""
    try:
        with open(path) as f:
            lines = [f.readline() for _ in range(20)]
        preview_contents = "".join(lines).strip()
        if len(preview_contents) > 600:
            preview_contents = preview_contents[:600] + "\n  ... (truncated)"
    except Exception:
        preview_contents = "(could not preview script contents)"

    preview_lines = [
        f"Run setup script: {path.name}",
        f"  Path        : {path}",
        f"  Interpreter : {interpreter}",
        f"  Args        : {args or '(none)'}",
        f"  Reason      : {reason}",
        "",
        "Script preview (first 20 lines):",
        preview_contents,
    ]

    return needs_confirmation(
        tool_name="run_setup_script",
        tool_input=data,
        preview="\n".join(preview_lines),
        risk="high",
    )


def search_install_history(data: dict) -> dict:
    """
    Search past installation records for a package or tool.

    Returns prior successful installs with the method used, platform,
    and any troubleshooting notes.  Use before install_package to check
    if something was installed before and what worked.

    Args:
        query: Package or tool name to search for.

    Returns:
        Dict with matches (list) and count.
    """
    query = (data.get("query") or "").strip()
    if not query:
        return {"error": "query is required", "matches": [], "count": 0}

    try:
        from familiar.skills.host_setup.knowledge import search_install_knowledge

        matches = search_install_knowledge(query)
        return {
            "query": query,
            "matches": matches[:20],
            "count": len(matches),
            "message": (
                f"Found {len(matches)} prior install(s) for '{query}'."
                if matches
                else f"No prior installs found for '{query}'."
            ),
        }
    except Exception as e:
        return {"error": str(e), "matches": [], "count": 0}


# ---------------------------------------------------------------------------
# TOOLS registry
# ---------------------------------------------------------------------------

TOOLS = [
    {
        "name": "detect_system_info",
        "description": (
            "Detect the host machine's OS, architecture, package manager, "
            "available disk space, and install methods. "
            "Use this before installing anything to know which package manager "
            "to use and whether there's enough disk space. "
            "Read-only — no changes, no confirmation needed."
        ),
        "input_schema": {"type": "object", "properties": {}, "required": []},
        "handler": detect_system_info,
        "category": "host_setup",
    },
    {
        "name": "check_installed",
        "description": (
            "Check whether a program is installed on the host machine and get its version. "
            "Use before install_package to see if something is already available. "
            "Examples: check_installed('nmap'), check_installed('python3'), "
            "check_installed('docker'). "
            "Read-only — no changes, no confirmation needed."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "name": {
                    "type": "string",
                    "description": "Program name to check (e.g. 'nmap', 'git', 'docker')",
                },
                "version_flag": {
                    "type": "string",
                    "description": "Flag for version output (default '--version')",
                    "default": "--version",
                },
            },
            "required": ["name"],
        },
        "handler": check_installed,
        "category": "host_setup",
    },
    {
        "name": "list_installed",
        "description": (
            "List installed packages matching a search term. "
            "Use to see what's already installed before adding dependencies. "
            "Read-only — no changes, no confirmation needed."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "filter": {
                    "type": "string",
                    "description": "Search term to filter by (e.g. 'python', 'net')",
                },
                "manager": {
                    "type": "string",
                    "description": "Package manager (auto-detected if omitted)",
                },
            },
            "required": [],
        },
        "handler": list_installed,
        "category": "host_setup",
    },
    {
        "name": "install_package",
        "description": (
            "Install one or more packages on the host machine. "
            "Auto-detects the right package manager (dnf, apt, pip, flatpak, snap). "
            "Runs a dry-run first so you can see exactly what will be installed "
            "before anything is touched. Requires user confirmation. "
            "Examples: install nmap, install python3-requests, "
            "install spiderfoot via pip, install vlc via flatpak."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "packages": {
                    "description": "Package name(s) — string or list (e.g. 'nmap' or ['nmap', 'iperf3'])",
                },
                "manager": {
                    "type": "string",
                    "enum": ["auto", "dnf", "apt", "pip", "flatpak", "snap"],
                    "description": "Package manager to use (default: auto-detect)",
                    "default": "auto",
                },
                "reason": {
                    "type": "string",
                    "description": "Why this package is needed",
                },
            },
            "required": ["packages"],
        },
        "handler": install_package,
        "category": "host_setup",
    },
    {
        "name": "download_file",
        "description": (
            "Download a file from a URL to the host machine. "
            "Optionally verifies the download against a SHA-256 checksum before confirming. "
            "Saves to ~/Downloads/<filename> by default. "
            "Use this to get installers, configs, datasets, or any remote file. "
            "Requires user confirmation."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "url": {
                    "type": "string",
                    "description": "URL to download",
                },
                "destination": {
                    "type": "string",
                    "description": "Local path to save to (default: ~/Downloads/<filename>)",
                },
                "checksum": {
                    "type": "string",
                    "description": "Optional SHA-256 hex digest to verify the download",
                },
                "reason": {
                    "type": "string",
                    "description": "Why this file is needed",
                },
            },
            "required": ["url"],
        },
        "handler": download_file,
        "category": "host_setup",
    },
    {
        "name": "setup_binary",
        "description": (
            "Download a single-file binary or AppImage, make it executable, "
            "and install it to ~/.local/bin (or ~/Applications for AppImages). "
            "Use for tools distributed as standalone executables: "
            "rclone, restic, mkcert, spiderfoot, loki, etc. "
            "Verifies SHA-256 checksum when provided. "
            "Requires user confirmation."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "url": {
                    "type": "string",
                    "description": "URL to download the binary from",
                },
                "name": {
                    "type": "string",
                    "description": "Command name after install (e.g. 'rclone', 'spiderfoot')",
                },
                "checksum": {
                    "type": "string",
                    "description": "Optional SHA-256 hex digest for integrity verification",
                },
                "destination": {
                    "type": "string",
                    "description": "Override install directory (default: ~/.local/bin)",
                },
                "reason": {
                    "type": "string",
                    "description": "Why this binary is needed",
                },
            },
            "required": ["url", "name"],
        },
        "handler": setup_binary,
        "category": "host_setup",
    },
    {
        "name": "remove_package",
        "description": (
            "Remove an installed package from the host machine. "
            "Uses the system package manager or pip. "
            "High-risk — shows what will be removed before asking for confirmation."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "packages": {
                    "description": "Package name(s) to remove — string or list",
                },
                "manager": {
                    "type": "string",
                    "description": "Package manager (auto-detected if omitted)",
                },
                "reason": {
                    "type": "string",
                    "description": "Why this package is being removed",
                },
            },
            "required": ["packages"],
        },
        "handler": remove_package,
        "category": "host_setup",
    },
    {
        "name": "run_setup_script",
        "description": (
            "Run a local installer or setup script with user approval. "
            "Shows the first 20 lines of the script in the confirmation so "
            "the user knows exactly what it will do. "
            "Use after download_file() to run a downloaded installer. "
            "High-risk — requires explicit user confirmation."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "script_path": {
                    "type": "string",
                    "description": "Absolute path to the script to run",
                },
                "args": {
                    "description": "Arguments to pass to the script (string or list)",
                },
                "interpreter": {
                    "type": "string",
                    "enum": ["bash", "sh", "python3"],
                    "description": "Interpreter to use (default: bash)",
                    "default": "bash",
                },
                "reason": {
                    "type": "string",
                    "description": "Why this script is being run",
                },
            },
            "required": ["script_path"],
        },
        "handler": run_setup_script,
        "category": "host_setup",
    },
    {
        "name": "search_install_history",
        "description": (
            "Search past installation records for a package or tool. "
            "Returns prior successful installs with the method used, platform, "
            "and any troubleshooting notes. Use before install_package to check "
            "if something was installed before and what approach worked."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "Package or tool name to search for",
                },
            },
            "required": ["query"],
        },
        "handler": search_install_history,
        "category": "host_setup",
    },
]
