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

set -e

# Default to production environment if not specified
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="codex"
_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-codex-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_CODEX_EXIT_FILE="${CODEVIBE_TMPDIR}/codevibe-codex-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_CODEX_VER="missing"
command -v codex >/dev/null 2>&1 && _CV_CODEX_VER="$(codex --version 2>/dev/null | cv_sanitize)"
[ -z "$_CV_CODEX_VER" ] && _CV_CODEX_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_CODEX_AUTH="false"; [ -f "$HOME/.codex/auth.json" ] && _CV_CODEX_AUTH="true"
_CV_CODEX_CONFIG="false"; [ -f "$HOME/.codex/config.toml" ] && _CV_CODEX_CONFIG="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\",\"codex_version\":\"$_CV_CODEX_VER\",\"node_version\":\"$_CV_NODE_VER\",\"tmux_version\":\"$_CV_TMUX_VER\",\"codex_auth_present\":$_CV_CODEX_AUTH,\"codex_config_present\":$_CV_CODEX_CONFIG,\"inside_tmux\":$_CV_INSIDE_TMUX,\"is_terminal\":$_CV_IS_TTY"

# Configuration
TMUX_SESSION_PREFIX="codevibe-codex"
LOG_FILE="${CODEVIBE_TMPDIR}/codevibe-codex-wrapper.log"
MCP_LOG_FILE="${CODEVIBE_TMPDIR}/codevibe-codex-mcp.log"

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

# Cleanup function to kill 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 codex_exit="unknown"
        if [ -f "$_CV_CODEX_EXIT_FILE" ]; then
            codex_exit="$(cat "$_CV_CODEX_EXIT_FILE" 2>/dev/null | head -c 10 | tr -d '\n\r ')"
            [ -z "$codex_exit" ] && codex_exit="unknown"
        fi
        local lifetime=$(( $(date +%s) - _CV_STARTED_AT ))
        local codex_lifetime=0
        if [ "$_CV_AGENT_STARTED_AT" -gt 0 ] 2>/dev/null; then
            codex_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 codex" — distinct from
        # "we invoked codex 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 [ "$codex_exit" != "unknown" ] && [ "$codex_exit" != "0" ]; then
            outcome="error_exit"
        elif [ "$codex_lifetime" -lt 5 ] 2>/dev/null; then
            outcome="early_exit"
        elif [ "$codex_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,\"codex_exit_code\":\"$codex_exit\",\"codex_lifetime_seconds\":$codex_lifetime,\"tmux_session_started\":$_CV_TMUX_STARTED,\"agent_invoked\":$_CV_AGENT_INVOKED,\"session_start_hook_fired\":$hook_fired,\"terminal_outcome\":\"$outcome\""
    fi

    if [ -n "$SERVER_PID" ] && kill -0 "$SERVER_PID" 2>/dev/null; then
        log "Stopping server (PID: $SERVER_PID)"
        kill "$SERVER_PID" 2>/dev/null || true
        wait "$SERVER_PID" 2>/dev/null || true
    fi
    # Remove PID file and port file
    rm -f "${CODEVIBE_TMPDIR}/codevibe-codex-server-$$.pid"
    rm -f "${CODEVIBE_TMPDIR}/codevibe-codex-${SESSION_NAME}.port"
    rm -f "$_CV_CODEX_EXIT_FILE"

    # Remove our hooks from ~/.codex/hooks.json (idempotent, concurrent-safe).
    # Uses Node (always available — required to run the daemon) so users
    # without jq don't end up with stale CodeVibe hook entries leftover
    # after the wrapper exits. Ownership check is path-prefix + filename
    # tail (NOT a bare substring "codevibe-codex" match) so user paths
    # that happen to contain the string codevibe-codex (e.g. a username,
    # a directory like /Users/codevibe-codex/) cannot trigger removal of
    # the user's unrelated hooks.
    if [ -f "$HOME/.codex/hooks.json" ] && command -v node &> /dev/null; then
        # Only remove if no other codevibe-codex sessions are running
        if [ "$(pgrep -f 'codevibe-codex' | wc -l)" -le 1 ]; then
            log "Removing CodeVibe hooks from $HOME/.codex/hooks.json"
            # Cleanup mirrors install: same ownership predicate + the
            # same INNER-hooks[] split so a matcher entry mixing user
            # hooks with ours keeps the user hook. Same per-process
            # tmp suffix avoids two concurrent cleanups racing.
            if CV_HOOKS_FILE="$HOME/.codex/hooks.json" node -e '
              const fs = require("fs");
              const OUR_HOOK_FILES = new Set([
                "session-start.sh",
                "user-prompt.sh",
                "pre-tool-use.sh",
                "post-tool-use.sh",
                "stop.sh",
              ]);
              const OWN_PATH_MARKER = "codevibe-codex-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;
              };
              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 };
              };
              try {
                const target = process.env.CV_HOOKS_FILE;
                const data = JSON.parse(fs.readFileSync(target, "utf8"));
                if (data && typeof data === "object" && data.hooks && typeof data.hooks === "object") {
                  for (const k of Object.keys(data.hooks)) {
                    if (Array.isArray(data.hooks[k])) {
                      data.hooks[k] = data.hooks[k]
                        .map(stripOurHooksFromEntry)
                        .filter((entry) => entry !== null && entry !== undefined);
                    }
                  }
                }
                const tmp = target + "." + process.pid + "." + Date.now() + ".tmp";
                fs.writeFileSync(tmp, JSON.stringify(data, null, 2));
                fs.renameSync(tmp, target);
              } catch (e) {
                process.stderr.write("CLEAN_FAILED:" + (e && e.message ? e.message : String(e)));
                process.exit(1);
              }
            ' 2>>"$LOG_FILE"; then
              :
            else
              log "WARN: failed to clean CodeVibe hooks from hooks.json (left as-is)"
            fi
        else
            log "Other codevibe-codex sessions active, keeping hooks"
        fi
    fi
}

# 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 codex is installed
if ! command -v codex &> /dev/null; then
    echo "Error: codex CLI is not installed."
    echo "Install with: npm install -g @openai/codex"
    cv_failed "codex_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 server is built
if [ ! -f "$PLUGIN_DIR/dist/server.js" ]; then
    echo "Error: 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-codex 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 codex 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
# (codex remains the foreground process for the duration).
if [ -n "$TMUX" ]; then
    log "Already inside tmux, running codex directly"
    _CV_AGENT_INVOKED="true"
    _CV_AGENT_STARTED_AT="$(date +%s)"
    # `|| _CV_RC=$?` is load-bearing: with `set -e`, a non-zero exit from
    # codex would abort the wrapper before we capture the exit code,
    # leaving wrapper_exited with codex_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
    codex "$@" || _CV_RC=$?
    printf '%s' "$_CV_RC" > "$_CV_CODEX_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 codex directly"
    _CV_AGENT_INVOKED="true"
    _CV_AGENT_STARTED_AT="$(date +%s)"
    _CV_RC=0
    codex "$@" || _CV_RC=$?
    printf '%s' "$_CV_RC" > "$_CV_CODEX_EXIT_FILE" 2>/dev/null || true
    exit "$_CV_RC"
fi

# Start the session log watcher server in background
log "Starting session log watcher server..."
export CODEX_WORKING_DIRECTORY="$WORKING_DIR"
export CODEVIBE_CODEX_TMUX_SESSION="$SESSION_NAME"
export CODEVIBE_CODEX_PLUGIN_DIR="$PLUGIN_DIR"

# Install hooks.json for Codex CLI with absolute paths (idempotent)
# Codex CLI does NOT expand env vars in hook commands, so we must
# write absolute paths at runtime.
CODEX_HOOKS_FILE="$HOME/.codex/hooks.json"
HOOKS_DIR="$PLUGIN_DIR/hooks"

generate_hooks_json() {
  cat <<HOOKEOF
{
  "hooks": {
    "SessionStart": [{"matcher": "*", "hooks": [{"type": "command", "command": "bash ${HOOKS_DIR}/session-start.sh"}]}],
    "UserPromptSubmit": [{"matcher": "*", "hooks": [{"type": "command", "command": "bash ${HOOKS_DIR}/user-prompt.sh"}]}],
    "PreToolUse": [{"matcher": "*", "hooks": [{"type": "command", "command": "bash ${HOOKS_DIR}/pre-tool-use.sh"}]}],
    "PostToolUse": [{"matcher": "*", "hooks": [{"type": "command", "command": "bash ${HOOKS_DIR}/post-tool-use.sh"}]}],
    "Stop": [{"matcher": "*", "hooks": [{"type": "command", "command": "bash ${HOOKS_DIR}/stop.sh"}]}]
  }
}
HOOKEOF
}

# Ensure hooks feature flag is enabled in Codex config
CODEX_CONFIG="$HOME/.codex/config.toml"
if [ -f "$CODEX_CONFIG" ]; then
  if ! grep -q "codex_hooks" "$CODEX_CONFIG" 2>/dev/null; then
    echo -e '\n[features]\ncodex_hooks = true' >> "$CODEX_CONFIG"
    log "Enabled codex_hooks feature flag in config.toml"
  fi
else
  mkdir -p "$HOME/.codex"
  echo -e '[features]\ncodex_hooks = true' > "$CODEX_CONFIG"
  log "Created config.toml with codex_hooks feature flag"
fi

mkdir -p "$HOME/.codex"
# Hook installation outcome — feeds the hooks_install_outcome telemetry
# beacon below. Distinguishes fresh install, merge-into-existing,
# already-installed, and failure modes. Codex events flow ONLY through
# hooks (no JSONL fallback), so a silent skip here = a 0-event ghost
# session in DDB.
_CV_HOOKS_OUTCOME="unknown"
_CV_HOOKS_REASON=""

# Single Node-based hook installer: handles fresh install, idempotent
# merge into the user's existing hooks.json, and "already installed"
# detection — all with a stable ownership predicate.
#
# Design notes:
#
#  - "already installed" detection: a structured walk of the JSON
#    looking for our specific hook-script filenames at expected event
#    keys. Replaces the prior `grep -q "codevibe-codex"` substring
#    check, which false-positived on unrelated user paths containing
#    that string.
#
#  - Merge dedupe: filters our previously-installed entries out of
#    each event key's array, then appends the fresh template entries.
#    Idempotent — multiple runs (including concurrent ones) converge
#    to the same hooks.json. Filters at the INNER hooks[] item level
#    so a matcher entry that combines a user hook with one of ours
#    keeps the user hook (only the CodeVibe hooks get stripped, not
#    the surrounding entry).
#
#  - Ownership predicate: path-prefix `codevibe-codex-plugin/hooks/`
#    AND filename tail in the allow-list of our 5 hook script names.
#    Rejects user paths like /Users/codevibe-codex/foo.sh.
#
#  - Per-process tmp file (.<pid>.<time>.tmp): two concurrent installs
#    won't clobber each other's intermediate state. Final rename is
#    POSIX-atomic — last writer wins with content equivalent to what
#    the first writer would have produced (idempotency by construction).
#
# Wrapping in `if … ; then … else INSTALLER_RC=$? ; fi` is required to
# capture non-zero exit codes; under `set -e`, a bare `VAR=$(cmd)`
# followed by `RC=$?` would short-circuit on Node's non-zero exit
# before $? gets read, defeating the entire fail-closed mapping below.
GENERATED_HOOKS=$(generate_hooks_json)
if INSTALLER_OUTPUT=$(CV_EXISTING="$CODEX_HOOKS_FILE" \
                     CV_NEW="$GENERATED_HOOKS" \
                     node -e '
  const fs = require("fs");
  const OUR_HOOK_FILES = new Set([
    "session-start.sh",
    "user-prompt.sh",
    "pre-tool-use.sh",
    "post-tool-use.sh",
    "stop.sh",
  ]);
  const OWN_PATH_MARKER = "codevibe-codex-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 (whole entry should be
  // dropped); otherwise returns a new entry with only the user-owned
  // hooks. Returns the original entry unchanged if it contains none of
  // ours (preserves identity for unrelated entries).
  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 };
  };
  // Does this matcher entry contain at least one of our hooks anywhere
  // in its inner hooks[]? Used by the "already installed" walk only —
  // it does NOT decide whether the entry survives a strip.
  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 mvs the others half-written file.
  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*)
      log "CodeVibe hooks already installed in $CODEX_HOOKS_FILE"
      _CV_HOOKS_OUTCOME="already_installed"
      ;;
    OUTCOME:merged*)
      log "Merged CodeVibe hooks into existing $CODEX_HOOKS_FILE"
      _CV_HOOKS_OUTCOME="merged"
      ;;
    OUTCOME:fresh_install*)
      log "Installed CodeVibe hooks to $CODEX_HOOKS_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
  log "ERROR: hooks installer failed (rc=$INSTALLER_RC, reason=$_CV_HOOKS_REASON)"
fi
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 —
# Codex 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 into $CODEX_HOOKS_FILE ($_CV_HOOKS_OUTCOME)."
    echo "       Without hooks installed, Codex 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

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

log "Server started with PID: $SERVER_PID"

# Wait a moment for server to initialize
sleep 1

# Check if server is still running (exits if auth failed)
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
    log "ERROR: 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 codex
log "Creating tmux session: $SESSION_NAME"

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

# Create the session running codex.
# The inner shell writes codex's exit code to $_CV_CODEX_EXIT_FILE so the
# wrapper's cleanup trap can report it via `wrapper_exited` telemetry —
# tmux's own attach exit code is independent of the inner process exit.
tmux new-session -d -s "$SESSION_NAME" -x "$(tput cols)" -y "$(tput lines)" \
    "export CODEVIBE_CODEX_TMUX_SESSION='$SESSION_NAME'; export ENVIRONMENT='$ENVIRONMENT'; $CODEX_CMD; printf '%s' \"\$?\" > '$_CV_CODEX_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
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-codex-tmux-session-$$"

log "Attaching to tmux session: $SESSION_NAME"

# Attach to the session
tmux attach-session -t "$SESSION_NAME"

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