#!/bin/bash
#
# codevibe-gemini - Wrapper to run Gemini CLI inside tmux for mobile control
#
# This script launches Gemini CLI inside a tmux session, enabling:
# - Mobile prompts when screen is locked (via tmux send-keys)
# - Same user experience as regular gemini command
# - Automatic sync with iOS app via MCP server
#
# Usage:
#   codevibe-gemini [gemini-args...]
#   codevibe-gemini login              # Sign in via browser
#   codevibe-gemini logout             # Sign out
#   codevibe-gemini status             # Show auth status
#
# Environment:
#   ENVIRONMENT                         # Set to 'production' (default) or 'development'
#
# Examples:
#   codevibe-gemini                    # Start new session
#   codevibe-gemini --resume           # Resume last session
#   codevibe-gemini "fix the bug"      # Start with prompt
#   ENVIRONMENT=development codevibe-gemini login  # Login to development
#

set -e

# Default to production environment if not specified
# Users can set ENVIRONMENT=development for local development
export ENVIRONMENT="${ENVIRONMENT:-production}"

# Use TMPDIR if set (macOS sets this to user-specific temp), otherwise /tmp
CODEVIBE_TMPDIR="${TMPDIR:-/tmp}"

# Get the directory where this script is located (resolving symlinks)
# This is needed because npm global installs symlink bin scripts to /usr/local/bin/
SOURCE="${BASH_SOURCE[0]}"
while [ -L "$SOURCE" ]; do
  DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
  SOURCE="$(readlink "$SOURCE")"
  [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE"
done
SCRIPT_DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
PLUGIN_DIR="$(dirname "$SCRIPT_DIR")"

# ─── PATH augmentation ───────────────────────────────────────────────
# When the install one-liner runs in a fresh terminal, Homebrew's
# installer writes the shellenv eval into ~/.zprofile (and similar)
# but the user's current shell hasn't sourced it yet. Subsequent
# codevibe-* runs in that same terminal then fail tmux discovery
# because /opt/homebrew/bin isn't on PATH. Prepend common locations
# so the wrapper recovers without forcing the user to open a new
# terminal. Prepend (not append) is deliberate: the Homebrew binary
# install.sh just laid down should win over any older system binary
# (e.g. a stale /usr/bin/node on Linux) at the same name. To preserve
# the relative ordering of augmented dirs, build a single prefix
# string and prepend it once — iterating prepend-per-dir would
# reverse intended order. ${PATH:+:$PATH} keeps an empty starting
# PATH from producing a trailing colon (which makes cwd searchable).
_CV_NEW_PATHS=""
for _CV_DIR in /opt/homebrew/bin /opt/homebrew/sbin /usr/local/bin /usr/local/sbin /opt/local/bin /usr/bin /bin; do
  case ":$PATH:" in
    *":$_CV_DIR:"*) ;;
    *) [ -d "$_CV_DIR" ] && _CV_NEW_PATHS="$_CV_NEW_PATHS:$_CV_DIR" ;;
  esac
done
[ -n "$_CV_NEW_PATHS" ] && export PATH="${_CV_NEW_PATHS#:}${PATH:+:$PATH}"
unset _CV_DIR _CV_NEW_PATHS

# ─── Wrapper telemetry (GA4 Measurement Protocol) ─────────────────────
# Diagnoses agent CLI failures: pre-flight bailouts, fast-die patterns,
# whether SessionStart hook fired, exit code. Background curl, fail
# silently, no PII (hashed hostname + per-run random id only). Honors
# CODEVIBE_TELEMETRY_SOURCE=test for internal testing.
_CV_MID="G-GS74YEQTB8"
_CV_SEC="lAfOF6OxRzSQ-NsLBRjhAg"
_CV_CID="$(echo "$(uname -n)-$(id -u)" | (sha256sum 2>/dev/null || shasum -a 256 2>/dev/null || echo "anonymous-fallback ") | cut -c1-36)"
_CV_RUN_ID="$(head -c 16 /dev/urandom 2>/dev/null | od -An -tx1 | tr -d ' \n' | cut -c1-32)"
[ -z "$_CV_RUN_ID" ] && _CV_RUN_ID="fallback-$(date +%s)-$$"
_CV_AGENT="gemini"
_CV_SOURCE="${CODEVIBE_TELEMETRY_SOURCE:-production}"
_CV_STARTED_AT="$(date +%s)"
_CV_EXITED=""    # set by terminal events; suppresses trap double-fire
_CV_PLUGIN_VERSION="$(node -p "require('$PLUGIN_DIR/package.json').version" 2>/dev/null || echo unknown)"
_CV_MCP_LOG="${CODEVIBE_TMPDIR}/codevibe-gemini-mcp.log"
_CV_MCP_LOG_BASELINE=0
if [ -f "$_CV_MCP_LOG" ]; then
  _CV_MCP_LOG_BASELINE=$(wc -l < "$_CV_MCP_LOG" 2>/dev/null | tr -d ' ')
  [ -z "$_CV_MCP_LOG_BASELINE" ] && _CV_MCP_LOG_BASELINE=0
fi
_CV_TMUX_STARTED="false"
_CV_AGENT_INVOKED="false"
_CV_AGENT_STARTED_AT=0
_CV_GEMINI_EXIT_FILE="${CODEVIBE_TMPDIR}/codevibe-gemini-exit-$$"

# Strip an arbitrary string down to a JSON-safe identifier alphabet.
# Removes anything that could break the hand-built JSON payload below
# (quotes, backslashes, ANSI escapes, control bytes, tabs, newlines).
# Truncates to 40 chars to bound the impact of pathological CLI version
# output. Caller is responsible for emptiness check after sanitize.
cv_sanitize() {
  printf '%s' "$1" | LC_ALL=C tr -cd 'A-Za-z0-9._\- ' | cut -c1-40
}

# Sanitize trusted-but-still-string values that go into the payload
# (plugin version, source label) so future schema additions can't
# accidentally reintroduce a JSON-injection path.
_CV_PLUGIN_VERSION="$(cv_sanitize "$_CV_PLUGIN_VERSION")"
[ -z "$_CV_PLUGIN_VERSION" ] && _CV_PLUGIN_VERSION="unknown"
_CV_SOURCE="$(cv_sanitize "$_CV_SOURCE")"
[ -z "$_CV_SOURCE" ] && _CV_SOURCE="production"

cv_telem() {
  local event="$1"; shift
  local params="$*"
  curl -s -X POST \
    "https://www.google-analytics.com/mp/collect?measurement_id=${_CV_MID}&api_secret=${_CV_SEC}" \
    -H "Content-Type: application/json" \
    -d "{\"client_id\":\"${_CV_CID}\",\"events\":[{\"name\":\"${event}\",\"params\":{\"agent\":\"${_CV_AGENT}\",\"plugin_version\":\"${_CV_PLUGIN_VERSION}\",\"source\":\"${_CV_SOURCE}\",\"run_id\":\"${_CV_RUN_ID}\"${params:+,$params}}}]}" \
    </dev/null >/dev/null 2>&1 &
}

cv_failed() {
  [ -n "$_CV_EXITED" ] && return 0
  _CV_EXITED="failed"
  cv_telem "wrapper_failed" "\"reason\":\"$1\",\"lifetime_seconds\":$(( $(date +%s) - _CV_STARTED_AT ))"
}

# Handle auth commands (login, logout, status, reset-device)
# Delegate to codevibe-core CLI (shared auth across all plugins)
case "$1" in
    login|logout|status|reset-device)
        CORE_CLI="$PLUGIN_DIR/node_modules/@quantiya/codevibe-core/bin/codevibe.js"
        # Also check hoisted location (when installed via @quantiya/codevibe meta-package)
        if [ ! -f "$CORE_CLI" ]; then
            CORE_CLI="$PLUGIN_DIR/../codevibe-core/bin/codevibe.js"
        fi
        if [ -f "$CORE_CLI" ]; then
            cv_telem "wrapper_started" "\"invocation\":\"auth_$1\",\"os\":\"$(uname -s | cv_sanitize)\",\"arch\":\"$(uname -m | cv_sanitize)\""
            exec node "$CORE_CLI" "$1"
        else
            echo "Error: codevibe-core not found. Try reinstalling: npm install -g @quantiya/codevibe"
            cv_failed "core_not_found"
            sleep 1
            exit 1
        fi
        ;;
esac

# Capture environment facts for the session-flow wrapper_started event.
# Each probe is non-fatal — if a CLI is missing we record "missing" rather
# than aborting; pre-flight checks below still gate execution. Every
# string that lands in the JSON payload goes through cv_sanitize so an
# agent CLI emitting ANSI escapes or quotes in `--version` can't break
# the hand-built payload.
_CV_GEMINI_VER="missing"
command -v gemini >/dev/null 2>&1 && _CV_GEMINI_VER="$(gemini --version 2>/dev/null | cv_sanitize)"
[ -z "$_CV_GEMINI_VER" ] && _CV_GEMINI_VER="unknown"
_CV_NODE_VER="missing"
command -v node >/dev/null 2>&1 && _CV_NODE_VER="$(node -v 2>/dev/null | cv_sanitize)"
[ -z "$_CV_NODE_VER" ] && _CV_NODE_VER="unknown"
_CV_TMUX_VER="missing"
command -v tmux >/dev/null 2>&1 && _CV_TMUX_VER="$(tmux -V 2>/dev/null | cv_sanitize)"
[ -z "$_CV_TMUX_VER" ] && _CV_TMUX_VER="unknown"
_CV_OS_VER="$(uname -s | cv_sanitize)"
[ -z "$_CV_OS_VER" ] && _CV_OS_VER="unknown"
_CV_ARCH_VER="$(uname -m | cv_sanitize)"
[ -z "$_CV_ARCH_VER" ] && _CV_ARCH_VER="unknown"
_CV_GEMINI_AUTH="false"; [ -f "$HOME/.gemini/oauth_creds.json" ] && _CV_GEMINI_AUTH="true"
_CV_GEMINI_SETTINGS="false"; [ -f "$HOME/.gemini/settings.json" ] && _CV_GEMINI_SETTINGS="true"
_CV_INSIDE_TMUX="false"; [ -n "$TMUX" ] && _CV_INSIDE_TMUX="true"
_CV_IS_TTY="false"; { [ -t 0 ] && [ -t 1 ]; } && _CV_IS_TTY="true"
cv_telem "wrapper_started" "\"invocation\":\"session\",\"os\":\"$_CV_OS_VER\",\"arch\":\"$_CV_ARCH_VER\",\"gemini_version\":\"$_CV_GEMINI_VER\",\"node_version\":\"$_CV_NODE_VER\",\"tmux_version\":\"$_CV_TMUX_VER\",\"gemini_auth_present\":$_CV_GEMINI_AUTH,\"gemini_settings_present\":$_CV_GEMINI_SETTINGS,\"inside_tmux\":$_CV_INSIDE_TMUX,\"is_terminal\":$_CV_IS_TTY"

# Configuration — hoisted ABOVE configure_hooks_if_needed because the
# function logs to $LOG_FILE on failure paths. With LOG_FILE undefined,
# `2>>"$LOG_FILE"` becomes `2>>""` and the redirection silently fails
# (or, on stricter shells, breaks the function entirely). Codified
# 2026-04-30 after R1 caught the ordering bug.
TMUX_SESSION_PREFIX="codevibe-gemini"
LOG_FILE="${CODEVIBE_TMPDIR}/codevibe-gemini-wrapper.log"
MCP_LOG_FILE="${CODEVIBE_TMPDIR}/codevibe-gemini-mcp.log"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"
}

# jq is required at HOOK RUNTIME (not just install time) by every Gemini
# hook script — see hooks/*.sh: jq parses the stdin JSON Gemini CLI feeds
# into each hook. The wrapper's own hook merger (below) was ported off jq
# but the bundled hook scripts still require it. Without jq, hooks fire
# but produce empty payloads, which lands as a 0-event ghost session in
# DDB — same silent-failure pattern as the codex jq-merge bug. Until the
# hooks are ported to a no-jq parser (see follow-up task), fail closed
# with a clear reason so users get an actionable error instead of a
# session that quietly never syncs.
#
# Codex hooks are unaffected — they don't use jq at runtime.
if ! command -v jq &> /dev/null; then
    echo "Error: jq is required by CodeVibe Gemini hooks but is not installed."
    echo "Install with: brew install jq  (macOS) or  apt-get install jq  (Debian/Ubuntu) or  dnf install jq  (Fedora)"
    cv_failed "jq_missing_for_hooks"
    sleep 1
    exit 1
fi

# Export hooks directory for hook scripts to use
export CODEVIBE_HOOKS_DIR="$PLUGIN_DIR/hooks"

# Auto-configure hooks on startup if not already configured.
# Sets _CV_HOOKS_OUTCOME / _CV_HOOKS_REASON for the hooks_install_outcome
# telemetry beacon fired in the caller. Gemini events flow ONLY through
# hooks (no transcript fallback for tool/approval events), so a silent
# skip here = a 0-event ghost session in DDB. Codified after the susyustc
# 2026-04 silent-failure pattern was traced to the codex twin of this bug.
_CV_HOOKS_OUTCOME="unknown"
_CV_HOOKS_REASON=""
configure_hooks_if_needed() {
    GEMINI_SETTINGS_DIR="$HOME/.gemini"
    GEMINI_SETTINGS_FILE="$GEMINI_SETTINGS_DIR/settings.json"
    HOOKS_DIR="$PLUGIN_DIR/hooks"
    TEMPLATE_FILE="$PLUGIN_DIR/gemini-hooks-settings.json"

    # Make hook scripts executable
    chmod +x "$HOOKS_DIR"/*.sh 2>/dev/null || true

    # Create .gemini directory if it doesn't exist
    mkdir -p "$GEMINI_SETTINGS_DIR"

    # Check if hooks need to be configured
    # Guard: if template is missing, skip auto-configure entirely
    if [ ! -f "$TEMPLATE_FILE" ]; then
        echo "⚠ Hooks template not found at: $TEMPLATE_FILE"
        echo "  Try: npm install -g @quantiya/codevibe@latest"
        _CV_HOOKS_OUTCOME="template_missing"
        _CV_HOOKS_REASON="bundled_template_not_found"
        return
    fi

    # Single Node-based hook installer: handles fresh install, idempotent
    # merge, and "already installed" detection — all with a stable
    # ownership predicate (path-prefix + filename-tail). Why one combined
    # script vs three branches with grep:
    #
    #  - The "already installed" check used to be `grep -q "codevibe"`
    #    over the whole file. That false-positives if any unrelated user
    #    setting happens to contain "codevibe" (a path, a comment field
    #    in JSON, etc.). The structured check looks for our specific
    #    hook-script files.
    #
    #  - The merge used to unconditionally APPEND our entries to existing
    #    arrays. Two concurrent codevibe-gemini starts could each read
    #    the pre-merge state and each append, producing duplicate entries
    #    that then double-fire every event. The new merger filters out
    #    OUR previously-installed entries (any version's path) before
    #    appending fresh ones. Multiple runs converge to the same
    #    settings.json regardless of interleaving.
    HOOKS_JSON=$(sed "s|__CODEVIBE_HOOKS_DIR__|$HOOKS_DIR|g" "$TEMPLATE_FILE")
    # Wrapping in `if … ; then … else INSTALLER_RC=$? ; fi` is required
    # under `set -e` to capture Node's non-zero exit codes; a bare
    # `VAR=$(cmd)` followed by `RC=$?` would short-circuit on Node's
    # exit before $? gets read, defeating the fail-closed mapping below.
    if INSTALLER_OUTPUT=$(CV_EXISTING="$GEMINI_SETTINGS_FILE" \
                         CV_NEW="$HOOKS_JSON" \
                         node -e '
      const fs = require("fs");
      const OUR_HOOK_FILES = new Set([
        "session-start.sh",
        "session-end.sh",
        "before-agent.sh",
        "after-agent.sh",
        "before-tool.sh",
        "after-tool.sh",
        "notification.sh",
      ]);
      const OWN_PATH_MARKER = "codevibe-gemini-plugin/hooks/";
      const isOurCommand = (command) => {
        if (typeof command !== "string") return false;
        if (command.indexOf(OWN_PATH_MARKER) === -1) return false;
        for (const f of OUR_HOOK_FILES) {
          if (command.endsWith("/" + f)) return true;
        }
        return false;
      };
      // Strip our hooks from a single matcher entrys inner hooks[] array.
      // Returns null if no non-owned hooks remain (drop the entry);
      // otherwise returns a new entry with only the user hooks. Returns
      // the original entry unchanged when it contains none of ours.
      const stripOurHooksFromEntry = (entry) => {
        if (!entry || typeof entry !== "object" || !Array.isArray(entry.hooks)) {
          return entry;
        }
        const filtered = entry.hooks.filter((h) => !(h && isOurCommand(h.command)));
        if (filtered.length === entry.hooks.length) return entry;
        if (filtered.length === 0) return null;
        return { ...entry, hooks: filtered };
      };
      const entryContainsOurs = (entry) =>
        entry && typeof entry === "object" && Array.isArray(entry.hooks) &&
        entry.hooks.some((h) => h && isOurCommand(h.command));

      let existingObj = null;
      let existed = false;
      try {
        if (fs.existsSync(process.env.CV_EXISTING)) {
          existed = true;
          existingObj = JSON.parse(fs.readFileSync(process.env.CV_EXISTING, "utf8"));
        }
      } catch (e) {
        process.stderr.write("PARSE_EXISTING_FAILED:" + (e && e.message ? e.message : String(e)));
        process.exit(2);
      }
      let nextObj;
      try {
        nextObj = JSON.parse(process.env.CV_NEW);
      } catch (e) {
        process.stderr.write("PARSE_NEW_FAILED:" + (e && e.message ? e.message : String(e)));
        process.exit(3);
      }

      const existingHooks = (existingObj && typeof existingObj === "object" &&
        existingObj.hooks && typeof existingObj.hooks === "object") ? existingObj.hooks : {};
      const nextHooks = (nextObj && typeof nextObj === "object" &&
        nextObj.hooks && typeof nextObj.hooks === "object") ? nextObj.hooks : {};
      let allPresent = existed;
      for (const k of Object.keys(nextHooks)) {
        const arr = Array.isArray(existingHooks[k]) ? existingHooks[k] : [];
        if (!arr.some(entryContainsOurs)) {
          allPresent = false;
          break;
        }
      }
      if (allPresent) {
        process.stdout.write("OUTCOME:already_installed\n");
        process.exit(0);
      }

      const mergedHooks = { ...existingHooks };
      for (const k of Object.keys(nextHooks)) {
        const existingArr = Array.isArray(existingHooks[k]) ? existingHooks[k] : [];
        const cleanedExisting = existingArr
          .map(stripOurHooksFromEntry)
          .filter((entry) => entry !== null && entry !== undefined);
        const nextArr = Array.isArray(nextHooks[k]) ? nextHooks[k] : [];
        mergedHooks[k] = [...cleanedExisting, ...nextArr];
      }

      const out = (existingObj && typeof existingObj === "object")
        ? { ...existingObj, hooks: mergedHooks }
        : { hooks: mergedHooks };
      const outcome = existed ? "merged" : "fresh_install";
      const target = process.env.CV_EXISTING;
      // Per-process tmp suffix avoids two concurrent installs both
      // writing ${target}.tmp and one renaming the others half-write.
      const tmp = target + "." + process.pid + "." + Date.now() + ".tmp";
      try {
        fs.writeFileSync(tmp, JSON.stringify(out, null, 2));
        fs.renameSync(tmp, target);
      } catch (e) {
        try { fs.unlinkSync(tmp); } catch {}
        process.stderr.write("WRITE_FAILED:" + (e && e.message ? e.message : String(e)));
        process.exit(4);
      }
      process.stdout.write("OUTCOME:" + outcome + "\n");
    ' 2>>"$LOG_FILE"); then
      INSTALLER_RC=0
    else
      INSTALLER_RC=$?
    fi
    if [ "$INSTALLER_RC" -eq 0 ]; then
        case "$INSTALLER_OUTPUT" in
          OUTCOME:already_installed*)
            _CV_HOOKS_OUTCOME="already_installed"
            ;;
          OUTCOME:merged*)
            echo "✓ CodeVibe hooks added to: $GEMINI_SETTINGS_FILE"
            _CV_HOOKS_OUTCOME="merged"
            ;;
          OUTCOME:fresh_install*)
            echo "✓ Hooks configured at: $GEMINI_SETTINGS_FILE"
            _CV_HOOKS_OUTCOME="fresh_install"
            ;;
          *)
            _CV_HOOKS_OUTCOME="merge_failed"
            _CV_HOOKS_REASON="unrecognized_outcome"
            log "ERROR: hooks installer returned unrecognized outcome: $INSTALLER_OUTPUT"
            ;;
        esac
    else
        case "$INSTALLER_RC" in
          2) _CV_HOOKS_OUTCOME="merge_failed"; _CV_HOOKS_REASON="parse_existing_error" ;;
          3) _CV_HOOKS_OUTCOME="merge_failed"; _CV_HOOKS_REASON="parse_template_error" ;;
          4) _CV_HOOKS_OUTCOME="merge_failed"; _CV_HOOKS_REASON="write_failed" ;;
          *) _CV_HOOKS_OUTCOME="merge_failed"; _CV_HOOKS_REASON="installer_exit_$INSTALLER_RC" ;;
        esac
        echo "⚠ Failed to install/merge CodeVibe hooks (rc=$INSTALLER_RC, reason=$_CV_HOOKS_REASON)"
        log "ERROR: hooks installer failed (rc=$INSTALLER_RC, reason=$_CV_HOOKS_REASON)"
    fi
}

# Configure hooks before starting
configure_hooks_if_needed
cv_telem "hooks_install_outcome" "\"outcome\":\"$_CV_HOOKS_OUTCOME\",\"reason\":\"$_CV_HOOKS_REASON\""

# If hook installation didn't land, there's no point starting the daemon —
# Gemini events flow ONLY through hooks. Bail with a clear message so the
# user can fix the underlying file/permission issue rather than running a
# silent ghost session.
case "$_CV_HOOKS_OUTCOME" in
  fresh_install|merged|already_installed) ;;
  *)
    echo "Error: failed to install CodeVibe hooks ($_CV_HOOKS_OUTCOME)."
    echo "       Without hooks installed, Gemini sessions cannot sync to mobile."
    echo "       Check $LOG_FILE for the underlying error."
    cv_failed "hooks_install_$_CV_HOOKS_OUTCOME"
    sleep 1
    exit 1
    ;;
esac

# (TMUX_SESSION_PREFIX/LOG_FILE/MCP_LOG_FILE/log() were hoisted above
# configure_hooks_if_needed.)

# Cleanup function to kill MCP server when wrapper exits
cleanup() {
    local wrapper_exit_code=$?
    log "Cleanup triggered"

    # Fire wrapper_exited telemetry BEFORE killing the server so the MCP
    # log is intact when we grep for SessionStart. cv_failed sets
    # _CV_EXITED on pre-flight failures so this block won't double-fire.
    if [ -z "$_CV_EXITED" ]; then
        _CV_EXITED="exited"
        local gemini_exit="unknown"
        if [ -f "$_CV_GEMINI_EXIT_FILE" ]; then
            gemini_exit="$(cat "$_CV_GEMINI_EXIT_FILE" 2>/dev/null | head -c 10 | tr -d '\n\r ')"
            [ -z "$gemini_exit" ] && gemini_exit="unknown"
        fi
        local lifetime=$(( $(date +%s) - _CV_STARTED_AT ))
        local gemini_lifetime=0
        if [ "$_CV_AGENT_STARTED_AT" -gt 0 ] 2>/dev/null; then
            gemini_lifetime=$(( $(date +%s) - _CV_AGENT_STARTED_AT ))
        fi
        local hook_fired="false"
        if [ -f "$_CV_MCP_LOG" ]; then
            if tail -n "+$((_CV_MCP_LOG_BASELINE + 1))" "$_CV_MCP_LOG" 2>/dev/null \
                | grep -q "SessionStart" 2>/dev/null; then
                hook_fired="true"
            fi
        fi
        # Outcome priority: SIGINT/SIGTERM beats everything (user intent).
        # Then "we never got far enough to invoke gemini" — distinct from
        # "we invoked gemini via passthrough but never started a tmux of
        # our own" (the latter is a normal direct-run, not an abort).
        local outcome
        if [ "$wrapper_exit_code" = "130" ] || [ "$wrapper_exit_code" = "143" ]; then
            outcome="interrupted"
        elif [ "$_CV_AGENT_INVOKED" = "false" ]; then
            outcome="pre_invoke_abort"
        elif [ "$gemini_exit" != "unknown" ] && [ "$gemini_exit" != "0" ]; then
            outcome="error_exit"
        elif [ "$gemini_lifetime" -lt 5 ] 2>/dev/null; then
            outcome="early_exit"
        elif [ "$gemini_lifetime" -lt 60 ] 2>/dev/null; then
            outcome="clean_short"
        else
            outcome="clean_long"
        fi
        cv_telem "wrapper_exited" "\"exit_code\":$wrapper_exit_code,\"lifetime_seconds\":$lifetime,\"gemini_exit_code\":\"$gemini_exit\",\"gemini_lifetime_seconds\":$gemini_lifetime,\"tmux_session_started\":$_CV_TMUX_STARTED,\"agent_invoked\":$_CV_AGENT_INVOKED,\"session_start_hook_fired\":$hook_fired,\"terminal_outcome\":\"$outcome\""
    fi

    if [ -n "$MCP_PID" ] && kill -0 "$MCP_PID" 2>/dev/null; then
        log "Stopping MCP server (PID: $MCP_PID)"
        kill "$MCP_PID" 2>/dev/null || true
        wait "$MCP_PID" 2>/dev/null || true
    fi
    # Remove PID file
    rm -f "${CODEVIBE_TMPDIR}/codevibe-gemini-mcp-$$.pid"
    rm -f "$_CV_GEMINI_EXIT_FILE"
}

# Set up trap for cleanup
trap cleanup EXIT INT TERM

# Check if tmux is installed
if ! command -v tmux &> /dev/null; then
    echo "Error: tmux is required but not installed."
    echo "Install with: brew install tmux"
    cv_failed "tmux_missing"
    sleep 1
    exit 1
fi

# Check if gemini is installed
if ! command -v gemini &> /dev/null; then
    echo "Error: gemini CLI is not installed."
    echo "Install from: https://github.com/google-gemini/gemini-cli"
    cv_failed "gemini_missing"
    sleep 1
    exit 1
fi

# Check if node is installed
if ! command -v node &> /dev/null; then
    echo "Error: Node.js is required but not installed."
    cv_failed "node_missing"
    sleep 1
    exit 1
fi

# Check if MCP server is built
if [ ! -f "$PLUGIN_DIR/dist/server.js" ]; then
    echo "Error: MCP server not built. Run 'npm run build' in the plugin directory first."
    cv_failed "server_not_built"
    sleep 1
    exit 1
fi

# Generate a unique session name
SESSION_NAME="${TMUX_SESSION_PREFIX}-$$"
WORKING_DIR="$(pwd)"

log "Starting codevibe-gemini with session: $SESSION_NAME"
log "Working directory: $WORKING_DIR"
log "Arguments: $*"

# Check if we're already inside tmux.
# We deliberately do NOT `exec` here — running gemini as a child process
# lets the EXIT trap fire after it returns so wrapper_exited still gets
# emitted on these direct-run paths. Behaviorally identical for the user
# (gemini remains the foreground process for the duration).
if [ -n "$TMUX" ]; then
    log "Already inside tmux, running gemini directly"
    _CV_AGENT_INVOKED="true"
    _CV_AGENT_STARTED_AT="$(date +%s)"
    # `|| _CV_RC=$?` is load-bearing: with `set -e`, a non-zero exit
    # from gemini would abort the wrapper before we capture the exit
    # code, leaving wrapper_exited with gemini_exit_code="unknown". The
    # `||` form catches non-zero without triggering set -e, while exit
    # 0 leaves _CV_RC at its 0 default. printf's `|| true` keeps a
    # disk-full failure from clobbering diagnostics.
    _CV_RC=0
    gemini "$@" || _CV_RC=$?
    printf '%s' "$_CV_RC" > "$_CV_GEMINI_EXIT_FILE" 2>/dev/null || true
    exit "$_CV_RC"
fi

# Check if running in a terminal — same direct-run treatment as above.
if [ ! -t 0 ] || [ ! -t 1 ]; then
    log "Not running in a terminal, running gemini directly"
    _CV_AGENT_INVOKED="true"
    _CV_AGENT_STARTED_AT="$(date +%s)"
    _CV_RC=0
    gemini "$@" || _CV_RC=$?
    printf '%s' "$_CV_RC" > "$_CV_GEMINI_EXIT_FILE" 2>/dev/null || true
    exit "$_CV_RC"
fi

# Start MCP server in background BEFORE launching Gemini
# The MCP server will watch the transcript file for changes
log "Starting MCP server..."
export GEMINI_WORKING_DIRECTORY="$WORKING_DIR"
export CODEVIBE_GEMINI_TMUX_SESSION="$SESSION_NAME"

# Start MCP server and capture its PID
node "$PLUGIN_DIR/dist/server.js" >> "$MCP_LOG_FILE" 2>&1 &
MCP_PID=$!
echo "$MCP_PID" > "${CODEVIBE_TMPDIR}/codevibe-gemini-mcp-$$.pid"

log "MCP server started with PID: $MCP_PID"

# Wait a moment for MCP server to initialize
sleep 1

# Check if MCP server is still running (exits if auth failed)
if ! kill -0 "$MCP_PID" 2>/dev/null; then
    log "ERROR: MCP server failed to start"
    # Show the last few lines of the log for context (e.g., auth error)
    echo ""
    tail -3 "$MCP_LOG_FILE" 2>/dev/null | grep -v '^\[' | head -1
    echo ""
    echo "Server failed to start. Check $MCP_LOG_FILE for details."
    cv_failed "server_died_on_startup"
    sleep 1
    exit 1
fi

# Create tmux session and run gemini
log "Creating tmux session: $SESSION_NAME"

# Build the gemini command with proper escaping
GEMINI_CMD="gemini"
for arg in "$@"; do
    # Escape single quotes in arguments
    escaped_arg=$(printf '%s' "$arg" | sed "s/'/'\\\\''/g")
    GEMINI_CMD="$GEMINI_CMD '$escaped_arg'"
done

# Create the session running gemini, then attach
# We use a wrapper that:
# 1. Exports the session name so prompts can find it
# 2. Runs gemini
# 3. Captures gemini's exit code to $_CV_GEMINI_EXIT_FILE for the
#    wrapper's cleanup trap (tmux's own attach exit code is independent
#    of the inner process exit, so the file is the only reliable signal)
# 4. Exits the tmux session when gemini exits

tmux new-session -d -s "$SESSION_NAME" -x "$(tput cols)" -y "$(tput lines)" \
    "export CODEVIBE_GEMINI_TMUX_SESSION='$SESSION_NAME'; export ENVIRONMENT='$ENVIRONMENT'; $GEMINI_CMD; printf '%s' \"\$?\" > '$_CV_GEMINI_EXIT_FILE'; exit"
_CV_TMUX_STARTED="true"
_CV_AGENT_INVOKED="true"
_CV_AGENT_STARTED_AT="$(date +%s)"

# Enable mouse support for scrolling
tmux set-option -t "$SESSION_NAME" -g mouse on

# Enable copy/paste with system clipboard (macOS: pbcopy, WSL: clip.exe, Linux: xclip)
tmux set-option -t "$SESSION_NAME" set-clipboard on
tmux set-window-option -t "$SESSION_NAME" mode-keys vi
if command -v pbcopy >/dev/null 2>&1; then
  CLIP_CMD="pbcopy"
elif grep -qi microsoft /proc/sys/kernel/osrelease 2>/dev/null && command -v clip.exe >/dev/null 2>&1; then
  CLIP_CMD="clip.exe"
elif command -v wl-copy >/dev/null 2>&1; then
  CLIP_CMD="wl-copy"
elif command -v xclip >/dev/null 2>&1; then
  CLIP_CMD="xclip -selection clipboard"
fi
if [ -n "${CLIP_CMD:-}" ]; then
  tmux bind-key -T copy-mode-vi y send-keys -X copy-pipe-and-cancel "$CLIP_CMD"
  tmux bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "$CLIP_CMD"
fi

# Store session mapping for mobile prompts
echo "$SESSION_NAME" > "${CODEVIBE_TMPDIR}/codevibe-gemini-tmux-session-$$"

log "Attaching to tmux session: $SESSION_NAME"

# Attach to the session
# This will show gemini's UI to the user
# When tmux session exits, the trap will clean up the MCP server
tmux attach-session -t "$SESSION_NAME"

# After tmux exits, cleanup is handled by trap
log "Tmux session ended"
