#!/usr/bin/env bash
# GridShare Node Agent — Linux Installer
# Works on Ubuntu 20.04+, Debian 11+, and most NVIDIA gaming rigs
#
# One-liner install:
#   curl -fsSL https://gridshare.in/dl/install.sh | sudo bash
#
# Or download and run:
#   wget https://gridshare.in/dl/install.sh && chmod +x install.sh && sudo bash install.sh

set -euo pipefail

# Account-first install: set GRIDSHARE_TOKEN (a gsp_… provision token minted in the
# portal) to install with ZERO prompts. Central reads name/UPI/city off the provider
# record saved at signup and returns the node identity + long-lived API key.
# Fleet mode: set GRIDSHARE_KEY to reuse an existing provider key across machines.
# Priority: TOKEN > KEY > interactive prompts.
GRIDSHARE_TOKEN="${GRIDSHARE_TOKEN:-}"
GRIDSHARE_KEY="${GRIDSHARE_KEY:-}"
GRIDSHARE_LABEL="${GRIDSHARE_LABEL:-$(hostname -s 2>/dev/null || echo node)}"

# Non-interactive mode — set EARLY (the tty/clear guards below depend on it).
# Triggered by the Windows/WSL installer + fleet deploys via env vars, AND by a
# provision token (the token path asks ZERO questions).
NONINTERACTIVE=false
[[ -n "${GS_NONINTERACTIVE:-}" || -n "${GS_OWNER_NAME:-}" || -n "$GRIDSHARE_TOKEN" ]] && NONINTERACTIVE=true

# Reconnect stdin to the terminal for interactive curl|bash so prompts work — but
# NEVER when non-interactive: `exec < /dev/tty` there steals stdin from the
# curl|bash pipe and aborts the download mid-stream (the curl error 23 we hit in WSL).
if [[ "$NONINTERACTIVE" != true ]] && [[ ! -t 0 ]] && [[ -r /dev/tty ]]; then exec < /dev/tty; fi

CENTRAL_URL="${GS_CENTRAL_URL:-https://relay.gridshare.in}"
WHEEL_NAME="gridshare_node-1.3.64-py3-none-any.whl"
INSTALL_DIR="/opt/gridshare-node"
DATA_DIR="/var/lib/gridshare"
LOG_DIR="/var/log/gridshare"
CONFIG_FILE="$DATA_DIR/config.json"
SERVICE_NAME="gridshare-node"
VENV_DIR="$INSTALL_DIR/venv"

# ── Colours ───────────────────────────────────────────────────────────────────

_red()    { echo -e "\033[31m$*\033[0m"; }
_green()  { echo -e "\033[32m$*\033[0m"; }
_yellow() { echo -e "\033[33m$*\033[0m"; }
_cyan()   { echo -e "\033[36m$*\033[0m"; }
_gray()   { echo -e "\033[90m$*\033[0m"; }

step()  { echo; _cyan "── $*"; }
ok()    { _green "  [✓] $*"; }
info()  { _gray  "      $*"; }
warn()  { _yellow "  [!] $*"; }
fail()  { _red "  [✗] $*"; echo; exit 1; }

ask() {
    local prompt="$1" default="${2:-}"
    local answer
    if [[ -n "$default" ]]; then
        read -rp "      $prompt [$default]: " answer
        echo "${answer:-$default}"
    else
        read -rp "      $prompt: " answer
        echo "$answer"
    fi
}

# ── Header ────────────────────────────────────────────────────────────────────

if [[ "$NONINTERACTIVE" != true ]]; then clear 2>/dev/null || true; fi
echo
_cyan "  ╔══════════════════════════════════════════════════════════════╗"
_cyan "  ║        GridShare — GPU Node Agent Installer                 ║"
_cyan "  ║        Earn INR from your idle GPU  •  gridshare.in         ║"
_cyan "  ╚══════════════════════════════════════════════════════════════╝"
echo
echo "  Welcome! This installer registers your GPU with India's compute"
echo "  marketplace so you can earn INR from your idle PC."
echo
_gray "  Relay server : $CENTRAL_URL"
echo

# ── Root check ────────────────────────────────────────────────────────────────

if [[ $EUID -ne 0 ]]; then
    fail "Please run as root: sudo bash install.sh"
fi

# ── OS detection ──────────────────────────────────────────────────────────────

step "Checking OS"
if [[ -f /etc/os-release ]]; then
    source /etc/os-release
    ok "OS: $PRETTY_NAME"
else
    warn "Could not detect OS — proceeding anyway"
fi

# ── GPU detection ─────────────────────────────────────────────────────────────

step "Detecting GPU"

GPU_NAMES=()
GPU_VRAMS=()
GPU_VENDOR="cpu"
HAS_NVIDIA=false
HAS_AMD=false

# NVIDIA detection via nvidia-smi
if command -v nvidia-smi &>/dev/null; then
    HAS_NVIDIA=true
    GPU_VENDOR="nvidia"
    while IFS=',' read -r name vram power; do
        name=$(echo "$name" | xargs)
        vram_gb=$(echo "scale=1; ${vram:-0}/1024" | bc 2>/dev/null || echo "0")
        GPU_NAMES+=("$name")
        GPU_VRAMS+=("$vram_gb")
        ok "NVIDIA GPU: $name (${vram_gb} GB VRAM)"
    done < <(nvidia-smi --query-gpu=name,memory.total,power.limit --format=csv,noheader,nounits 2>/dev/null || true)
fi

# AMD detection via rocm-smi (ROCm) or /sys/class/drm
if [[ ${#GPU_NAMES[@]} -eq 0 ]]; then
    if command -v rocm-smi &>/dev/null; then
        HAS_AMD=true
        GPU_VENDOR="amd"
        while IFS=',' read -r name vram; do
            name=$(echo "$name" | xargs)
            [[ -n "$name" ]] && GPU_NAMES+=("$name") && GPU_VRAMS+=("0") && ok "AMD GPU: $name (ROCm)"
        done < <(rocm-smi --showproductname 2>/dev/null | grep -v "^=\|GPU\[" | grep -v "^$" || true)
    elif ls /sys/class/drm/card*/device/vendor 2>/dev/null | xargs grep -l "0x1002" &>/dev/null; then
        # AMD vendor ID 0x1002
        HAS_AMD=true
        GPU_VENDOR="amd"
        GPU_NAME=$(cat /sys/class/drm/card0/device/product_name 2>/dev/null || echo "AMD Radeon GPU")
        GPU_NAMES+=("$GPU_NAME")
        GPU_VRAMS+=("0")
        warn "AMD GPU detected: $GPU_NAME (ROCm not installed — limited ML support)"
        warn "Install ROCm for best earnings: https://rocm.docs.amd.com/en/latest/deploy/linux/"
    fi
fi

if [[ ${#GPU_NAMES[@]} -eq 0 ]]; then
    warn "No GPU detected — will register as CPU-only node (lower earnings)"
    warn "Supported GPUs: NVIDIA (driver + CUDA), AMD (ROCm)"
    echo
    if [[ "$NONINTERACTIVE" == true ]]; then
        info "Non-interactive: continuing in CPU-only mode"
    else
        read -rp "      Continue in CPU-only mode? [Y/n]: " cont
        [[ "${cont:-y}" =~ ^[Yy] ]] || exit 0
    fi
fi

# Detect if this is a desktop/interactive machine (has display) vs headless server
IS_DESKTOP=false
if [[ -n "${DISPLAY:-}" ]] || [[ -n "${WAYLAND_DISPLAY:-}" ]] || command -v Xorg &>/dev/null; then
    IS_DESKTOP=true
    info "Desktop environment detected — auto-pause will be enabled (pauses when you use the PC)"
else
    info "Headless/server mode detected — auto-pause disabled"
fi

# CPU and RAM info
CPU_MODEL=$(grep "model name" /proc/cpuinfo | head -1 | cut -d: -f2 | xargs 2>/dev/null || echo "Unknown")
CPU_CORES=$(nproc 2>/dev/null || echo 0)
RAM_GB=$(awk '/MemTotal/{printf "%.1f", $2/1024/1024}' /proc/meminfo 2>/dev/null || echo 0)
ok "CPU: $CPU_MODEL ($CPU_CORES cores)"
ok "RAM: $RAM_GB GB"

# ── Dependencies ──────────────────────────────────────────────────────────────

step "Installing system dependencies"

if command -v apt-get &>/dev/null; then
    apt-get update -qq
    DEBIAN_FRONTEND=noninteractive apt-get install -y -qq python3 python3-pip python3-venv curl wget bc fuse3 unzip ca-certificates gnupg 2>/dev/null
    ok "apt packages installed"
elif command -v yum &>/dev/null; then
    yum install -y -q python3 python3-pip curl wget bc fuse3 unzip ca-certificates gnupg2 2>/dev/null
    ok "yum packages installed"
elif command -v dnf &>/dev/null; then
    dnf install -y -q python3 python3-pip curl wget bc fuse3 unzip ca-certificates gnupg2 2>/dev/null
    ok "dnf packages installed"
else
    warn "Unknown package manager — assuming python3 is already installed"
fi

# ── rclone — required for Network Volume mounting ────────────────────────────
# GridShare Network Volumes are backed by DigitalOcean Spaces (S3-compatible).
# The node agent uses rclone to mount volumes inside containers via FUSE.
# Without rclone, POST /buyers/volumes/ + volume_ids=[] at launch won't work.
step "Installing rclone (Network Volume support)"
if ! command -v rclone &>/dev/null; then
    info "Downloading rclone..."
    RCLONE_ARCH="amd64"
    case "$(uname -m)" in arm64|aarch64) RCLONE_ARCH="arm64" ;; esac
    curl -fsSL "https://downloads.rclone.org/rclone-current-linux-${RCLONE_ARCH}.zip" \
        -o /tmp/rclone.zip 2>/dev/null && \
    unzip -q /tmp/rclone.zip -d /tmp/rclone-extract 2>/dev/null && \
    cp /tmp/rclone-extract/rclone-*/rclone /usr/local/bin/rclone && \
    chmod +x /usr/local/bin/rclone && \
    rm -rf /tmp/rclone.zip /tmp/rclone-extract && \
    ok "rclone installed: $(rclone --version 2>/dev/null | head -1)" || \
    warn "rclone install failed — Network Volumes won't mount. Install manually: https://rclone.org/install/"
else
    ok "rclone already installed: $(rclone --version 2>/dev/null | head -1)"
fi

# ── Docker + GPU container runtime ──────────────────────────────────────────
# The node runs buyer workloads in Docker containers (docker_mgr.py). Without
# Docker the node registers but CANNOT run a single job. With an NVIDIA GPU it
# also needs the NVIDIA Container Toolkit so `docker run --gpus all` reaches the
# GPU. This step installs both and verifies GPU passthrough.
step "Installing Docker + GPU container runtime"

# WSL2 detection — in WSL2 Docker often has no systemd to start it.
IS_WSL2=false
if grep -qiE "microsoft|wsl" /proc/version 2>/dev/null; then IS_WSL2=true; info "WSL2 environment detected"; fi
HAS_SYSTEMD=false
[[ -d /run/systemd/system ]] && HAS_SYSTEMD=true

start_docker() {
    if [[ "$HAS_SYSTEMD" == true ]]; then
        systemctl enable docker >/dev/null 2>&1 || true
        systemctl start  docker >/dev/null 2>&1 || true
    elif ! docker info >/dev/null 2>&1; then
        # WSL2 / no-systemd: bring the daemon up directly
        service docker start >/dev/null 2>&1 || (nohup dockerd >"$LOG_DIR/dockerd.log" 2>&1 &) || true
        sleep 4
    fi
}

if ! command -v docker &>/dev/null; then
    info "Installing Docker (official apt repo) ..."
    # NOTE: get.docker.com detects WSL and often SKIPS the engine (recommends Docker
    # Desktop) — that's why WSL2 nodes ended up with no docker. Install via apt repo.
    if command -v apt-get &>/dev/null; then
        install -m 0755 -d /etc/apt/keyrings 2>/dev/null || true
        curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc 2>/dev/null || true
        chmod a+r /etc/apt/keyrings/docker.asc 2>/dev/null || true
        . /etc/os-release 2>/dev/null || true
        # Docker only publishes apt repos for specific Ubuntu codenames. Bleeding-edge /
        # non-LTS releases (e.g. 26.04 "resolute" that WSL2 now ships by default) have NO
        # Docker repo, so the apt path 404s and we fall through to get.docker.com — which
        # refuses on WSL. Map anything Docker doesn't publish to the newest LTS it does.
        DOCKER_CODENAME="${VERSION_CODENAME:-noble}"
        case "$DOCKER_CODENAME" in
            focal|jammy|noble) : ;;
            *) info "Ubuntu '$DOCKER_CODENAME' has no Docker repo — using 'noble' (24.04 LTS) packages"; DOCKER_CODENAME="noble" ;;
        esac
        echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu ${DOCKER_CODENAME} stable" \
            > /etc/apt/sources.list.d/docker.list 2>/dev/null || true
        apt-get update -qq 2>/dev/null || true
        DEBIAN_FRONTEND=noninteractive apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-buildx-plugin 2>/dev/null || true
    fi
    # Robust fallback: Ubuntu's OWN docker.io package, built for the running release
    # (incl. bleeding-edge like 26.04). The Docker CE repo we just added carries the wrong
    # codename for this release, which makes `apt-get update` fail and leaves docker.io
    # with "no installation candidate" (the whole update is poisoned). So REMOVE that repo,
    # refresh apt cleanly, then install docker.io (runs GPU containers fine w/ the toolkit).
    if ! command -v docker &>/dev/null; then
        info "Docker CE repo didn't yield a binary — cleaning apt + installing Ubuntu's docker.io ..."
        rm -f /etc/apt/sources.list.d/docker.list 2>/dev/null || true
        apt-get update -qq 2>/dev/null || true
        DEBIAN_FRONTEND=noninteractive apt-get install -y -qq docker.io 2>/dev/null || true
    fi
    # Bulletproof fallback: Docker's STATIC binaries — no apt repo / codename / universe
    # dependency, so they install on ANY x86_64 Linux incl. bleeding-edge WSL2 Ubuntu
    # (26.04 "resolute" ships neither a Docker CE repo nor docker.io). Grab the newest
    # stable tarball, drop the engine into /usr/local/bin; start_docker brings up dockerd
    # (dockerd supervises its own bundled containerd).
    if ! command -v docker &>/dev/null; then
        info "Installing Docker static binaries (distro-independent) ..."
        _dbase="https://download.docker.com/linux/static/stable/$(uname -m)"
        _dver=$(curl -fsSL "$_dbase/" 2>/dev/null | grep -oE 'docker-[0-9]+\.[0-9]+\.[0-9]+\.tgz' | sort -V | tail -1)
        if [ -n "$_dver" ] && curl -fsSL "$_dbase/$_dver" -o /tmp/docker-static.tgz 2>/dev/null; then
            tar -xzf /tmp/docker-static.tgz -C /tmp 2>/dev/null && install -m0755 /tmp/docker/* /usr/local/bin/ 2>/dev/null || true
            rm -rf /tmp/docker /tmp/docker-static.tgz 2>/dev/null || true
            # Static binaries ship no service unit. On systemd-WSL2, start_docker calls
            # `systemctl start docker`, which needs a unit — create one so dockerd is
            # managed + auto-restarts. (No-systemd WSL2 uses start_docker's nohup path.)
            if command -v docker &>/dev/null && [[ "$HAS_SYSTEMD" == true ]] && [ ! -e /etc/systemd/system/docker.service ] && [ ! -e /lib/systemd/system/docker.service ]; then
                cat > /etc/systemd/system/docker.service <<'DOCKERUNIT'
[Unit]
Description=Docker Application Container Engine (static)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/local/bin/dockerd
Restart=always
RestartSec=3
LimitNOFILE=infinity
[Install]
WantedBy=multi-user.target
DOCKERUNIT
                systemctl daemon-reload 2>/dev/null || true
            fi
            command -v docker &>/dev/null && info "Docker static binaries installed: $(docker --version 2>/dev/null)"
        else
            warn "Could not fetch Docker static binaries from $_dbase"
        fi
    fi
    # Last-resort fallback to the convenience script if apt didn't produce a binary —
    # but ONLY off-WSL: get.docker.com detects WSL and refuses (recommends Docker Desktop),
    # which is exactly the dead end that left WSL2 nodes with no Docker.
    if ! command -v docker &>/dev/null && [[ "$IS_WSL2" != true ]]; then
        curl -fsSL https://get.docker.com 2>/dev/null | sh 2>/dev/null || true
    fi
    if command -v docker &>/dev/null; then
        ok "Docker installed: $(docker --version 2>/dev/null)"
    else
        warn "Docker install failed on both the CE repo and docker.io — real apt error follows:"
        apt-get install -y docker.io 2>&1 | tail -15 || true
        fail "Docker could not be installed — a GPU node needs Docker to run jobs. Install it manually (https://docs.docker.com/engine/install/ubuntu/) and re-run."
    fi
else
    ok "Docker already installed: $(docker --version 2>/dev/null)"
fi
start_docker
if docker info >/dev/null 2>&1; then ok "Docker daemon running"; else warn "Docker installed but daemon not up yet — check 'systemctl status docker' (it should start with the service)"; fi

# NVIDIA Container Toolkit — lets containers use the GPU (apt-based distros).
if [[ "$HAS_NVIDIA" == true ]]; then
    if docker info 2>/dev/null | grep -qi nvidia; then
        ok "NVIDIA container runtime already configured"
    elif command -v apt-get &>/dev/null; then
        info "Installing NVIDIA Container Toolkit ..."
        curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey 2>/dev/null \
            | gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg 2>/dev/null || true
        curl -fsSL https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list 2>/dev/null \
            | sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' \
            > /etc/apt/sources.list.d/nvidia-container-toolkit.list 2>/dev/null || true
        apt-get update -qq 2>/dev/null || true
        if DEBIAN_FRONTEND=noninteractive apt-get install -y -qq nvidia-container-toolkit 2>/dev/null; then
            nvidia-ctk runtime configure --runtime=docker >/dev/null 2>&1 || true
            start_docker
            ok "NVIDIA Container Toolkit installed + configured"
        else
            warn "NVIDIA Container Toolkit install failed — GPU jobs won't work. https://docs.nvidia.com/datacenter/cloud-native/"
        fi
    else
        warn "Auto-install of the NVIDIA Container Toolkit supports apt distros only — install manually for GPU passthrough."
    fi
    # Best-effort verification that the GPU reaches a container (non-fatal).
    info "Verifying GPU passthrough (docker run --gpus all) ..."
    if docker run --rm --gpus all nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smi >/dev/null 2>&1; then
        ok "GPU passthrough verified — containers can see the GPU ✓"
    else
        warn "Could not verify GPU-in-container yet (driver/toolkit may need a restart) — node will still register."
    fi
fi

if [[ "$HAS_AMD" == true ]]; then
    info "AMD ROCm container access uses /dev/kfd + /dev/dri (no toolkit) — ensure ROCm is installed (see install-amd.sh)."
fi

# ── Open DDP/NCCL inter-node port ────────────────────────────────────────────
# Gaming cafe cluster mode: when a buyer requests multiple GPUs across LAN nodes,
# PyTorch DistributedDataParallel uses NCCL for gradient sync.
# NCCL's rendezvous listens on port 29500 (configurable via master_port).
# We open this port from the LAN subnet so rank-0 (master) can accept connections
# from worker nodes without firewall blocks.
# Ref: https://pytorch.org/docs/stable/distributed.html#initialization
step "Opening NCCL inter-node port (29500) for cluster mode"
if command -v ufw &>/dev/null; then
  UFW_STATUS=$(ufw status 2>/dev/null | grep "Status:" | awk '{print $2}')
  if [ "$UFW_STATUS" = "active" ]; then
    ufw allow 29500/tcp comment "GridShare NCCL cluster DDP" 2>/dev/null && \
      ok "ufw: port 29500/tcp opened for NCCL" || \
      warn "ufw rule failed — open port 29500/tcp manually if using cluster mode"
  else
    info "ufw is inactive — no firewall rule needed for NCCL"
  fi
else
  info "ufw not found — skipping NCCL firewall rule (iptables-based systems: open port 29500/tcp manually)"
fi
ok "Cluster mode: NCCL port configured"

# ── Python version check ──────────────────────────────────────────────────────

PYTHON_BIN=""
for candidate in python3.12 python3.11 python3.10 python3 python; do
    if command -v "$candidate" &>/dev/null; then
        ver=$("$candidate" -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" 2>/dev/null || echo "")
        if [[ "$ver" =~ ^3\.(1[0-9]|[2-9][0-9])$ ]]; then
            PYTHON_BIN=$(command -v "$candidate")
            ok "Python $ver at $PYTHON_BIN"
            break
        fi
    fi
done

[[ -z "$PYTHON_BIN" ]] && fail "Python 3.10+ not found. Install with: sudo apt install python3.11"

# ── Create directories ────────────────────────────────────────────────────────

step "Creating directories"
mkdir -p "$INSTALL_DIR" "$DATA_DIR" "$LOG_DIR"
ok "Install : $INSTALL_DIR"
ok "Data    : $DATA_DIR"

# ── Virtual environment + wheel ───────────────────────────────────────────────

step "Installing GridShare Node Agent"

if [[ ! -f "$VENV_DIR/bin/python" ]]; then
    info "Creating virtual environment ..."
    "$PYTHON_BIN" -m venv "$VENV_DIR"
fi

PIP="$VENV_DIR/bin/pip"
NODE_BIN="$VENV_DIR/bin/gridshare-node"

info "Upgrading pip ..."
"$PIP" install --upgrade pip --quiet 2>&1 | tail -1 || true

WHEEL_PATH="/tmp/$WHEEL_NAME"
# The version we MUST end up running, parsed straight from the wheel name. We verify
# against this after install — never trust "a binary exists" (an old copy left over
# from a prior install masks a silently-failed upgrade; that's the 1.3.32-vs-1.3.35
# trap we hit, and it would be invisible across a whole fleet).
EXPECTED_VER=$(echo "$WHEEL_NAME" | sed -E 's/^gridshare_node-([0-9][0-9.]*)-.*/\1/')
_PIP_LOG=$(mktemp 2>/dev/null || echo /tmp/gs-pip.log)
info "Downloading GridShare Node Agent ..."
if curl -fsSL -o "$WHEEL_PATH" "$CENTRAL_URL/dl/$WHEEL_NAME" 2>/dev/null; then
    ok "Downloaded $WHEEL_NAME"
    # --force-reinstall so an OTA/older copy already in the venv can NEVER shadow the
    # wheel we just downloaded. Output is captured (not swallowed) so a real failure
    # surfaces in the verify step below instead of being hidden behind `|| true`.
    "$PIP" install "$WHEEL_PATH" --force-reinstall --upgrade --quiet >"$_PIP_LOG" 2>&1 || true
    "$PIP" install "bcrypt>=4.0,<5.0" --quiet >>"$_PIP_LOG" 2>&1 || true
    rm -f "$WHEEL_PATH"
else
    warn "Could not download from server — trying PyPI ..."
    "$PIP" install gridshare-node --force-reinstall --upgrade --quiet >"$_PIP_LOG" 2>&1 || true
fi

[[ -f "$NODE_BIN" ]] || { tail -20 "$_PIP_LOG" 2>/dev/null; rm -f "$_PIP_LOG"; fail "gridshare-node not found after install. Re-run the installer."; }

# Verify the EXACT version landed in the venv the service runs. This is the check
# that turns a silent pip failure into a loud, actionable error instead of a node
# that quietly keeps running stale code.
INSTALLED_VER=$("$VENV_DIR/bin/python" -c "import gridshare_node,sys; sys.stdout.write(gridshare_node.__version__)" 2>/dev/null || echo "")
if [[ -n "$EXPECTED_VER" && "$INSTALLED_VER" != "$EXPECTED_VER" ]]; then
    warn "Version check FAILED: venv has '${INSTALLED_VER:-none}', expected '$EXPECTED_VER'."
    warn "pip did not install cleanly — real output below:"
    tail -25 "$_PIP_LOG" 2>/dev/null
    rm -f "$_PIP_LOG"
    fail "Node agent is not at $EXPECTED_VER. Fix the error above (often low disk/RAM) and re-run."
fi
rm -f "$_PIP_LOG"
ok "GridShare Node Agent installed (v${INSTALLED_VER:-$EXPECTED_VER})"

# ── Registration ──────────────────────────────────────────────────────────────

step "Registering with GridShare"

# Check if already registered
SKIP_REG=false
if [[ -f "$CONFIG_FILE" ]]; then
    EXISTING_KEY=$(python3 -c "import json; d=json.load(open('$CONFIG_FILE')); print(d.get('api_key',''))" 2>/dev/null || echo "")
    EXISTING_NODE=$(python3 -c "import json; d=json.load(open('$CONFIG_FILE')); print(d.get('node_id',''))" 2>/dev/null || echo "")
    if [[ -n "$EXISTING_KEY" && -n "$EXISTING_NODE" ]]; then
        ok "Already registered: node $EXISTING_NODE"
        if [[ "$NONINTERACTIVE" == true ]]; then
            SKIP_REG=true
        else
            read -rp "      Re-register with new details? [y/N]: " redo
            [[ "${redo:-n}" =~ ^[Yy] ]] || SKIP_REG=true
        fi
    fi
fi

if [[ "$SKIP_REG" == false && -n "$GRIDSHARE_TOKEN" ]]; then
    # ── Account-first mode: single tokenized exchange, ZERO prompts ───────────
    # The provider entered name/UPI/city at signup; central reads them off the
    # provider record. We skip the ENTIRE questionnaire (name/UPI/city/price/
    # email/schedule) and make ONE call to /nodes/provision. A stale token sends
    # the provider back to the portal — we do NOT fall back to prompting.
    echo
    echo "  ── Account-First Install ───────────────────────────────────────────"
    _gray "  Provision token found — no questions asked."
    _gray "  Your saved name / UPI / city come straight from your dashboard."
    echo

    MACHINE_LABEL="$GRIDSHARE_LABEL"
    LOCAL_ID="node_$(python3 -c "import uuid; print(uuid.uuid4().hex[:16])")"

    GPU_JSON="[]"
    if [[ ${#GPU_NAMES[@]} -gt 0 ]]; then
        GPU_JSON="["
        for i in "${!GPU_NAMES[@]}"; do
            [[ $i -gt 0 ]] && GPU_JSON+=","
            GPU_JSON+="{\"name\":\"${GPU_NAMES[$i]}\",\"vram_gb\":${GPU_VRAMS[$i]:-0}}"
        done
        GPU_JSON+="]"
    fi
    HW_JSON="{\"cpu_model\":\"$CPU_MODEL\",\"cpu_cores\":$CPU_CORES,\"ram_gb\":$RAM_GB,\"gpu_count\":${#GPU_NAMES[@]},\"gpus\":$GPU_JSON,\"platform\":\"linux\"}"

    info "Provisioning with GridShare ..."
    PROV_HTTP=$(curl -fsSL -w "\n%{http_code}" -X POST "$CENTRAL_URL/nodes/provision" \
        -H "Content-Type: application/json" \
        -d "{\"token\":\"$GRIDSHARE_TOKEN\",\"local_id\":\"$LOCAL_ID\",\"machine_label\":\"$MACHINE_LABEL\",\"hardware\":$HW_JSON,\"version\":\"1.3.31\"}" \
        2>/dev/null || echo $'\n000')
    PROV_CODE=$(echo "$PROV_HTTP" | tail -1)
    PROV_RESP=$(echo "$PROV_HTTP" | sed '$d')

    case "$PROV_CODE" in
        200|201) : ;;
        401) fail "That install link is invalid. Copy a fresh one from your dashboard." ;;
        409) fail "This install link was already used. Mint a new one per machine." ;;
        410) fail "This install link expired. Generate a new one from your dashboard." ;;
        000) fail "Could not reach GridShare server. Check your connection and re-run." ;;
        *)   fail "Provisioning failed (HTTP $PROV_CODE). Mint a fresh install link and re-run." ;;
    esac

    NODE_ID=$(echo "$PROV_RESP"    | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('node_id',''))" 2>/dev/null || echo "")
    API_KEY=$(echo "$PROV_RESP"    | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('api_key',''))" 2>/dev/null || echo "")
    PROV_ID=$(echo "$PROV_RESP"    | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('provider_id',''))" 2>/dev/null || echo "")
    OWNER_NAME=$(echo "$PROV_RESP" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('owner_name',''))" 2>/dev/null || echo "")
    UPI_ID=$(echo "$PROV_RESP"     | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('upi_id',''))" 2>/dev/null || echo "")
    CITY=$(echo "$PROV_RESP"       | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('city',''))" 2>/dev/null || echo "")
    PRICE_PER_HOUR=$(echo "$PROV_RESP" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('price_per_hour_inr',''))" 2>/dev/null || echo "")
    [[ -z "$PRICE_PER_HOUR" ]] && PRICE_PER_HOUR=49

    [[ -n "$API_KEY" && -n "$NODE_ID" ]] || \
        fail "Provisioning response was incomplete. Mint a fresh install link and re-run."

    ok "Node registered: $NODE_ID"
    ok "Installed for ${OWNER_NAME:-your account} · payouts to ${UPI_ID:-UPI on file} · ₹${PRICE_PER_HOUR}/hr"

    # Token installs are always 24/7 (schedule can be set later in the portal).
    SCHEDULE_ENABLED=false
    SCHEDULE_START="22:00"
    SCHEDULE_END="08:00"

    # Write config.json (same block as the prompt path; values came from /provision)
    python3 - <<PYEOF
import json
is_desktop = """$IS_DESKTOP""" == "true"
schedule_enabled = """$SCHEDULE_ENABLED""" == "true"
cfg = {
    "node_id": """$NODE_ID""" or None,
    "api_key": """$API_KEY""" or None,
    "upi_id": """$UPI_ID""",
    "owner_name": """$OWNER_NAME""",
    "city": """$CITY""",
    "local_id": """$LOCAL_ID""",
    "machine_label": """$MACHINE_LABEL""",
    "max_gpu_temp_c": 85,
    "max_cpu_temp_c": 90,
    "max_vrm_temp_c": 105,
    "max_psu_pct": 85,
    "min_fan_rpm": 400,
    "quiet_hours_start": 22,
    "quiet_hours_end": 7,
    "quiet_hours_enabled": True,
    "quiet_gpu_tdp_pct": 60,
    "available": True,
    "allow_spot": True,
    "allow_ondemand": True,
    "max_jobs_concurrent": 1,
    "bandwidth_limit_mbps": 0,
    "accept_job_types": ["training","inference","batch"],
    "safety_auto_cutoff": True,
    "sustained_load_cutoff_hours": 4,
    "sustained_cooldown_minutes": 10,
    "auto_pause_on_user_activity": is_desktop,
    "schedule_enabled": schedule_enabled,
    "schedule_start": """$SCHEDULE_START""",
    "schedule_end": """$SCHEDULE_END""",
    "schedule_timezone": "Asia/Kolkata",
    "schedule_days": ["Mon","Tue","Wed","Thu","Fri","Sat","Sun"],
    "price_per_hour_inr": float("""$PRICE_PER_HOUR"""),
    "dashboard_port": 7842,
    "dashboard_open_on_start": is_desktop,
}
cfg["node_id"] = cfg["node_id"] if cfg["node_id"] else None
cfg["api_key"] = cfg["api_key"] if cfg["api_key"] else None
with open("$CONFIG_FILE", "w") as f:
    json.dump(cfg, f, indent=2)
PYEOF
    ok "Config saved: $CONFIG_FILE"

elif [[ "$SKIP_REG" == false ]]; then
    echo
    echo "  We need a few details to set up your earnings account."
    _gray "  These are used for UPI payouts only — never shared publicly."
    echo

    OWNER_NAME="${GS_OWNER_NAME:-}"
    while [[ -z "$OWNER_NAME" ]]; do
        OWNER_NAME=$(ask "Your full name (for UPI payouts)")
        [[ -z "$OWNER_NAME" ]] && warn "Name is required."
    done

    UPI_ID="${GS_UPI_ID:-}"
    while [[ "$UPI_ID" != *@* ]]; do
        UPI_ID=$(ask "UPI ID  (e.g. yourname@upi, mobile@ybl, name@paytm)")
        [[ "$UPI_ID" != *@* ]] && warn "Invalid UPI ID — must contain @"
    done

    CITY="${GS_CITY:-}"
    if [[ -z "$CITY" ]]; then
        echo
        echo "      Select your city:"
        CITIES=("Chennai" "Bangalore" "Hyderabad" "Mumbai" "Delhi" "Pune" "Kolkata" "Ahmedabad" "Jaipur" "Other")
        for i in "${!CITIES[@]}"; do
            _gray "        $((i+1)). ${CITIES[$i]}"
        done
        CITY_NUM=$(ask "City number (1-${#CITIES[@]})" "1")
        if [[ "$CITY_NUM" =~ ^[0-9]+$ ]] && (( CITY_NUM >= 1 && CITY_NUM <= ${#CITIES[@]} )); then
            CITY="${CITIES[$((CITY_NUM-1))]}"
        else
            CITY="$CITY_NUM"
        fi
        [[ "$CITY" == "Other" ]] && CITY=$(ask "Enter your city name")
    fi

    # ── Pricing (GridShare-assigned) ──────────────────────────────────────────
    # GridShare sets the hourly rate by GPU tier — providers do NOT set it, and any value
    # sent here is overridden by central's tier on register. So NO prompt. You keep 70%;
    # your exact rate is shown right after your node registers.
    echo
    _gray "  GridShare sets your hourly rate by GPU tier — you keep 70%."
    _gray "  Your exact rate is shown once your node registers."
    PRICE_PER_HOUR=0   # placeholder; central assigns the real tier price on register

    EMAIL_OPT="${GS_EMAIL:-}"
    [[ -z "$EMAIL_OPT" && "$NONINTERACTIVE" != true ]] && EMAIL_OPT=$(ask "Email (optional, for account recovery)" "")

    # Schedule setup (optional — for gamers who want to rent only during off-hours)
    echo
    echo "  ── Availability Schedule (Optional) ─────────────────────────────"
    _gray "  Set the hours when your GPU is available for rent."
    _gray "  Example: 22:00-08:00 means renting only 10pm–8am (perfect for gamers)"
    _gray "  Press Enter to skip and stay available 24/7."
    echo
    SCHEDULE_ENABLED=false
    SCHEDULE_START="22:00"
    SCHEDULE_END="08:00"
    if [[ "$NONINTERACTIVE" == true ]]; then
        info "Non-interactive: GPU available 24/7 (auto-pause still applies)"
    else
        SCHEDULE_ENABLED_RAW=$(ask "Set availability schedule? [y/N]" "N")
        if [[ "${SCHEDULE_ENABLED_RAW:-n}" =~ ^[Yy] ]]; then
            SCHEDULE_ENABLED=true
            SCHEDULE_START=$(ask "Available from (HH:MM 24-hour)" "22:00")
            SCHEDULE_END=$(ask "Available until (HH:MM 24-hour, can be next day)" "08:00")
            ok "Schedule set: ${SCHEDULE_START}–${SCHEDULE_END} daily"
        else
            info "No schedule set — GPU available 24/7 (auto-pause still works when you use the PC)"
        fi
    fi

    echo
    info "Registering with GridShare ..."

    # Call /nodes/issue-key
    API_KEY=""
    PROV_ID=""
    REG_RESP=$(curl -fsSL -X POST "$CENTRAL_URL/nodes/issue-key" \
        -H "Content-Type: application/json" \
        -d "{\"owner_name\":\"$OWNER_NAME\",\"upi_id\":\"$UPI_ID\",\"email\":\"$EMAIL_OPT\"}" \
        2>/dev/null || echo "")

    if [[ -n "$REG_RESP" ]]; then
        API_KEY=$(echo "$REG_RESP" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('api_key',''))" 2>/dev/null || echo "")
        PROV_ID=$(echo "$REG_RESP" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('provider_id',''))" 2>/dev/null || echo "")
        if [[ -n "$API_KEY" ]]; then
            ok "Account created. Provider ID: $PROV_ID"
        else
            warn "Registration response: $(echo "$REG_RESP" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('detail','?'))" 2>/dev/null)"
        fi
    else
        warn "Could not reach GridShare server. Config saved — will register on first connection."
    fi

    # Generate local node ID
    LOCAL_ID="node_$(python3 -c "import uuid; print(uuid.uuid4().hex[:16])")"

    # Register node hardware
    NODE_ID=""
    if [[ -n "$API_KEY" ]]; then
        GPU_JSON="[]"
        if [[ ${#GPU_NAMES[@]} -gt 0 ]]; then
            GPU_JSON="["
            for i in "${!GPU_NAMES[@]}"; do
                [[ $i -gt 0 ]] && GPU_JSON+=","
                GPU_JSON+="{\"name\":\"${GPU_NAMES[$i]}\",\"vram_gb\":${GPU_VRAMS[$i]:-0}}"
            done
            GPU_JSON+="]"
        fi
        HW_JSON="{\"cpu_model\":\"$CPU_MODEL\",\"cpu_cores\":$CPU_CORES,\"ram_gb\":$RAM_GB,\"gpu_count\":${#GPU_NAMES[@]},\"gpus\":$GPU_JSON,\"platform\":\"linux\"}"

        NODE_RESP=$(curl -fsSL -X POST "$CENTRAL_URL/nodes/register" \
            -H "Content-Type: application/json" \
            -d "{\"api_key\":\"$API_KEY\",\"upi_id\":\"$UPI_ID\",\"owner_name\":\"$OWNER_NAME\",\"city\":\"$CITY\",\"local_id\":\"$LOCAL_ID\",\"hardware\":$HW_JSON,\"version\":\"1.3.27\"}" \
            2>/dev/null || echo "")

        if [[ -n "$NODE_RESP" ]]; then
            NODE_ID=$(echo "$NODE_RESP" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('node_id',''))" 2>/dev/null || echo "")
            [[ -n "$NODE_ID" ]] && ok "Node registered: $NODE_ID" || warn "Node registration pending — will complete on first connection."
        fi
    fi

    # Write config.json
    python3 - <<PYEOF
import json
is_desktop = """$IS_DESKTOP""" == "true"
schedule_enabled = """$SCHEDULE_ENABLED""" == "true"
cfg = {
    "node_id": """$NODE_ID""" or None,
    "api_key": """$API_KEY""" or None,
    "upi_id": """$UPI_ID""",
    "owner_name": """$OWNER_NAME""",
    "city": """$CITY""",
    "local_id": """$LOCAL_ID""",
    "max_gpu_temp_c": 85,
    "max_cpu_temp_c": 90,
    "max_vrm_temp_c": 105,
    "max_psu_pct": 85,
    "min_fan_rpm": 400,
    "quiet_hours_start": 22,
    "quiet_hours_end": 7,
    "quiet_hours_enabled": True,
    "quiet_gpu_tdp_pct": 60,
    "available": True,
    "allow_spot": True,
    "allow_ondemand": True,
    "max_jobs_concurrent": 1,
    "bandwidth_limit_mbps": 0,
    "accept_job_types": ["training","inference","batch"],
    "safety_auto_cutoff": True,
    "sustained_load_cutoff_hours": 4,
    "sustained_cooldown_minutes": 10,
    # On desktop machines: pause when you use the PC (essential for gamers)
    # On headless servers: disable (no user input to detect)
    "auto_pause_on_user_activity": is_desktop,
    # Schedule-based availability (set by user during install)
    "schedule_enabled": schedule_enabled,
    "schedule_start": """$SCHEDULE_START""",
    "schedule_end": """$SCHEDULE_END""",
    "schedule_timezone": "Asia/Kolkata",
    "schedule_days": ["Mon","Tue","Wed","Thu","Fri","Sat","Sun"],
    "price_per_hour_inr": float("""$PRICE_PER_HOUR"""),
    "dashboard_port": 7842,
    "dashboard_open_on_start": is_desktop,
}
cfg["node_id"] = cfg["node_id"] if cfg["node_id"] else None
cfg["api_key"] = cfg["api_key"] if cfg["api_key"] else None
with open("$CONFIG_FILE", "w") as f:
    json.dump(cfg, f, indent=2)
PYEOF
    ok "Config saved: $CONFIG_FILE"
fi

# ── systemd service ───────────────────────────────────────────────────────────

step "Installing background service"

if [[ "$HAS_SYSTEMD" == true ]]; then
    cat > /etc/systemd/system/${SERVICE_NAME}.service <<EOF
[Unit]
Description=GridShare Node Agent — Earn INR from idle GPU
After=network-online.target docker.service
Wants=network-online.target

[Service]
Type=simple
ExecStart=${NODE_BIN} start
WorkingDirectory=${DATA_DIR}
Environment=GRIDSHARE_CONFIG_DIR=${DATA_DIR}
Environment=PYTHONUNBUFFERED=1
Restart=always
RestartSec=10
StandardOutput=append:${LOG_DIR}/gridshare.log
StandardError=append:${LOG_DIR}/gridshare-error.log
CPUQuota=20%
MemoryMax=512M
SupplementaryGroups=video render

[Install]
WantedBy=multi-user.target
EOF
    systemctl daemon-reload && systemctl enable "${SERVICE_NAME}" >/dev/null 2>&1 \
        && ok "Service installed: ${SERVICE_NAME}" \
        || warn "Could not register systemd service — start manually: ${NODE_BIN} start"

    if [[ "$NONINTERACTIVE" == true ]]; then
        start_now="y"
    else
        echo
        read -rp "      Start GridShare Node Agent now? [Y/n]: " start_now
    fi
    if [[ "${start_now:-y}" =~ ^[Yy] ]]; then
        # restart (not start): on a reinstall the OLD version is already running, and
        # `start` is a no-op on an active service — so the node would keep running stale
        # code while we reported success. restart guarantees the freshly-installed
        # version is the one that comes up.
        systemctl restart "${SERVICE_NAME}" 2>/dev/null || systemctl start "${SERVICE_NAME}" 2>/dev/null || true
        sleep 3
        if systemctl is-active --quiet "${SERVICE_NAME}" 2>/dev/null; then
            ok "Service started and running!"
        else
            warn "Service starting — check: systemctl status ${SERVICE_NAME}"
        fi
    fi
else
    # WSL2 / no-systemd: start via nohup now. Boot persistence is set up on the
    # Windows side by install.ps1 (a scheduled task running `wsl … gridshare-node
    # start` at login), since a WSL2 distro has no init to auto-start services.
    warn "No systemd (WSL2/container) detected — starting via nohup; the Windows installer handles boot-start."
    # Kill any old agent first — otherwise a reinstall leaves the STALE process running
    # (and a second nohup'd agent racing it). pkill the old, then launch the new code.
    pkill -f "gridshare-node start" 2>/dev/null || true
    sleep 1
    nohup env GRIDSHARE_CONFIG_DIR="${DATA_DIR}" PYTHONUNBUFFERED=1 \
        "${NODE_BIN}" start >"${LOG_DIR}/gridshare.log" 2>"${LOG_DIR}/gridshare-error.log" &
    sleep 3
    if pgrep -f "gridshare-node start" >/dev/null 2>&1; then
        ok "Node agent started (nohup, PID $(pgrep -f 'gridshare-node start' | head -1))"
    else
        warn "Agent not running — start manually: ${NODE_BIN} start"
    fi
fi

# ── Verify the node is actually LIVE (connected to the relay) ────────────────
# "Started" ≠ "online": a firewall / blocked outbound HTTPS or a slow first
# connect can leave the agent running but never visible in the marketplace, so
# the provider assumes they're earning and silently isn't. Confirm the real WS
# connection via the local agent's /api/status before declaring success.
echo ""
echo "  Verifying your node is LIVE ..."
NODE_ONLINE=false
for _i in $(seq 1 20); do   # poll ~40s
  _conn=$(curl -s --max-time 3 http://127.0.0.1:7842/api/status 2>/dev/null \
          | python3 -c "import sys,json; print(json.load(sys.stdin).get('connection',{}).get('connected', False))" 2>/dev/null || echo "")
  if [ "$_conn" = "True" ]; then NODE_ONLINE=true; break; fi
  sleep 2
done
if [ "$NODE_ONLINE" = "true" ]; then
  ok "✅ LIVE — your node is connected and now listed in the marketplace."
else
  warn "Agent is running but hasn't connected to GridShare yet (firewall / blocked outbound HTTPS / slow first connect)."
  warn "Watch it connect:  journalctl -u ${SERVICE_NAME} -f   (or: tail -f ${LOG_DIR}/gridshare.log)"
  warn "Give it a minute, then confirm at https://gridshare.in/provider"
fi

# ── Done ─────────────────────────────────────────────────────────────────────

echo
echo
_green "  ╔══════════════════════════════════════════════════════════════╗"
_green "  ║   ✅  GridShare Node Agent Installed Successfully!           ║"
_green "  ╚══════════════════════════════════════════════════════════════╝"
echo

if [[ -f "$CONFIG_FILE" ]]; then
    DISP_NAME=$(python3 -c "import json; d=json.load(open('$CONFIG_FILE')); print(d.get('owner_name',''))" 2>/dev/null || echo "")
    DISP_UPI=$(python3 -c "import json; d=json.load(open('$CONFIG_FILE')); print(d.get('upi_id',''))" 2>/dev/null || echo "")
    DISP_NODE=$(python3 -c "import json; d=json.load(open('$CONFIG_FILE')); print(d.get('node_id',''))" 2>/dev/null || echo "")
    DISP_KEY=$(python3 -c "import json; d=json.load(open('$CONFIG_FILE')); print(d.get('api_key') or '')" 2>/dev/null || echo "")
    [[ -n "$DISP_NAME" ]] && echo "  Provider : $DISP_NAME"
    [[ -n "$DISP_UPI" ]]  && echo "  UPI ID   : $DISP_UPI"
    [[ -n "$DISP_NODE" ]] && echo "  Node ID  : $DISP_NODE"
    if [[ -n "$DISP_KEY" ]]; then
        echo "  API Key  : $DISP_KEY"
        _gray "             ↳ keep this safe — it's your login to the cloud portal"
    fi
    echo
fi

echo "  Local dashboard : http://localhost:7842 (this machine)"
echo "  Cloud portal    : https://gridshare.in/provider  (log in with the API Key above to track earnings across all your PCs)"
echo "  Logs      : $LOG_DIR"
echo "  Config    : $CONFIG_FILE"
echo
echo "  Service commands:"
echo "    Status : systemctl status ${SERVICE_NAME}"
echo "    Stop   : systemctl stop ${SERVICE_NAME}"
echo "    Start  : systemctl start ${SERVICE_NAME}"
echo "    Logs   : journalctl -u ${SERVICE_NAME} -f"
echo
_gray "  Support: support@gridshare.in  |  https://gridshare.in"
echo
