# tools/docker_binaries.py - Download Docker CLI, Colima, and Lima as pre-built binaries.
# Uses only curl and tar (available on all macOS). No Homebrew, no CLTs.
from __future__ import annotations

import os
import platform
import stat
import subprocess
import sys

MUGI_DIR = os.path.join(os.path.expanduser("~"), ".mugi")
MUGI_BIN = os.path.join(MUGI_DIR, "bin")

# Apple Silicon only — Mugi does not ship or download x86_64 binaries.
_ARCH = "arm64"
# Docker static mac builds label arm64 as aarch64 on their CDN.
_DOCKER_ARCH = "aarch64"


def require_apple_silicon() -> tuple[bool, str]:
    """Fail closed before downloading Colima/Docker/Lima on unsupported hardware."""
    if platform.machine() != "arm64":
        return False, "Local Docker/SearXNG setup requires an Apple Silicon Mac (M-series)."
    return True, ""

# Pinned versions
# Docker CLI v24 exposes API 1.43; newer daemons can require >=1.44.
DOCKER_VERSION = "25.0.5"
COLIMA_VERSION = "0.10.1"
LIMA_VERSION = "2.0.3"
MIN_DOCKER_API_VERSION = "1.44"

DOCKER_URL = f"https://download.docker.com/mac/static/stable/{_DOCKER_ARCH}/docker-{DOCKER_VERSION}.tgz"
COLIMA_URL = f"https://github.com/abiosoft/colima/releases/download/v{COLIMA_VERSION}/colima-Darwin-{_ARCH}"
LIMA_URL = f"https://github.com/lima-vm/lima/releases/download/v{LIMA_VERSION}/lima-{LIMA_VERSION}-Darwin-{_ARCH}.tar.gz"


def _ensure_mugi_bin() -> None:
    os.makedirs(MUGI_BIN, exist_ok=True)


def _curl_download(url: str, dest: str, timeout: int = 300) -> tuple[bool, str]:
    """Download url to dest using curl. Returns (success, message)."""
    try:
        os.makedirs(os.path.dirname(dest), exist_ok=True)
        r = subprocess.run(
            ["/usr/bin/curl", "-fSL", "--retry", "3", "-o", dest, url],
            capture_output=True, text=True, timeout=timeout,
        )
        if r.returncode != 0:
            return False, (r.stderr or r.stdout or f"curl exit {r.returncode}").strip()
        if not os.path.isfile(dest) or os.path.getsize(dest) < 1024:
            return False, f"Download seems empty or missing: {dest}"
        return True, "OK"
    except subprocess.TimeoutExpired:
        return False, f"Download timed out after {timeout}s."
    except Exception as e:
        return False, str(e)


def _make_executable(path: str) -> None:
    try:
        st = os.stat(path)
        os.chmod(path, st.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
    except OSError as exc:
        raise OSError(f"Could not mark {path} executable: {exc}") from exc
    # Ad-hoc sign downloaded CLI binaries so Gatekeeper does not block first run.
    codesign = "/usr/bin/codesign"
    if os.path.isfile(codesign):
        try:
            subprocess.run(
                [codesign, "-s", "-", "--force", "--timestamp=none", path],
                capture_output=True,
                text=True,
                timeout=30,
                check=False,
            )
        except Exception:
            pass


def docker_cli_path() -> str | None:
    p = os.path.join(MUGI_BIN, "docker")
    return p if os.path.isfile(p) else None


def _parse_version_parts(ver: str) -> tuple[int, ...]:
    try:
        return tuple(int(x) for x in str(ver).strip().split("."))
    except Exception:
        return ()


def _version_lt(a: str, b: str) -> bool:
    a_parts = _parse_version_parts(a)
    b_parts = _parse_version_parts(b)
    if not a_parts or not b_parts:
        return False
    n = max(len(a_parts), len(b_parts))
    aa = a_parts + (0,) * (n - len(a_parts))
    bb = b_parts + (0,) * (n - len(b_parts))
    return aa < bb


def _docker_client_api_version(docker_path: str, timeout: int = 10) -> str:
    try:
        r = subprocess.run(
            [docker_path, "version", "--format", "{{.Client.APIVersion}}"],
            capture_output=True, text=True, timeout=timeout,
        )
        if r.returncode != 0:
            return ""
        return (r.stdout or "").strip()
    except Exception:
        return ""


def colima_path() -> str | None:
    p = os.path.join(MUGI_BIN, "colima")
    return p if os.path.isfile(p) else None


def limactl_path() -> str | None:
    p = os.path.join(MUGI_BIN, "limactl")
    return p if os.path.isfile(p) else None


def ensure_docker_cli(force_reinstall: bool = False, min_api_version: str = MIN_DOCKER_API_VERSION) -> tuple[bool, str]:
    """Download Docker CLI if not already present in ~/.mugi/bin/."""
    existing = docker_cli_path()
    if existing and not force_reinstall:
        api_ver = _docker_client_api_version(existing)
        if api_ver and _version_lt(api_ver, min_api_version):
            force_reinstall = True
        else:
            return True, "Docker CLI already installed."
    if existing and force_reinstall:
        try:
            os.remove(existing)
        except OSError:
            pass
    _ensure_mugi_bin()
    tmp = os.path.join(MUGI_DIR, "tmp", "docker-cli.tgz")
    os.makedirs(os.path.dirname(tmp), exist_ok=True)
    print(f"[mugi] Downloading Docker CLI v{DOCKER_VERSION}...", file=sys.stderr, flush=True)
    ok, msg = _curl_download(DOCKER_URL, tmp, timeout=120)
    if not ok:
        return False, f"Docker CLI download failed: {msg}"
    try:
        r = subprocess.run(
            ["/usr/bin/tar", "xzf", tmp, "-C", MUGI_BIN, "--strip-components=1", "docker/docker"],
            capture_output=True, text=True, timeout=60,
        )
        if r.returncode != 0:
            return False, f"Extracting Docker CLI failed: {(r.stderr or r.stdout or '').strip()}"
    except Exception as e:
        return False, f"Extracting Docker CLI failed: {e}"
    finally:
        try:
            os.remove(tmp)
        except OSError:
            pass
    target = os.path.join(MUGI_BIN, "docker")
    if not os.path.isfile(target):
        return False, "Docker binary not found after extraction."
    _make_executable(target)
    return True, f"Docker CLI v{DOCKER_VERSION} installed."


def ensure_colima() -> tuple[bool, str]:
    """Download Colima binary if not already present in ~/.mugi/bin/."""
    if colima_path():
        return True, "Colima already installed."
    _ensure_mugi_bin()
    target = os.path.join(MUGI_BIN, "colima")
    print(f"[mugi] Downloading Colima v{COLIMA_VERSION}...", file=sys.stderr, flush=True)
    ok, msg = _curl_download(COLIMA_URL, target, timeout=120)
    if not ok:
        return False, f"Colima download failed: {msg}"
    _make_executable(target)
    return True, f"Colima v{COLIMA_VERSION} installed."


def ensure_lima() -> tuple[bool, str]:
    """Download and extract Lima if limactl is not already present in ~/.mugi/bin/."""
    if limactl_path():
        return True, "Lima already installed."
    _ensure_mugi_bin()
    # Clean up any partial previous install
    for name in ("limactl", "lima", "lima-guestagent"):
        p = os.path.join(MUGI_BIN, name)
        if os.path.isfile(p):
            os.remove(p)
    tmp = os.path.join(MUGI_DIR, "tmp", "lima.tar.gz")
    os.makedirs(os.path.dirname(tmp), exist_ok=True)
    print(f"[mugi] Downloading Lima v{LIMA_VERSION}...", file=sys.stderr, flush=True)
    ok, msg = _curl_download(LIMA_URL, tmp, timeout=180)
    if not ok:
        return False, f"Lima download failed: {msg}"
    # Lima tarball has bin/, share/, and libexec/ at the top level.
    # Extract directly into ~/.mugi/ so paths resolve correctly
    # (limactl at ~/.mugi/bin/limactl finds share at ~/.mugi/share/lima/).
    try:
        r = subprocess.run(
            ["/usr/bin/tar", "xzf", tmp, "-C", MUGI_DIR],
            capture_output=True, text=True, timeout=120,
        )
        if r.returncode != 0:
            return False, f"Extracting Lima failed: {(r.stderr or r.stdout or '').strip()}"
        # Ensure binaries are executable
        for name in ("limactl", "lima"):
            p = os.path.join(MUGI_BIN, name)
            if os.path.isfile(p):
                _make_executable(p)
    except Exception as e:
        return False, f"Extracting Lima failed: {e}"
    finally:
        try:
            os.remove(tmp)
        except OSError:
            pass
    if not os.path.isfile(os.path.join(MUGI_BIN, "limactl")):
        return False, "limactl not found after extraction."
    return True, f"Lima v{LIMA_VERSION} installed."


def ensure_all_binaries() -> tuple[bool, str]:
    """Ensure docker CLI, colima, and lima are all present. Returns (success, message)."""
    ok, msg = require_apple_silicon()
    if not ok:
        return False, msg
    results = []
    for name, fn in [("Lima", ensure_lima), ("Docker CLI", ensure_docker_cli), ("Colima", ensure_colima)]:
        ok, msg = fn()
        if not ok:
            return False, f"{name} installation failed: {msg}"
        results.append(msg)
    return True, " | ".join(results)
