#!/usr/bin/env python3
"""
geniesim_install.py — install AgibotTech/genie_sim (github.com/AgibotTech/genie_sim),
AgiBot's Isaac Sim/Omniverse-based humanoid simulation platform, following the
exact pattern of hyworld_install.py (disk-safe pip env, capabilities manifest,
JSON sentinel result) and reusing its helpers directly.

genie_sim is NOT like hyworld/lingbot: its heavy runtime (Isaac Sim, PhysX/
Newton, ROS 2 Jazzy) lives entirely inside a Docker container built and run
by genie_sim's own `geniesim` CLI — it hard-requires Linux + Docker + an
NVIDIA RTX-class GPU + Isaac Sim 5.1/6.0, none of which this script can
provide. So this installer only ever:
  1. clones the repo
  2. creates a LIGHTWEIGHT venv and pip-installs genie_sim's own CLI dispatcher
     (source/geniesim_cli/) — not the Isaac Sim stack itself
  3. detects (never installs) system-level prerequisites: git, Docker, the
     NVIDIA Container Toolkit, platform/WSL2, GPU/VRAM, disk space

Actually building/running the Docker image, and prompting to install missing
system dependencies, are handled by the JS layer (cli/geniesim.mjs +
cli/geniesimDeps.mjs) — installing Docker/WSL2/the NVIDIA Container Toolkit
modifies host OS state and needs a live, interactive confirmation, which
belongs in the user-facing CLI, not a stdlib-only background script.

Honest capability notes recorded in the manifest:
  - docker_present / docker_running: distinguishes "not installed" from
    "installed but the daemon isn't running" (only `docker info` can tell).
  - nvidia_container_toolkit: Linux-only; required for GPU passthrough into
    the genie_sim container.
  - wsl2_present: Windows-only, best-effort; "unknown" (not False) when the
    probe itself is inconclusive rather than claiming a false negative.
  - geniesim_cli_installed: whether `pip install -e source/geniesim_cli/`
    actually succeeded — this is the one piece of genie_sim that CAN run on
    any OS/Python, since it's just the CLI dispatcher, not Isaac Sim itself.
"""
from __future__ import annotations

import argparse
import json
import pathlib
import platform
import shutil
import subprocess
import sys

# Sibling-module reuse: when run as a script, sys.path[0] is this directory.
import hyworld_install as base

log = base.log

REPO_URL = "https://github.com/AgibotTech/genie_sim"

# Isaac Sim's own container images run tens of GB (Omniverse + PhysX/Newton +
# ROS 2 Jazzy) — this is NOT a figure published anywhere in genie_sim's docs,
# just a clearly-labeled rough estimate so the disk preflight has something
# to compare against. Verify against `docker images` after a real
# `geniesim docker build` on a capable host.
ISAAC_SIM_IMAGE_ESTIMATE_GB = 40


def _check_docker() -> dict:
    """`docker info` is the one check that distinguishes "Docker isn't
    installed" from "Docker is installed but the daemon isn't running" —
    `docker --version` alone can't tell those apart."""
    if not shutil.which("docker"):
        return {"present": False, "running": False}
    try:
        r = subprocess.run(["docker", "info"], capture_output=True, text=True, timeout=10)
        return {"present": True, "running": r.returncode == 0}
    except (subprocess.TimeoutExpired, OSError) as e:
        log.warning("docker present but `docker info` failed: %s", e)
        return {"present": True, "running": False}


def _check_nvidia_container_toolkit() -> bool:
    """Linux-only in practice (Windows/macOS Docker Desktop don't use this
    path for GPU passthrough) — probing unconditionally is harmless since
    `nvidia-ctk` simply won't exist elsewhere."""
    try:
        r = subprocess.run(["nvidia-ctk", "--version"], capture_output=True, text=True, timeout=5)
        return r.returncode == 0
    except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
        return False


def _repair_windows_symlink_placeholder(path: pathlib.Path) -> bool:
    """genie_sim's `source/geniesim_cli/VERSION` is a symlink to the
    repo-root `VERSION` file (its own pyproject.toml: "Single source of
    truth: the repo-root VERSION file"). Git on Windows, without symlink
    support explicitly enabled (`core.symlinks=true`, which needs Developer
    Mode or an elevated checkout), checks a symlink out as a plain-text file
    CONTAINING the link's target path instead of resolving it — e.g.
    `../../VERSION` as literal 13 bytes of text. setuptools then reads that
    text as the version string; its `/`-to-`-` normalization turns it into
    the exact `..-..-VERSION` `InvalidVersion` failure this function exists
    to catch. This is not a genie_sim defect — it's a Windows git-checkout
    limitation for any repo using symlinks — so the fix is local and
    install-time-only: read the real target's content and overwrite the
    placeholder, never touching genie_sim's tracked history. No-op (returns
    False) on Linux/macOS, where a real symlink already resolves
    transparently and this content check never matches.
    """
    if not path.exists() or not path.is_file():
        return False
    try:
        raw = path.read_text(encoding="utf-8")
    except (UnicodeDecodeError, OSError):
        return False
    content = raw.strip()
    # A resolved version string won't contain path separators; an unresolved
    # symlink placeholder is exactly the relative target path git wrote out.
    if "\n" in raw or not content or ("/" not in content and "\\" not in content):
        return False
    target = (path.parent / content).resolve()
    if not target.is_file() or target == path.resolve():
        return False
    try:
        path.write_text(target.read_text(encoding="utf-8"), encoding="utf-8")
        log.info("repaired Windows symlink placeholder: %s (was %r, now the real content of %s)", path, content, target)
        return True
    except OSError as e:
        log.warning("found an unresolved symlink placeholder at %s but could not repair it: %s", path, e)
        return False


def _check_platform() -> dict:
    """OS + (Windows-only) best-effort WSL2 presence. `wsl --status` fails
    when the WSL feature isn't enabled even though `wsl.exe` itself ships
    built into modern Windows — a real, if imperfect, signal. When `wsl`
    isn't even on PATH (unusual, but possible on stripped-down/Server
    installs) the honest answer is "unknown", not a false "not present"."""
    system = platform.system().lower()  # "windows" | "linux" | "darwin"
    info = {"system": system}
    if system == "windows":
        if not shutil.which("wsl"):
            info["wsl2_present"] = "unknown"
        else:
            try:
                r = subprocess.run(["wsl", "--status"], capture_output=True, text=True, timeout=10)
                info["wsl2_present"] = r.returncode == 0
            except (subprocess.TimeoutExpired, OSError):
                info["wsl2_present"] = "unknown"
    return info


def main():
    ap = argparse.ArgumentParser(
        description="Install AgibotTech/genie_sim's CLI dispatcher into an isolated venv "
                    "(the Isaac Sim/Docker runtime itself is handled separately, on a capable host)"
    )
    ap.add_argument("--path", default=str(pathlib.Path.home() / ".zelpi" / "genie_sim"))
    args = ap.parse_args()

    import logging
    logging.basicConfig(level=logging.INFO, format="%(asctime)s  %(name)s  %(levelname)s  %(message)s", datefmt="%H:%M:%S")

    install_path = pathlib.Path(args.path).expanduser()
    manifest_path = install_path / ".zelpi_capabilities.json"
    capabilities = {
        "git": shutil.which("git") is not None,
        "docker_present": False,
        "docker_running": False,
        "nvidia_container_toolkit": False,
        "nvidia_gpu": False,
        "gpu_vram_gb": None,
        "platform": None,
        "wsl2_present": None,
        "geniesim_cli_installed": False,
        "windows_symlink_repaired": False,
        "docker_image_built": False,
        "python": None,
        "path": str(install_path),
    }
    if manifest_path.exists():
        try:
            capabilities.update(json.loads(manifest_path.read_text(encoding="utf-8")))
        except Exception:
            pass
    result = {"code": "success", "capabilities": capabilities}

    log.info("─── PREFLIGHT ───")
    base._check_disk_space(install_path, required_gb=ISAAC_SIM_IMAGE_ESTIMATE_GB)
    vram = base._check_gpu_vram()
    capabilities["nvidia_gpu"] = vram is not None
    capabilities["gpu_vram_gb"] = vram

    platform_info = _check_platform()
    capabilities["platform"] = platform_info["system"]
    capabilities["wsl2_present"] = platform_info.get("wsl2_present")

    docker = _check_docker()
    capabilities["docker_present"] = docker["present"]
    capabilities["docker_running"] = docker["running"]
    log.info("Docker: present=%s running=%s", docker["present"], docker["running"])

    capabilities["nvidia_container_toolkit"] = _check_nvidia_container_toolkit()
    log.info("NVIDIA Container Toolkit: %s", capabilities["nvidia_container_toolkit"])

    log.info("─── CLONE/PULL REPO ───")
    if not base._clone_or_pull(REPO_URL, install_path):
        result["code"] = "clone_failed"
        print("GENIESIM_INSTALL_RESULT " + json.dumps(result))
        sys.exit(1)

    # See _repair_windows_symlink_placeholder()'s docstring: source/geniesim_cli/VERSION
    # is a symlink to the repo-root VERSION file, which Windows git checkouts
    # (without symlink support) turn into a broken plain-text placeholder that
    # makes the editable install below fail with an InvalidVersion error.
    capabilities["windows_symlink_repaired"] = _repair_windows_symlink_placeholder(
        install_path / "source" / "geniesim_cli" / "VERSION"
    )

    log.info("─── CREATE LIGHTWEIGHT VENV (CLI dispatcher only) ───")
    if not base._create_venv(install_path):
        result["code"] = "venv_failed"
        print("GENIESIM_INSTALL_RESULT " + json.dumps(result))
        sys.exit(1)

    venv_python = base._get_venv_python(install_path)
    capabilities["python"] = str(venv_python)
    pip_env = base._pip_env(install_path)

    log.info("─── UPGRADE PIP/SETUPTOOLS ───")
    subprocess.run([str(venv_python), "-m", "pip", "install", "--upgrade", "pip", "setuptools", "wheel"], env=pip_env, check=False)

    # Only the CLI dispatcher — NOT the Isaac Sim stack, which lives entirely
    # in Docker and is never installed into this venv. genie_sim isn't on
    # PyPI; its own docs require an editable install from the cloned repo.
    log.info("─── INSTALL geniesim_cli (editable) ───")
    cli_path = install_path / "source" / "geniesim_cli"
    if base._pip_install_editable(venv_python, cli_path, pip_env):
        capabilities["geniesim_cli_installed"] = True
        log.info("✓ geniesim_cli installed")
    else:
        capabilities["geniesim_cli_installed"] = False
        log.warning("✗ geniesim_cli install failed — `geniesim` CLI subcommands won't be available; "
                    "clone + capability detection still succeeded, so this isn't a fatal error.")

    manifest_path.write_text(json.dumps(capabilities, indent=2), encoding="utf-8")
    log.info("─── DONE ───")
    log.info(
        "docker=%s/%s  nvidia_gpu=%s  nvidia_container_toolkit=%s  geniesim_cli=%s",
        capabilities["docker_present"], capabilities["docker_running"],
        capabilities["nvidia_gpu"], capabilities["nvidia_container_toolkit"],
        capabilities["geniesim_cli_installed"],
    )
    result["capabilities"] = capabilities
    print("GENIESIM_INSTALL_RESULT " + json.dumps(result))


if __name__ == "__main__":
    main()
