#!/bin/bash
# ============================================================================
# ODS Installer — Phase 07: Developer Tools
# ============================================================================
# Part of: installers/phases/
# Purpose: Install Claude Code, Codex CLI, and OpenCode
#
# Expects: DRY_RUN, INSTALL_DIR, LOG_FILE, LLM_MODEL, MAX_CONTEXT,
#           PKG_MANAGER,
#           ai(), ai_ok(), ai_warn(), log()
# Provides: (developer tools installed to ~/.npm-global)
#
# Modder notes:
#   Add new developer tools or change installation methods here.
# ============================================================================

ods_progress 42 "devtools" "Installing developer tools"
if $DRY_RUN; then
    log "[DRY RUN] Would install AI developer tools (Claude Code, Codex CLI, OpenCode)"
    log "[DRY RUN] Would configure OpenCode for local llama-server (user-level systemd service on port 3003)"
else
    ai "Installing AI developer tools..."

    # Ensure Node.js/npm is available (needed for Claude Code and Codex)
    if ! command -v npm &> /dev/null; then
        # In non-interactive mode, fail fast if sudo requires a password — otherwise
        # the sudo prompt hangs and the trailing error-mask silently skips Node.js,
        # leaving downstream Claude Code / Codex CLI installs to be skipped without
        # any visible failure. See pattern in 05-docker.sh:61-65.
        if [[ "${INTERACTIVE:-true}" != "true" ]] && ! sudo -n true 2>/dev/null; then
            error "Cannot install Node.js: sudo password required but running in --non-interactive mode. Re-run interactively or configure NOPASSWD sudo."
        fi
        ai "Installing Node.js..."
        case "$PKG_MANAGER" in
            apt)
                tmpfile=$(mktemp /tmp/nodesource-setup.XXXXXX.sh)
                if curl -fsSL --max-time 300 https://deb.nodesource.com/setup_22.x -o "$tmpfile" 2>/dev/null; then
                    sudo -E bash "$tmpfile" 2>&1 | tee -a "$LOG_FILE" || ai_warn "Failed to run NodeSource apt setup script (non-fatal — Claude Code/Codex CLI will be skipped)"
                fi
                rm -f "$tmpfile"
                sudo apt-get install -y nodejs 2>&1 | tee -a "$LOG_FILE" || ai_warn "Failed to install nodejs via apt-get (non-fatal — Claude Code/Codex CLI will be skipped)"
                ;;
            dnf)
                sudo dnf module install -y nodejs:22 2>&1 | tee -a "$LOG_FILE" || \
                    sudo dnf install -y nodejs 2>&1 | tee -a "$LOG_FILE" || ai_warn "Failed to install nodejs via dnf (non-fatal — Claude Code/Codex CLI will be skipped)"
                ;;
            pacman)
                sudo pacman -S --noconfirm --needed nodejs npm 2>&1 | tee -a "$LOG_FILE" || ai_warn "Failed to install nodejs via pacman (non-fatal — Claude Code/Codex CLI will be skipped)"
                ;;
            zypper)
                sudo zypper --non-interactive install nodejs22 2>&1 | tee -a "$LOG_FILE" || \
                    sudo zypper --non-interactive install nodejs 2>&1 | tee -a "$LOG_FILE" || ai_warn "Failed to install nodejs via zypper (non-fatal — Claude Code/Codex CLI will be skipped)"
                ;;
            *)
                ai_warn "Unknown package manager — cannot install Node.js automatically"
                ;;
        esac
    fi

    if command -v npm &> /dev/null; then
        # Set up user-level npm global prefix (no sudo needed)
        NPM_GLOBAL_DIR="$HOME/.npm-global"
        if [[ ! -d "$NPM_GLOBAL_DIR" ]]; then
            mkdir -p "$NPM_GLOBAL_DIR"
            npm config set prefix "$NPM_GLOBAL_DIR" 2>/dev/null || true
        fi
        # Ensure user-level bin is on PATH for this session
        export PATH="$NPM_GLOBAL_DIR/bin:$PATH"

        # Install Claude Code (Anthropic's CLI for Claude)
        if ! command -v claude &> /dev/null; then
            npm install -g @anthropic-ai/claude-code >> "$LOG_FILE" 2>&1 && \
                ai_ok "Claude Code installed (run 'claude' to start)" || \
                ai_warn "Claude Code install failed — install later with: npm i -g @anthropic-ai/claude-code"
        else
            ai_ok "Claude Code already installed"
        fi

        # Install Codex CLI (OpenAI's terminal agent)
        if ! command -v codex &> /dev/null; then
            npm install -g @openai/codex >> "$LOG_FILE" 2>&1 && \
                ai_ok "Codex CLI installed (run 'codex' to start)" || \
                ai_warn "Codex CLI install failed — install later with: npm i -g @openai/codex"
        else
            ai_ok "Codex CLI already installed"
        fi

        # Ensure ~/.npm-global/bin is on PATH permanently
        if [[ -d "$NPM_GLOBAL_DIR/bin" ]] && ! grep -q 'npm-global' "$HOME/.bashrc" 2>/dev/null; then
            echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> "$HOME/.bashrc"
            ai "Added ~/.npm-global/bin to PATH in ~/.bashrc"
        fi
    else
        ai_warn "npm not available — skipping Claude Code and Codex CLI install"
        ai "  Install later: npm i -g @anthropic-ai/claude-code @openai/codex"
    fi

    _opencode_candidate_is_file() {
        local candidate="$1"
        [[ -n "$candidate" && "$candidate" == /* && -x "$candidate" && ! -d "$candidate" ]]
    }

    _find_opencode_bin() {
        local candidate
        candidate="$HOME/.opencode/bin/opencode"
        if _opencode_candidate_is_file "$candidate"; then
            printf '%s\n' "$candidate"
            return 0
        fi
        candidate="$(type -P opencode 2>/dev/null || true)"
        if _opencode_candidate_is_file "$candidate"; then
            printf '%s\n' "$candidate"
            return 0
        fi
        return 1
    }

    # ── OpenCode (local agentic coding platform) ──
    OPENCODE_BIN="$(_find_opencode_bin || true)"
    if [[ -z "$OPENCODE_BIN" ]]; then
        ai "Installing OpenCode..."
        tmpfile=$(mktemp /tmp/opencode-install.XXXXXX.sh)
        if curl -fsSL --max-time 300 https://opencode.ai/install -o "$tmpfile" 2>/dev/null && bash "$tmpfile" >> "$LOG_FILE" 2>&1; then
            OPENCODE_BIN="$(_find_opencode_bin || true)"
            ai_ok "OpenCode installer completed"
        else
            ai_warn "OpenCode install failed — install later with: curl -fsSL https://opencode.ai/install | bash"
        fi
        rm -f "$tmpfile"
        [[ -n "$OPENCODE_BIN" ]] && ai_ok "OpenCode installed ($OPENCODE_BIN)" || ai_warn "OpenCode installer completed but opencode was not found"
    else
        ai_ok "OpenCode already installed ($OPENCODE_BIN)"
    fi

    # Configure OpenCode to use local llama-server
    if [[ -n "$OPENCODE_BIN" && -x "$OPENCODE_BIN" ]]; then
        OPENCODE_CONFIG_DIR="$HOME/.config/opencode"
        mkdir -p "$OPENCODE_CONFIG_DIR"
        # Read OLLAMA_PORT and ODS_MODE from .env generated in phase 06
        if [[ -f "$INSTALL_DIR/.env" ]]; then
            [[ -z "${OLLAMA_PORT:-}" ]] && OLLAMA_PORT=$(grep -m1 '^OLLAMA_PORT=' "$INSTALL_DIR/.env" | cut -d= -f2-)
            # Always re-read ODS_MODE from .env — Phase 06 may have changed it
            # (e.g. "local" → "lemonade" for AMD) but the shell variable is stale.
            ODS_MODE=$(grep -m1 '^ODS_MODE=' "$INSTALL_DIR/.env" | cut -d= -f2-)
            [[ -z "${LITELLM_KEY:-}" ]] && LITELLM_KEY=$(grep -m1 '^LITELLM_KEY=' "$INSTALL_DIR/.env" | cut -d= -f2-)
        fi
        # Route through LiteLLM on AMD/Lemonade, direct to llama-server otherwise.
        #
        # The Lemonade branch hits LiteLLM at :4000. LiteLLM is NOT auth-disabled
        # on this install — its container env carries LITELLM_MASTER_KEY from
        # .env (phase 06 wires it; the docker-compose for LiteLLM honors it),
        # and any request without a matching Authorization header gets 401.
        # OpenCode previously sent `apiKey: "no-key"` here (comment claimed
        # auth was removed for local installs — never was), and every chat
        # completion came back 401. The user-visible symptom is OpenCode
        # showing "no connected db" because its provider probe to /v1/models
        # fails auth before it can populate the model selector. Verified on
        # strix-halo + spark + mac-mini + m5-mbp 2026-05-20:
        #   $ curl -sSI http://127.0.0.1:4000/v1/models   → 401
        #   $ curl -sSI -H "Authorization: Bearer $LITELLM_KEY" ... → 200
        #
        # Use LITELLM_KEY (read above at line 122) on the lemonade branch.
        # The llama-server-direct branch keeps "no-key" — llama.cpp's OpenAI-
        # compat server doesn't validate the key.
        if [[ "${ODS_MODE:-local}" == "lemonade" ]]; then
            _opencode_url="http://127.0.0.1:4000/v1"
            _opencode_key="${LITELLM_KEY:-no-key}"
        else
            _opencode_url="http://127.0.0.1:${OLLAMA_PORT:-8080}/v1"
            _opencode_key="no-key"
        fi

        # Writes a fresh opencode.json from the template. Used for first-install
        # and as deterministic recovery when the jq rewrite path finds an
        # existing malformed file it cannot parse (issue #332).
        _opencode_write_fresh() {
            cat > "$1" <<OPENCODE_EOF
{
  "\$schema": "https://opencode.ai/config.json",
  "model": "llama-server/${LLM_MODEL}",
  "small_model": "llama-server/${LLM_MODEL}",
  "provider": {
    "llama-server": {
      "npm": "@ai-sdk/openai-compatible",
      "name": "llama-server (local)",
      "options": {
        "baseURL": "${_opencode_url}",
        "apiKey": "${_opencode_key}"
      },
      "models": {
        "${LLM_MODEL}": {
          "name": "${LLM_MODEL}",
          "limit": {
            "context": ${MAX_CONTEXT:-65536},
            "output": 32768
          }
        }
      }
    }
  }
}
OPENCODE_EOF
        }

        if [[ ! -f "$OPENCODE_CONFIG_DIR/opencode.json" ]]; then
            _opencode_write_fresh "$OPENCODE_CONFIG_DIR/opencode.json"
            ai_ok "OpenCode configured for local llama-server (model: ${LLM_MODEL})"
        else
            # Reinstall: update API key and URL in existing config (key may have changed)
            _opencode_updated=false
            if command -v jq >/dev/null 2>&1; then
                _opencode_tmp="$OPENCODE_CONFIG_DIR/opencode.json.tmp.$$"
                if jq --arg url "$_opencode_url" --arg key "$_opencode_key" \
                    '.provider["llama-server"].options.baseURL = $url
                     | .provider["llama-server"].options.apiKey = $key' \
                    "$OPENCODE_CONFIG_DIR/opencode.json" > "$_opencode_tmp" 2>/dev/null; then
                    mv "$_opencode_tmp" "$OPENCODE_CONFIG_DIR/opencode.json"
                    ai_ok "OpenCode config updated (API key and URL refreshed)"
                    _opencode_updated=true
                else
                    rm -f "$_opencode_tmp"
                    ai_warn "OpenCode config jq rewrite failed (existing file unparseable) — regenerating from template"
                fi
            else
                # Fallback without jq: narrow sed that only matches the quoted value,
                # preserving any trailing comma on the line
                _sed_i "s|\"apiKey\": *\"[^\"]*\"|\"apiKey\": \"${_opencode_key}\"|" "$OPENCODE_CONFIG_DIR/opencode.json"
                _sed_i "s|\"baseURL\": *\"[^\"]*\"|\"baseURL\": \"${_opencode_url}\"|" "$OPENCODE_CONFIG_DIR/opencode.json"
                ai_ok "OpenCode config updated (API key and URL refreshed)"
                _opencode_updated=true
            fi
            # Recovery path (issue #332): if the update branch above failed to
            # produce a valid file (jq parse error on pre-existing corruption),
            # regenerate deterministically from the template.
            if [[ "$_opencode_updated" != "true" ]]; then
                _opencode_write_fresh "$OPENCODE_CONFIG_DIR/opencode.json"
                ai_ok "OpenCode config regenerated from template (recovered from corruption)"
            fi
        fi
        # OpenCode reads config.json, not opencode.json — always sync
        cp "$OPENCODE_CONFIG_DIR/opencode.json" "$OPENCODE_CONFIG_DIR/config.json"

        # Install OpenCode Web UI as user-level systemd service (no sudo required)
        if [[ -f "$INSTALL_DIR/opencode/opencode-web.service" ]]; then
            SYSTEMD_USER_DIR="$HOME/.config/systemd/user"
            mkdir -p "$SYSTEMD_USER_DIR"

            svc_tmp="$(mktemp "${TMPDIR:-/tmp}/opencode-web.service.XXXXXX")" || svc_tmp=""
            if [[ -z "$svc_tmp" ]]; then
                ai_warn "Failed to create secure temp file for opencode-web.service; skipping user-level unit install"
            else
                cp "$INSTALL_DIR/opencode/opencode-web.service" "$svc_tmp"
                # Escape sed special chars to prevent injection from path values
                _home_esc=$(printf '%s\n' "$HOME" | sed 's/[&/\]/\\&/g')
                _opencode_bin_esc=$(printf '%s\n' "$OPENCODE_BIN" | sed 's/[&/\]/\\&/g')
                _opencode_bin_dir_esc=$(printf '%s\n' "$(dirname "$OPENCODE_BIN")" | sed 's/[&/\]/\\&/g')
                _sed_i "s|__HOME__|${_home_esc}|g" "$svc_tmp"
                _sed_i "s|__OPENCODE_BIN__|${_opencode_bin_esc}|g" "$svc_tmp"
                _sed_i "s|__OPENCODE_BIN_DIR__|${_opencode_bin_dir_esc}|g" "$svc_tmp"
                cp "$svc_tmp" "$SYSTEMD_USER_DIR/opencode-web.service"
                rm -f "$svc_tmp"
            fi

            systemctl --user daemon-reload 2>/dev/null || true
            systemctl --user enable --now opencode-web.service >> "$LOG_FILE" 2>&1 && \
                ai_ok "OpenCode Web UI service installed (user-level, port 3003)" || \
                ai_warn "OpenCode Web UI service failed to start"

            # Enable lingering so service survives logout
            loginctl enable-linger "$(whoami)" 2>/dev/null || \
                sudo -n loginctl enable-linger "$(whoami)" 2>/dev/null || \
                ai_warn "Could not enable linger. OpenCode may stop after logout. Run: loginctl enable-linger $(whoami)"
        fi
    fi
fi

# ── ODS Host Agent (extension lifecycle management) ──
# System-mode systemd unit (was --user mode pre-#573). Installs to
# /etc/systemd/system, runs as the installing user with SupplementaryGroups=docker
# so the agent can manage Docker without socket-mounting into a container.
if [[ -f "$INSTALL_DIR/bin/ods-host-agent.py" ]]; then
    AGENT_PYTHON="$(command -v python3)"
    if [[ -n "$AGENT_PYTHON" ]]; then
        if systemctl status >/dev/null 2>&1 || [[ -d /run/systemd/system ]]; then
            # Migrate any pre-existing user-mode unit (idempotent — no-op if absent).
            if [[ -f "$HOME/.config/systemd/user/ods-host-agent.service" ]]; then
                systemctl --user stop ods-host-agent.service 2>/dev/null || true
                systemctl --user disable ods-host-agent.service 2>/dev/null || true
                rm -f "$HOME/.config/systemd/user/ods-host-agent.service"
                systemctl --user daemon-reload 2>/dev/null || true
                ai_ok "Migrated host agent from --user mode to system mode"
            fi

            # System-mode install requires sudo. Fail fast in non-interactive
            # mode if passwordless sudo isn't available (mirrors phase 05).
            if [[ "${INTERACTIVE:-true}" != "true" ]] && ! sudo -n true 2>/dev/null; then
                ai_bad "Host agent install requires sudo and sudo requires a password."
                ai_bad "In non-interactive mode, either:"
                ai "  1. Run with passwordless sudo (NOPASSWD in sudoers)"
                ai "  2. Run the installer interactively (without --non-interactive)"
                error "Cannot install host agent system unit without sudo in non-interactive mode."
            fi

            # Determine the user that should own the running agent. Under
            # `sudo bash install.sh`, $(whoami) returns root — wrong; we want
            # the original invoking user. SUDO_USER is set by sudo and is
            # empty otherwise. INSTALL_USER override wins if explicitly set.
            if [[ -n "${INSTALL_USER:-}" ]]; then
                _agent_user="$INSTALL_USER"
            elif [[ -n "${SUDO_USER:-}" ]]; then
                _agent_user="$SUDO_USER"
            else
                _agent_user="$(whoami)"
            fi

            # Surface (don't block) the case where the agent will run as root.
            # Happens under `sudo su` → `bash install.sh` (SUDO_USER unset, whoami=root).
            # Some appliance/single-user installs may want this; warn so the operator
            # can override with INSTALL_USER if it's unintentional.
            if [[ "$_agent_user" == "root" ]]; then
                ai_warn "Resolved install user is 'root' — host agent will run as root."
                ai_warn "  Set INSTALL_USER=<non-root user> before re-running install if this is unintentional."
            fi

            if [[ -f "$INSTALL_DIR/scripts/systemd/ods-host-agent.service" ]]; then
                svc_tmp="$(mktemp "${TMPDIR:-/tmp}/ods-host-agent.service.XXXXXX")" || svc_tmp=""
                if [[ -z "$svc_tmp" ]]; then
                    ai_warn "Failed to create secure temp file for ods-host-agent.service; skipping systemd unit install"
                else
                    cp "$INSTALL_DIR/scripts/systemd/ods-host-agent.service" "$svc_tmp"
                    # Substitute placeholders — use sed directly with | delimiter
                    # (paths contain / but never |, so | is a safe delimiter).
                    # Dual-form for BSD/GNU sed compatibility.
                    sed -i "s|__INSTALL_DIR__|${INSTALL_DIR}|g" "$svc_tmp" 2>/dev/null || \
                        sed -i '' "s|__INSTALL_DIR__|${INSTALL_DIR}|g" "$svc_tmp"
                    sed -i "s|__HOME__|${HOME}|g" "$svc_tmp" 2>/dev/null || \
                        sed -i '' "s|__HOME__|${HOME}|g" "$svc_tmp"
                    sed -i "s|__PYTHON3__|${AGENT_PYTHON}|g" "$svc_tmp" 2>/dev/null || \
                        sed -i '' "s|__PYTHON3__|${AGENT_PYTHON}|g" "$svc_tmp"
                    sed -i "s|__INSTALL_USER__|${_agent_user}|g" "$svc_tmp" 2>/dev/null || \
                        sed -i '' "s|__INSTALL_USER__|${_agent_user}|g" "$svc_tmp"
                    # Verify placeholders were actually rendered
                    if grep -q '__INSTALL_DIR__\|__HOME__\|__PYTHON3__\|__INSTALL_USER__' "$svc_tmp"; then
                        ai_warn "Host agent systemd unit has unrendered placeholders — check $svc_tmp"
                    else
                        sudo install -m 644 "$svc_tmp" /etc/systemd/system/ods-host-agent.service
                    fi
                    rm -f "$svc_tmp"
                fi
            fi
            sudo systemctl daemon-reload 2>/dev/null || true
            # Pipe through tee (matching the file's existing sudo+log idiom at L31-45)
            # so the redirect runs in the user's shell rather than under sudo (avoids
            # SC2024). pipefail is set in install-core.sh, so the if branches on the
            # actual systemctl exit status, not tee's.
            if sudo systemctl enable --now ods-host-agent.service 2>&1 | tee -a "$LOG_FILE" >/dev/null; then
                ai_ok "ODS host agent installed (systemd system-mode, user=${_agent_user}, port 7710)"
            else
                ai_warn "ODS host agent service failed to start — run: ods agent start"
            fi
            # Force-restart so the running process matches the binary the installer
            # just rewrote. enable --now is a no-op when the unit was already active,
            # which would leave an old daemon holding a deleted inode and serving
            # stale code after a reinstall. See issue #334. Use is-enabled (not
            # is-active) so a temporarily-down daemon during a fresh install still
            # triggers the restart rather than skipping it.
            if sudo systemctl is-enabled ods-host-agent.service >/dev/null 2>&1; then
                if sudo systemctl restart ods-host-agent.service 2>&1 | tee -a "$LOG_FILE" >/dev/null; then
                    ai_ok "ODS host agent restarted (loaded new binary)"
                else
                    ai_warn "ODS host agent restart failed (non-fatal) — run: sudo systemctl restart ods-host-agent.service"
                fi
            fi
            # loginctl enable-linger no longer needed for host agent (system-mode unit)

        else
            ai_warn "No systemd detected — ods host agent not auto-installed."
            ai_warn "  Start manually: ods agent start"
        fi
    else
        ai_warn "python3 not found — ods host agent not installed"
    fi
fi

# ── ODS mDNS Announcer (publishes ods.local on the LAN) ──
# Makes the device discoverable from any phone/laptop on the same network
# without typing an IP. See docs/MDNS.md for details. Linux-only; macOS
# announces hostname.local automatically via Bonjour, the script is a no-op
# there. Windows support TBD.
if [[ -f "$INSTALL_DIR/bin/ods-mdns.py" ]] && [[ "$(uname -s)" == "Linux" ]]; then
    # Install python3-zeroconf via the system package manager. Non-fatal —
    # mDNS is a quality-of-life feature; if zeroconf isn't available the
    # device is still reachable by IP.
    #
    # Two-tier strategy:
    #   1. Try the distro package manager first (best: integrates with apt
    #      upgrades, doesn't require pip / network in offline mode).
    #   2. Fall back to `pip install --user zeroconf` if the package isn't
    #      in the distro's repos (e.g. minimal images, stale apt cache,
    #      Ubuntu universe disabled) — without this, the install logs a
    #      warning and the mDNS announcer never starts even though the
    #      Python module is one pip away.
    _install_zeroconf_via_pkg() {
        case "$PKG_MANAGER" in
            apt)    sudo apt-get install -y python3-zeroconf 2>&1 | tee -a "$LOG_FILE" ;;
            dnf)    sudo dnf install -y python3-zeroconf 2>&1 | tee -a "$LOG_FILE" ;;
            pacman) sudo pacman -S --noconfirm --needed python-zeroconf 2>&1 | tee -a "$LOG_FILE" ;;
            zypper) sudo zypper --non-interactive install python3-zeroconf 2>&1 | tee -a "$LOG_FILE" ;;
            *)      return 99 ;;
        esac
    }
    _install_zeroconf_via_pip() {
        # `--user` writes into ~/.local/lib/python3.x/site-packages so we
        # don't need sudo and don't fight PEP 668 (Debian/Ubuntu mark the
        # system site-packages as externally-managed). The mDNS announcer
        # runs as $USER, not root, so --user is the right install scope.
        if command -v pip3 >/dev/null 2>&1; then
            pip3 install --user --quiet --no-warn-script-location zeroconf 2>&1 | tee -a "$LOG_FILE"
        else
            return 99
        fi
    }
    if ! python3 -c "import zeroconf" 2>/dev/null; then
        ai "Installing python3-zeroconf (for mDNS announcer)..."
        if _install_zeroconf_via_pkg && python3 -c "import zeroconf" 2>/dev/null; then
            ai_ok "Installed python3-zeroconf via $PKG_MANAGER"
        elif python3 -c "import zeroconf" 2>/dev/null; then
            : # Package manager returned non-zero, but the module is importable.
        elif _install_zeroconf_via_pip && python3 -c "import zeroconf" 2>/dev/null; then
            ai_ok "Installed zeroconf via pip --user (system package manager unavailable / failed)"
        else
            ai_warn "Failed to install zeroconf via $PKG_MANAGER AND pip --user — mDNS announcer will not start (non-fatal; device still reachable by IP)"
        fi
    fi

    # Install the systemd unit alongside ods-host-agent. Reuses the same
    # __PLACEHOLDER__ substitution pattern, the same user resolution
    # (INSTALL_USER → SUDO_USER → whoami), and the same sudo discipline.
    if python3 -c "import zeroconf" 2>/dev/null && \
       (systemctl status >/dev/null 2>&1 || [[ -d /run/systemd/system ]]) && \
       [[ -f "$INSTALL_DIR/scripts/systemd/ods-mdns.service" ]]; then
        MDNS_PYTHON="$(command -v python3)"
        # Reuse $_agent_user from the host-agent block above if set; otherwise
        # resolve fresh. Falls back to the same heuristic.
        if [[ -z "${_agent_user:-}" ]]; then
            if [[ -n "${INSTALL_USER:-}" ]]; then
                _agent_user="$INSTALL_USER"
            elif [[ -n "${SUDO_USER:-}" ]]; then
                _agent_user="$SUDO_USER"
            else
                _agent_user="$(whoami)"
            fi
        fi
        svc_tmp="$(mktemp "${TMPDIR:-/tmp}/ods-mdns.service.XXXXXX")" || svc_tmp=""
        if [[ -z "$svc_tmp" ]]; then
            ai_warn "Failed to create secure temp file for ods-mdns.service; skipping systemd unit install"
        else
            cp "$INSTALL_DIR/scripts/systemd/ods-mdns.service" "$svc_tmp"
            sed -i "s|__INSTALL_DIR__|${INSTALL_DIR}|g" "$svc_tmp" 2>/dev/null || \
                sed -i '' "s|__INSTALL_DIR__|${INSTALL_DIR}|g" "$svc_tmp"
            sed -i "s|__HOME__|${HOME}|g" "$svc_tmp" 2>/dev/null || \
                sed -i '' "s|__HOME__|${HOME}|g" "$svc_tmp"
            sed -i "s|__PYTHON3__|${MDNS_PYTHON}|g" "$svc_tmp" 2>/dev/null || \
                sed -i '' "s|__PYTHON3__|${MDNS_PYTHON}|g" "$svc_tmp"
            sed -i "s|__INSTALL_USER__|${_agent_user}|g" "$svc_tmp" 2>/dev/null || \
                sed -i '' "s|__INSTALL_USER__|${_agent_user}|g" "$svc_tmp"
            if grep -q '__INSTALL_DIR__\|__HOME__\|__PYTHON3__\|__INSTALL_USER__' "$svc_tmp"; then
                ai_warn "ods-mdns systemd unit has unrendered placeholders — check $svc_tmp"
            else
                sudo install -m 644 "$svc_tmp" /etc/systemd/system/ods-mdns.service
                sudo systemctl daemon-reload 2>/dev/null || true
                if sudo systemctl enable --now ods-mdns.service 2>&1 | tee -a "$LOG_FILE" >/dev/null; then
                    _device_name="$(grep -E '^ODS_DEVICE_NAME=' "$INSTALL_DIR/.env" 2>/dev/null | cut -d= -f2 | tr -d '"' || true)"
                    _device_name="${_device_name:-ods}"
                    # Be honest about what mDNS does on its own: it publishes the
                    # name. Whether that name LOADS anything in a browser depends
                    # on ods-proxy being on :80 and ODS_PROXY_BIND=0.0.0.0. Those
                    # are operator choices made elsewhere (and surfaced in the
                    # first-boot wizard); don't claim the URL works yet.
                    ai_ok "ODS mDNS announcer installed — '${_device_name}.local' now resolves on the LAN"
                    ai "  Enable ods-proxy (ODS_PROXY_BIND defaults to 0.0.0.0) to make http://${_device_name}.local serve chat. See docs/ODS-PROXY.md."
                else
                    ai_warn "ODS mDNS announcer failed to start (non-fatal — device is still reachable by IP)"
                fi
            fi
            rm -f "$svc_tmp"
        fi
    fi
fi
