#!/usr/bin/env bash
set -e

# Show help if requested
show_help() {
  cat << 'EOF'
Usage: ai-run [tool] [options] [-- tool-args...]

Run AI tools in a secure Docker sandbox.
When no tool is specified, opens an interactive shell with all installed tools.

Options:
  -s, --shell              Start interactive shell instead of running tool directly
  -n, --network [name]     Connect to Docker network(s) (comma-separated or interactive)
  -e, --expose <ports>     Expose container ports (comma-separated, e.g., 3000,5555)
  -p, --password <value>   Set OpenCode server password (for web/serve mode)
      --password-env <VAR> Read OpenCode server password from environment variable
      --allow-unsecured    Allow OpenCode server to run without password (suppresses warning)
      --git-fetch          Enable git fetch-only mode (blocks push)
      --no-nano-brain-auto-repair
                            Disable nano-brain preflight/auto-repair logic
  -h, --help               Show this help message
      --help-env           Show environment variables reference

OpenCode Server Authentication (web/serve mode only):
  When running 'opencode web' or 'opencode serve', you can control authentication:

  # Set password directly (visible in process list)
  ai-run opencode web --password mysecret

  # Read password from environment variable (more secure)
  MY_PASS=secret ai-run opencode web --password-env MY_PASS

  # Explicitly allow unsecured mode (suppresses warning)
  ai-run opencode web --allow-unsecured

  Default username: opencode (override with OPENCODE_SERVER_USERNAME env var)
  Precedence: --password > --password-env > OPENCODE_SERVER_PASSWORD env > interactive prompt

Examples:
  ai-run                                  # Open interactive shell with all tools
  ai-run claude                           # Run Claude in sandbox
  ai-run opencode web -e 4096             # Run OpenCode web with port exposed
  ai-run opencode web -p secret           # Run with password
  ai-run opencode --shell                 # Start shell, run tool manually
  ai-run aider -n mynetwork               # Connect to Docker network
  ai-run opencode --git-fetch             # Git fetch only (no push)

Documentation: https://github.com/nano-step/ai-sandbox-wrapper
EOF
  exit 0
}

show_help_env() {
  cat << 'EOF'
Environment Variables Reference
===============================

Runtime Environment Variables (inline with ai-run):
---------------------------------------------------
  AI_IMAGE_SOURCE=registry    Use pre-built images from container registry instead of local
                              Example: AI_IMAGE_SOURCE=registry ai-run opencode

  AI_IMAGE_REGISTRY=...       Override the registry/image path used when AI_IMAGE_SOURCE=registry
                              Default: ghcr.io/nano-step/ai-opencode
                              Example: AI_IMAGE_REGISTRY=registry.gitlab.com/kokorolee/ai-sandbox-wrapper/ai-sandbox ai-run opencode

  AI_IMAGE_TAG=...            Override the image tag used when AI_IMAGE_SOURCE=registry
                              Default: base   (options: base, full, base-sha-<short>, base-v<x.y.z>, full-sha-<short>, full-v<x.y.z>)
                              Example: AI_IMAGE_TAG=full ai-run opencode

  AI_RUN_DEBUG=1              Enable debug output (shows Docker command details)
                              Example: AI_RUN_DEBUG=1 ai-run opencode

  AI_RUN_PLATFORM=linux/amd64 Force specific platform (useful for cross-arch)
                              Example: AI_RUN_PLATFORM=linux/amd64 ai-run opencode

  AI_RUN_DISABLE_NANO_BRAIN_AUTO_REPAIR=1
                              Disable nano-brain preflight and automatic repair behavior
                              Example: AI_RUN_DISABLE_NANO_BRAIN_AUTO_REPAIR=1 ai-run npx nano-brain status

  PORT_BIND=all               Bind exposed ports to all interfaces (0.0.0.0) instead of localhost
                              ⚠️  Security risk - use only when needed
                              Example: PORT_BIND=all ai-run opencode web -e 4096

  PORT=3000,4000              (Deprecated) Expose ports - use --expose flag instead
                              Example: PORT=3000 ai-run opencode

Build Environment Variables (inline with setup.sh or install scripts):
----------------------------------------------------------------------
  DOCKER_NO_CACHE=1           Force rebuild without Docker cache
                              Example: DOCKER_NO_CACHE=1 bash setup.sh

  OPENCODE_VERSION=1.1.52     Install specific OpenCode version instead of latest
                              Example: OPENCODE_VERSION=1.1.52 bash lib/install-opencode.sh

  INSTALL_CHROME_DEVTOOLS_MCP=1  Install Chrome DevTools MCP in base image
  INSTALL_PLAYWRIGHT_MCP=1       Install Playwright MCP in base image
  INSTALL_PLAYWRIGHT=1           Install Playwright browser automation
  INSTALL_RUBY=1                 Install Ruby 3.3.0 + Rails 8.0.2
  INSTALL_SPEC_KIT=1             Install spec-kit (specify CLI)
  INSTALL_UX_UI_PROMAX=1         Install uipro CLI
  INSTALL_OPENSPEC=1             Install OpenSpec CLI
  INSTALL_ACLI=1                 Install Atlassian CLI (acli) for Jira/Confluence/Bitbucket

Container Environment Variables (passed to container via ~/.ai-sandbox/env):
-----------------------------------------------------------------------------
  ANTHROPIC_API_KEY           Anthropic API key for Claude
  OPENAI_API_KEY              OpenAI API key
  OPENCODE_SERVER_PASSWORD    Password for OpenCode web server
  OPENCODE_SERVER_USERNAME    Username for OpenCode web server (default: opencode)
  GITHUB_TOKEN                GitHub token for gh CLI authentication
  GH_TOKEN                    Alternative GitHub token variable

Configuration Files:
--------------------
  ~/.ai-sandbox/config.json   Main configuration (workspaces, git, networks, MCP)
  ~/.ai-sandbox/env           API keys and secrets (KEY=value format)
  ~/.ai-sandbox/workspaces    Legacy workspace list (one path per line)

Examples:
  # Debug mode with registry images
  AI_RUN_DEBUG=1 AI_IMAGE_SOURCE=registry ai-run opencode

  # Rebuild everything from scratch
  DOCKER_NO_CACHE=1 bash setup.sh --no-cache

  # Install specific OpenCode version
  OPENCODE_VERSION=1.1.52 bash lib/install-opencode.sh

  # Expose port to network (not just localhost)
  PORT_BIND=all ai-run opencode web -e 4096
EOF
  exit 0
}

# Check for help flag before anything else
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
  show_help
fi

if [[ "${1:-}" == "--help-env" ]]; then
  show_help_env
fi

TOOL="${1:-}"
if [[ -n "$TOOL" && ! "$TOOL" =~ ^- ]]; then
  shift
else
  TOOL=""
fi

# ────────────────────────────────────────────────────────────────
# OPEN-DESIGN SERVICE-TYPE TOOL DISPATCHER
# Unlike ephemeral CLI tools, open-design is a long-running daemon.
# Routes init/start/stop/restart/status/logs subcommands and exits.
# ────────────────────────────────────────────────────────────────
if [[ "$TOOL" == "open-design" ]]; then
  OD_CONTAINER_NAME="ai-open-design"
  OD_IMAGE="ai-open-design:latest"
  OD_NETWORK="ai-sandbox"
  OD_VOLUME="ai-open-design-data"
  OD_ENV_FILE="$HOME/.ai-sandbox/env"
  OD_DEFAULT_URL="http://ai-open-design:7456"

  od_print_help() {
    cat <<HLP
Usage: ai-run open-design <subcommand> [options]

Subcommands:
  init [--force]              One-time setup: generate API token, create network/volume
  start [--expose] [--port N] Boot daemon (detached). --expose publishes port to host
  stop                        Stop daemon (preserves container for restart)
  restart [start-flags...]    Stop then start
  status                      Show daemon state, network, port, token, health
  logs [-f|--follow]          Show daemon logs (-f to follow)
  --help, -h                  Show this help

Examples:
  ai-run open-design init
  ai-run open-design start
  ai-run open-design start --expose            # publishes 7456 to host
  ai-run open-design start --expose --port 17456
  ai-run open-design status
HLP
  }

  od_env_has_token() {
    [[ -f "$OD_ENV_FILE" ]] && grep -q "^OD_API_TOKEN=" "$OD_ENV_FILE"
  }

  od_read_token() {
    [[ -f "$OD_ENV_FILE" ]] && grep "^OD_API_TOKEN=" "$OD_ENV_FILE" | head -1 | cut -d= -f2-
  }

  od_init() {
    local force=false
    if [[ "${1:-}" == "--force" ]]; then
      force=true
    fi

    mkdir -p "$(dirname "$OD_ENV_FILE")"
    touch "$OD_ENV_FILE"
    chmod 600 "$OD_ENV_FILE"

    if od_env_has_token && [[ "$force" != "true" ]]; then
      echo "ℹ️  OD_API_TOKEN already set in $OD_ENV_FILE — nothing to do"
      echo "    Use 'ai-run open-design init --force' to regenerate (will invalidate running sessions)"
    else
      if [[ "$force" == "true" ]] && od_env_has_token; then
        printf "⚠️  This will replace the existing OD_API_TOKEN. Continue? [y/N] "
        read -r confirm
        if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
          echo "Aborted."
          exit 0
        fi
        # Strip existing OD_API_TOKEN line
        if [[ "$(uname)" == "Darwin" ]]; then
          sed -i '' '/^OD_API_TOKEN=/d' "$OD_ENV_FILE"
        else
          sed -i '/^OD_API_TOKEN=/d' "$OD_ENV_FILE"
        fi
      fi
      if ! command -v openssl >/dev/null 2>&1; then
        echo "❌ ERROR: 'openssl' is required to generate a token" >&2
        exit 1
      fi
      local token
      token="$(openssl rand -hex 32)"
      # Ensure trailing newline before append to avoid joining with previous line
      if [[ -s "$OD_ENV_FILE" && -n "$(tail -c 1 "$OD_ENV_FILE" 2>/dev/null)" ]]; then
        echo "" >> "$OD_ENV_FILE"
      fi
      echo "OD_API_TOKEN=$token" >> "$OD_ENV_FILE"
      echo "✅ Generated OD_API_TOKEN (256-bit, written to $OD_ENV_FILE)"
    fi

    # Ensure OD_DAEMON_URL line
    if ! grep -q "^OD_DAEMON_URL=" "$OD_ENV_FILE"; then
      if [[ -s "$OD_ENV_FILE" && -n "$(tail -c 1 "$OD_ENV_FILE" 2>/dev/null)" ]]; then
        echo "" >> "$OD_ENV_FILE"
      fi
      echo "OD_DAEMON_URL=$OD_DEFAULT_URL" >> "$OD_ENV_FILE"
      echo "✅ Set OD_DAEMON_URL=$OD_DEFAULT_URL in $OD_ENV_FILE"
    fi

    chmod 600 "$OD_ENV_FILE"

    # Ensure network
    if ensure_network "$OD_NETWORK"; then
      echo "✅ Docker network '$OD_NETWORK' ready"
    fi

    # Ensure volume
    if ! docker volume inspect "$OD_VOLUME" >/dev/null 2>&1; then
      docker volume create "$OD_VOLUME" >/dev/null
      echo "✅ Docker volume '$OD_VOLUME' created"
    else
      echo "ℹ️  Docker volume '$OD_VOLUME' already exists"
    fi

    echo ""
    echo "Next: ai-run open-design start"
  }

  od_start() {
    local expose=false
    local host_port=7456

    while [[ $# -gt 0 ]]; do
      case "$1" in
        --expose) expose=true; shift ;;
        --port)
          shift
          if [[ $# -gt 0 && "$1" =~ ^[0-9]+$ ]]; then
            if (( $1 < 1 || $1 > 65535 )); then
              echo "❌ ERROR: --port value '$1' out of range (1-65535)" >&2; exit 1
            fi
            host_port="$1"
            shift
          else
            echo "❌ ERROR: --port requires a numeric value (e.g. 7456)" >&2; exit 1
          fi
          ;;
        *) echo "❌ ERROR: unknown start flag: $1" >&2; exit 1 ;;
      esac
    done

    if ! od_env_has_token; then
      echo "❌ ERROR: OD_API_TOKEN not found in $OD_ENV_FILE"
      echo "    Run: ai-run open-design init"
      exit 1
    fi

    if ! docker image inspect "$OD_IMAGE" >/dev/null 2>&1; then
      echo "❌ ERROR: image '$OD_IMAGE' not built"
      echo "    Run: bash lib/install-open-design.sh"
      exit 1
    fi

    ensure_network "$OD_NETWORK" || exit 1
    docker volume inspect "$OD_VOLUME" >/dev/null 2>&1 || docker volume create "$OD_VOLUME" >/dev/null

    # If a container with this name exists, handle it gracefully
    if docker ps -a --format '{{.Names}}' | grep -q "^${OD_CONTAINER_NAME}$"; then
      if docker ps --format '{{.Names}}' | grep -q "^${OD_CONTAINER_NAME}$"; then
        echo "ℹ️  '$OD_CONTAINER_NAME' is already running"
        echo "    Use 'ai-run open-design restart' to apply new flags"
        return 0
      else
        echo "🔄 Removing stopped container '$OD_CONTAINER_NAME' to recreate with current flags..."
        docker rm "$OD_CONTAINER_NAME" >/dev/null
      fi
    fi

    local run_args=(
      run -d
      --name "$OD_CONTAINER_NAME"
      --network "$OD_NETWORK"
      --restart unless-stopped
      -v "$OD_VOLUME:/app/.od"
      --env-file "$OD_ENV_FILE"
      -p "127.0.0.1:${host_port}:7456"
    )

    if [[ "$expose" == "true" ]]; then
      echo "ℹ️  Port ${host_port} already published by default (127.0.0.1:${host_port}:7456)"
    fi

    run_args+=("$OD_IMAGE")

    echo "🔄 Starting $OD_CONTAINER_NAME..."
    docker "${run_args[@]}" >/dev/null
    echo "✅ $OD_CONTAINER_NAME running on network '$OD_NETWORK'"
    echo "   Published to host: http://localhost:${host_port}"
  }

  od_stop() {
    if docker ps --format '{{.Names}}' | grep -q "^${OD_CONTAINER_NAME}$"; then
      echo "🔄 Stopping $OD_CONTAINER_NAME..."
      docker stop "$OD_CONTAINER_NAME" >/dev/null
      echo "✅ $OD_CONTAINER_NAME stopped (data preserved in volume '$OD_VOLUME')"
    else
      echo "ℹ️  $OD_CONTAINER_NAME is not running"
    fi
  }

  od_restart() {
    od_stop
    od_start "$@"
  }

  od_status() {
    echo "Container : $OD_CONTAINER_NAME"
    if docker ps --format '{{.Names}}' | grep -q "^${OD_CONTAINER_NAME}$"; then
      echo "State     : running"
      local uptime
      uptime="$(docker inspect -f '{{.State.StartedAt}}' "$OD_CONTAINER_NAME" 2>/dev/null || echo unknown)"
      echo "Started   : $uptime"
      local ports
      ports="$(docker inspect -f '{{json .NetworkSettings.Ports}}' "$OD_CONTAINER_NAME" 2>/dev/null || echo '{}')"
      echo "Ports     : $ports"
    elif docker ps -a --format '{{.Names}}' | grep -q "^${OD_CONTAINER_NAME}$"; then
      echo "State     : stopped"
      echo "   Hint   : ai-run open-design start"
    else
      echo "State     : not installed"
      echo "   Hint   : ai-run open-design init && ai-run open-design start"
    fi

    echo "Network   : $OD_NETWORK"
    echo "Volume    : $OD_VOLUME"
    if od_env_has_token; then
      local tok
      tok="$(od_read_token)"
      echo "API token : set (***${tok: -4})"
    else
      echo "API token : (unset — run 'ai-run open-design init')"
    fi

    # Try health check (only meaningful if container is running)
    if docker ps --format '{{.Names}}' | grep -q "^${OD_CONTAINER_NAME}$"; then
      # Try in-container curl first (fast path); fall back to one-off container on same network
      # because upstream daemon image may not bundle curl
      local health_ok=false
      if docker exec "$OD_CONTAINER_NAME" sh -c 'command -v curl' >/dev/null 2>&1; then
        if docker exec "$OD_CONTAINER_NAME" curl -sf --max-time 3 http://127.0.0.1:7456/api/health >/dev/null 2>&1; then
          health_ok=true
        fi
      else
        # Fallback: probe via a tiny one-off container in the same network
        if docker run --rm --network "$OD_NETWORK" curlimages/curl:latest \
            -sf --max-time 3 "http://${OD_CONTAINER_NAME}:7456/api/health" >/dev/null 2>&1; then
          health_ok=true
        fi
      fi
      if [[ "$health_ok" == "true" ]]; then
        echo "Health    : OK"
      else
        echo "Health    : FAIL (daemon not responding on /api/health)"
      fi
    fi
  }

  od_logs() {
    if ! docker ps -a --format '{{.Names}}' | grep -q "^${OD_CONTAINER_NAME}$"; then
      echo "❌ ERROR: container '$OD_CONTAINER_NAME' does not exist" >&2
      exit 1
    fi
    docker logs "$@" "$OD_CONTAINER_NAME"
  }

  # Dispatch subcommand
  SUBCMD="${1:-}"
  if [[ -n "$SUBCMD" ]]; then shift; fi
  case "$SUBCMD" in
    init)    od_init "$@" ;;
    start)   od_start "$@" ;;
    stop)    od_stop ;;
    restart) od_restart "$@" ;;
    status)  od_status ;;
    logs)    od_logs "$@" ;;
    --help|-h|"") od_print_help ;;
    *)
      echo "❌ ERROR: unknown subcommand '$SUBCMD'" >&2
      echo ""
      od_print_help
      exit 1
      ;;
  esac
  exit 0
fi
# ────────────────────────────────────────────────────────────────
# END OPEN-DESIGN DISPATCHER
# ────────────────────────────────────────────────────────────────

# Handle no tool specified
if [[ -z "$TOOL" ]]; then
  if [[ ! -t 0 ]] || [[ ! -t 1 ]]; then
    echo "❌ ERROR: No tool specified and no TTY available."
    echo "   Use: ai-run <tool> [args...]"
    exit 1
  fi
  SHELL_MODE=true
fi

# Parse flags
# Note: SHELL_MODE may already be true if no tool was specified
SHELL_MODE="${SHELL_MODE:-false}"
NETWORK_FLAG=false
NETWORK_ARG=""
EXPOSE_ARG=""
SERVER_PASSWORD=""
PASSWORD_ENV_VAR=""
ALLOW_UNSECURED=false
GIT_FETCH_ONLY_FLAG=false
NANO_BRAIN_AUTO_REPAIR=true
TOOL_ARGS=()

if [[ "${AI_RUN_DISABLE_NANO_BRAIN_AUTO_REPAIR:-}" == "1" ]]; then
  NANO_BRAIN_AUTO_REPAIR=false
fi

while [[ $# -gt 0 ]]; do
  case "$1" in
    --shell|-s)
      SHELL_MODE=true
      shift
      ;;
    --network|-n)
      NETWORK_FLAG=true
      shift
      # Check if next arg is a network name (not another flag)
      if [[ $# -gt 0 && ! "$1" =~ ^- ]]; then
        NETWORK_ARG="$1"
        shift
      fi
      ;;
    --expose|-e)
      shift
      if [[ $# -gt 0 && ! "$1" =~ ^- ]]; then
        EXPOSE_ARG="$1"
        shift
      fi
      ;;
    --password|-p)
      shift
      if [[ $# -gt 0 && ! "$1" =~ ^- ]]; then
        SERVER_PASSWORD="$1"
        shift
      fi
      ;;
    --password-env)
      shift
      if [[ $# -gt 0 && ! "$1" =~ ^- ]]; then
        PASSWORD_ENV_VAR="$1"
        shift
      fi
      ;;
    --allow-unsecured)
      ALLOW_UNSECURED=true
      shift
      ;;
    --git-fetch)
      GIT_FETCH_ONLY_FLAG=true
      shift
      ;;
    --no-nano-brain-auto-repair)
      NANO_BRAIN_AUTO_REPAIR=false
      shift
      ;;
    *)
      TOOL_ARGS+=("$1")
      shift
      ;;
  esac
done

# ============================================================================
# MIGRATION V1: Move old .ai-* paths to ~/.ai-sandbox/
# ============================================================================
migrate_to_sandbox() {
  local SANDBOX_DIR="$HOME/.ai-sandbox"
  local MIGRATED_MARKER="$SANDBOX_DIR/.migrated"

  # Skip if already migrated
  [[ -f "$MIGRATED_MARKER" ]] && return 0

  # Check if any old paths exist
  local needs_migration=false
  [[ -d "$HOME/.ai-cache" ]] && needs_migration=true
  [[ -d "$HOME/.ai-home" ]] && needs_migration=true
  [[ -f "$HOME/.ai-workspaces" ]] && needs_migration=true
  [[ -f "$HOME/.ai-env" ]] && needs_migration=true
  [[ -f "$HOME/.ai-git-allowed" ]] && needs_migration=true

  # Check for git-keys files
  for keyfile in "$HOME"/.ai-git-keys-*; do
    [[ -f "$keyfile" ]] && needs_migration=true && break
  done

  [[ "$needs_migration" == "false" ]] && return 0

  echo "🔄 Migrating AI Sandbox configuration to ~/.ai-sandbox/..."
  mkdir -p "$SANDBOX_DIR"

  # Migrate directories
  if [[ -d "$HOME/.ai-cache" ]]; then
    mv "$HOME/.ai-cache" "$SANDBOX_DIR/cache" 2>/dev/null && echo "  ✓ Migrated cache"
  fi
  if [[ -d "$HOME/.ai-home" ]]; then
    mv "$HOME/.ai-home" "$SANDBOX_DIR/home" 2>/dev/null && echo "  ✓ Migrated home"
  fi

  # Migrate files
  if [[ -f "$HOME/.ai-workspaces" ]]; then
    mv "$HOME/.ai-workspaces" "$SANDBOX_DIR/workspaces" 2>/dev/null && echo "  ✓ Migrated workspaces"
  fi
  if [[ -f "$HOME/.ai-env" ]]; then
    mv "$HOME/.ai-env" "$SANDBOX_DIR/env" 2>/dev/null && echo "  ✓ Migrated env"
  fi
  if [[ -f "$HOME/.ai-git-allowed" ]]; then
    mv "$HOME/.ai-git-allowed" "$SANDBOX_DIR/git-allowed" 2>/dev/null && echo "  ✓ Migrated git-allowed"
  fi

  # Migrate git-keys (multiple files with pattern .ai-git-keys-*)
  local git_keys_found=false
  for keyfile in "$HOME"/.ai-git-keys-*; do
    [[ -f "$keyfile" ]] || continue
    git_keys_found=true
    mkdir -p "$SANDBOX_DIR/git-keys"
    local basename=$(basename "$keyfile")
    local newname="${basename#.ai-git-keys-}"
    mv "$keyfile" "$SANDBOX_DIR/git-keys/$newname" 2>/dev/null
  done
  [[ "$git_keys_found" == "true" ]] && echo "  ✓ Migrated git-keys"

  # Create marker
  date -Iseconds > "$MIGRATED_MARKER" 2>/dev/null || date > "$MIGRATED_MARKER"
  echo "✅ Migration complete!"
}

migrate_to_sandbox

# ============================================================================
# MIGRATION V2: Reorganize to tool-centric folder structure
# home/<tool>/ → tools/<tool>/home/
# cache/<tool>/ → tools/<tool>/cache/
# cache/git/ → shared/git/
# git-keys/ → shared/git/keys/
# ============================================================================
migrate_to_v2() {
  local SANDBOX_DIR="$HOME/.ai-sandbox"
  local V2_MARKER="$SANDBOX_DIR/.migrated-v2"

  # Skip if already migrated to v2
  [[ -f "$V2_MARKER" ]] && return 0

  # Check if any v1 paths exist that need migration
  local needs_migration=false
  [[ -d "$SANDBOX_DIR/home" ]] && needs_migration=true
  [[ -d "$SANDBOX_DIR/cache" ]] && needs_migration=true
  [[ -d "$SANDBOX_DIR/git-keys" ]] && needs_migration=true

  [[ "$needs_migration" == "false" ]] && { mkdir -p "$SANDBOX_DIR" && touch "$V2_MARKER"; return 0; }

  echo "🔄 Migrating to v2 folder structure..."
  mkdir -p "$SANDBOX_DIR/tools" "$SANDBOX_DIR/shared/git"

  # Migrate per-tool home directories: home/<tool>/ → tools/<tool>/home/
  if [[ -d "$SANDBOX_DIR/home" ]]; then
    for tool_home in "$SANDBOX_DIR/home"/*/; do
      [[ -d "$tool_home" ]] || continue
      local tool_name=$(basename "$tool_home")
      local new_path="$SANDBOX_DIR/tools/$tool_name/home"
      if [[ ! -d "$new_path" ]]; then
        mkdir -p "$SANDBOX_DIR/tools/$tool_name"
        mv "$tool_home" "$new_path" 2>/dev/null && echo "  ✓ Migrated home/$tool_name → tools/$tool_name/home"
      fi
      # Ensure common subdirectories exist and permissions are set
      mkdir -p "$new_path/.config" "$new_path/.local/share" "$new_path/.cache" "$new_path/.bun"
    done
    # Remove empty home dir
    rmdir "$SANDBOX_DIR/home" 2>/dev/null || true
  fi

  # Migrate per-tool cache directories: cache/<tool>/ → tools/<tool>/home/.cache/
  # But skip cache/git - that goes to shared/git
  if [[ -d "$SANDBOX_DIR/cache" ]]; then
    for tool_cache in "$SANDBOX_DIR/cache"/*/; do
      [[ -d "$tool_cache" ]] || continue
      local tool_name=$(basename "$tool_cache")
      # Skip git - handled separately
      [[ "$tool_name" == "git" ]] && continue
      local new_path="$SANDBOX_DIR/tools/$tool_name/home/.cache"
      if [[ ! -d "$new_path" ]]; then
        mkdir -p "$(dirname "$new_path")"
        mv "$tool_cache" "$new_path" 2>/dev/null && echo "  ✓ Migrated cache/$tool_name → tools/$tool_name/home/.cache"
      fi
    done
  fi

  # Migrate cache/git/ → shared/git/ (merge contents)
  if [[ -d "$SANDBOX_DIR/cache/git" ]]; then
    cp -r "$SANDBOX_DIR/cache/git/"* "$SANDBOX_DIR/shared/git/" 2>/dev/null && echo "  ✓ Migrated cache/git → shared/git"
    rm -rf "$SANDBOX_DIR/cache/git"
    # Remove empty cache dir
    rmdir "$SANDBOX_DIR/cache" 2>/dev/null || true
  fi

  # Migrate git-keys/ → shared/git/keys/
  if [[ -d "$SANDBOX_DIR/git-keys" ]]; then
    mkdir -p "$SANDBOX_DIR/shared/git/keys"
    cp -r "$SANDBOX_DIR/git-keys/"* "$SANDBOX_DIR/shared/git/keys/" 2>/dev/null && echo "  ✓ Migrated git-keys → shared/git/keys"
    rm -rf "$SANDBOX_DIR/git-keys"
  fi

  # Create v2 marker
  date -Iseconds > "$V2_MARKER" 2>/dev/null || date > "$V2_MARKER"
  echo "✅ V2 migration complete!"
}

migrate_to_v2

# ============================================================================
# PATH CONFIGURATION (consolidated under ~/.ai-sandbox/)
# ============================================================================
SANDBOX_DIR="$HOME/.ai-sandbox"
mkdir -p "$SANDBOX_DIR"

WORKSPACES_FILE="$SANDBOX_DIR/workspaces"
AI_SANDBOX_CONFIG="$SANDBOX_DIR/config.json"
CURRENT_DIR="$(pwd)"
ENV_FILE="$SANDBOX_DIR/env"

# Ensure jq is available (fallback to basic parsing if not)
has_jq() {
  command -v jq &>/dev/null
}

# Read workspaces from config.json or fallback to legacy file
read_workspaces() {
  if has_jq && [[ -f "$AI_SANDBOX_CONFIG" ]]; then
    jq -r '.workspaces[]?' "$AI_SANDBOX_CONFIG" 2>/dev/null
  elif [[ -f "$WORKSPACES_FILE" ]]; then
    cat "$WORKSPACES_FILE"
  fi
}

# Add workspace to config.json (and legacy file for backward compatibility)
add_workspace() {
  local ws_path="$1"

  # Add to legacy file for backward compatibility
  echo "$ws_path" >> "$WORKSPACES_FILE"

  # Add to config.json if jq available
  if has_jq && [[ -f "$AI_SANDBOX_CONFIG" ]]; then
    jq --arg ws "$ws_path" '.workspaces += [$ws] | .workspaces |= unique' "$AI_SANDBOX_CONFIG" > "$AI_SANDBOX_CONFIG.tmp" \
      && mv "$AI_SANDBOX_CONFIG.tmp" "$AI_SANDBOX_CONFIG"
    chmod 600 "$AI_SANDBOX_CONFIG"
  fi
}

# Migrate legacy workspaces and git-allowed files to config.json
migrate_legacy_to_config() {
  if ! has_jq; then
    return 0  # Can't migrate without jq
  fi

  local LEGACY_WORKSPACES="$SANDBOX_DIR/workspaces"
  local LEGACY_GIT_ALLOWED="$SANDBOX_DIR/git-allowed"

  # Migrate workspaces file
  if [[ -f "$LEGACY_WORKSPACES" ]]; then
    local ws_array="[]"
    while IFS= read -r ws; do
      [[ -n "$ws" ]] && ws_array=$(echo "$ws_array" | jq --arg w "$ws" '. + [$w]')
    done < "$LEGACY_WORKSPACES"

    jq --argjson ws "$ws_array" '.workspaces = $ws' "$AI_SANDBOX_CONFIG" > "$AI_SANDBOX_CONFIG.tmp" \
      && mv "$AI_SANDBOX_CONFIG.tmp" "$AI_SANDBOX_CONFIG"
    chmod 600 "$AI_SANDBOX_CONFIG"
  fi

  # Migrate git-allowed file
  if [[ -f "$LEGACY_GIT_ALLOWED" ]]; then
    local git_array="[]"
    while IFS= read -r ws; do
      [[ -n "$ws" ]] && git_array=$(echo "$git_array" | jq --arg w "$ws" '. + [$w]')
    done < "$LEGACY_GIT_ALLOWED"

    jq --argjson gw "$git_array" '.git.allowedWorkspaces = $gw' "$AI_SANDBOX_CONFIG" > "$AI_SANDBOX_CONFIG.tmp" \
      && mv "$AI_SANDBOX_CONFIG.tmp" "$AI_SANDBOX_CONFIG"
    chmod 600 "$AI_SANDBOX_CONFIG"
  fi
}

# Upgrade v1 config to v2 schema
upgrade_config_to_v2() {
  if ! has_jq; then
    return 0
  fi

  # Add missing v2 fields while preserving existing networks
  jq '.version = 2 | .workspaces = (.workspaces // []) | .git = (.git // {"allowedWorkspaces":[],"keySelections":{}}) | .git.fetchOnlyWorkspaces = (.git.fetchOnlyWorkspaces // [])' \
    "$AI_SANDBOX_CONFIG" > "$AI_SANDBOX_CONFIG.tmp" \
    && mv "$AI_SANDBOX_CONFIG.tmp" "$AI_SANDBOX_CONFIG"
  chmod 600 "$AI_SANDBOX_CONFIG"

  # Migrate legacy files after upgrade
  migrate_legacy_to_config
}

# Initialize config file if it doesn't exist or upgrade to v2
init_config() {
  mkdir -p "$(dirname "$AI_SANDBOX_CONFIG")"

  if [[ ! -f "$AI_SANDBOX_CONFIG" ]]; then
    # Create new v2 config
    local initial_config='{"version":2,"workspaces":[],"git":{"allowedWorkspaces":[],"fetchOnlyWorkspaces":[],"keySelections":{}},"networks":{"global":[],"workspaces":{}}}'
    echo "$initial_config" > "$AI_SANDBOX_CONFIG"
    chmod 600 "$AI_SANDBOX_CONFIG"

    # Migrate legacy files if they exist
    migrate_legacy_to_config
  elif has_jq; then
    # Check version and upgrade if needed
    local version=$(jq -r '.version // 1' "$AI_SANDBOX_CONFIG" 2>/dev/null)
    if [[ "$version" == "1" ]]; then
      upgrade_config_to_v2
    fi
  fi
}

# Initialize config and migrate legacy data
init_config

# Check if current directory is inside any whitelisted workspace
ALLOWED=false
while IFS= read -r ws; do
  [[ -z "$ws" ]] && continue
  if [[ "$CURRENT_DIR" == "$ws"* ]]; then
    ALLOWED=true
    break
  fi
done < <(read_workspaces)

if [[ "$ALLOWED" != "true" ]]; then
  echo "⚠️  SECURITY WARNING: You are running ${TOOL:-shell} outside a whitelisted workspace."
  echo "   Current path: $CURRENT_DIR"
  echo ""
  echo "Allowing this path gives the AI container access to this folder."

  # Only prompt if running interactively (TTY attached)
  if [[ -t 0 ]]; then
    read -p "Do you want to whitelist the current directory? [y/N]: " CONFIRM
    if [[ "$CONFIRM" =~ ^[Yy]$ ]]; then
      add_workspace "$CURRENT_DIR"
      echo "✅ Added $CURRENT_DIR to whitelisted workspaces"
    else
      echo "❌ Operation cancelled. Access denied."
      echo "📁 Allowed workspaces:"
      read_workspaces
      exit 1
    fi
  else
    # Non-interactive: fail securely
    echo "❌ Access denied (non-interactive mode). Please add this path manually:"
    echo "   echo '$CURRENT_DIR' >> $WORKSPACES_FILE"
    echo "📁 Allowed workspaces:"
    read_workspaces
    exit 1
  fi
fi

# Image source selection: local or registry (default: ghcr.io)
# Set AI_IMAGE_SOURCE=registry in ~/.ai-env to use pre-built images.
# Override registry path with AI_IMAGE_REGISTRY, override tag with AI_IMAGE_TAG.
AI_IMAGE_SOURCE="${AI_IMAGE_SOURCE:-local}"
AI_IMAGE_REGISTRY="${AI_IMAGE_REGISTRY:-ghcr.io/nano-step/ai-opencode}"
AI_IMAGE_TAG="${AI_IMAGE_TAG:-base}"

# ============================================================================
# SCREENSHOT DIRECTORY DETECTION (macOS Only)
# ============================================================================
if [[ "$(uname)" == "Darwin" ]] && [[ -t 0 ]]; then
  # 1. Detect location (default to Desktop if key missing)
  SCREENSHOT_DIR=$(defaults read com.apple.screencapture location 2>/dev/null || echo "$HOME/Desktop")
  # Expand tilde if present
  SCREENSHOT_DIR="${SCREENSHOT_DIR/#\~/$HOME}"

  # 2. Check if valid directory
  if [[ -d "$SCREENSHOT_DIR" ]]; then

    # 3. Check if ALREADY whitelisted (exact match or parent)
    IS_WHITELISTED=false
    while IFS= read -r ws; do
      # Check if screenshot dir IS a workspace, or IS INSIDE a workspace
      if [[ "$SCREENSHOT_DIR" == "$ws" ]] || [[ "$SCREENSHOT_DIR" == "$ws"/* ]]; then
        IS_WHITELISTED=true
        break
      fi
    done < <(read_workspaces)

    # 4. Prompt if not whitelisted
    if [[ "$IS_WHITELISTED" == "false" ]]; then
      echo ""
      echo "📸 Screenshot Directory Detected"
      echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
      echo "Location: $SCREENSHOT_DIR"
      echo "Status:   Not whitelisted"
      echo ""
      echo "Whitelisting this folder allows you to drag & drop screenshots into AI tools."
      echo ""
      # Use /dev/tty to ensure we read from user even if stdin is redirected
      read -p "Whitelist screenshots folder? [y/N]: " CONFIRM_SS < /dev/tty

      if [[ "$CONFIRM_SS" =~ ^[Yy]$ ]]; then
        add_workspace "$SCREENSHOT_DIR"
        echo "✅ Added to whitelist."
      else
        echo "❌ Skipped."
      fi
      echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    fi
  fi
fi

if [[ "$AI_IMAGE_SOURCE" == "registry" ]]; then
  IMAGE="${AI_IMAGE_REGISTRY}:${AI_IMAGE_TAG}"
else
  IMAGE="ai-sandbox:latest"
fi

# Tool validation (warn if tool not in config.json)
if [[ -n "$TOOL" ]]; then
  if command -v jq &>/dev/null && [[ -f "$AI_SANDBOX_CONFIG" ]]; then
    INSTALLED_TOOLS=$(jq -r '.tools.installed // [] | .[]' "$AI_SANDBOX_CONFIG" 2>/dev/null)
    if [[ -n "$INSTALLED_TOOLS" ]] && ! echo "$INSTALLED_TOOLS" | grep -qx "$TOOL"; then
      echo "⚠️  WARNING: Tool '$TOOL' may not be installed in the sandbox image."
      echo "   Installed tools: $(echo "$INSTALLED_TOOLS" | tr '\n' ', ' | sed 's/,$//')"
      echo "   Run setup.sh to add tools."
      echo ""
    fi
  fi
fi

# Shared home directory for all tools (unified image)
HOME_DIR="$SANDBOX_DIR/home"
GIT_SHARED_DIR="$SANDBOX_DIR/shared/git"
CACHE_DIR="$SANDBOX_DIR/cache"

mkdir -p "$HOME_DIR"
mkdir -p "$GIT_SHARED_DIR"
mkdir -p "$CACHE_DIR/npm" "$CACHE_DIR/bun" "$CACHE_DIR/pip" "$CACHE_DIR/playwright-browsers"

# ============================================================================
# MIGRATION V3: Merge per-tool home directories into unified home
# ============================================================================
migrate_to_unified_home() {
  local marker_file="$SANDBOX_DIR/.migrated-unified-home"

  # Skip if already migrated
  [[ -f "$marker_file" ]] && return 0

  # Check if any per-tool home directories exist
  local needs_migration=false
  for tool_home in "$SANDBOX_DIR"/tools/*/home; do
    if [[ -d "$tool_home" ]]; then
      needs_migration=true
      break
    fi
  done

  [[ "$needs_migration" == "false" ]] && { touch "$marker_file"; return 0; }

  echo "🔄 Migrating per-tool home directories to unified home..."

  local migrated_count=0
  for tool_home in "$SANDBOX_DIR"/tools/*/home; do
    [[ -d "$tool_home" ]] || continue
    local tool_name
    tool_name=$(basename "$(dirname "$tool_home")")
    echo "  🔄 Migrating tools/$tool_name/home/ → home/"

    # Use cp -rn (no-clobber) - works on macOS and Linux
    # Falls back to rsync if cp -rn fails (some older systems)
    # KNOWN ISSUE: glob * doesn't match dotfiles (.nano-brain, .config, etc.)
    # TODO: fix with dotglob or rsync-first approach
    if cp -rn "$tool_home/"* "$HOME_DIR/" 2>/dev/null; then
      ((migrated_count++))
    elif command -v rsync &>/dev/null; then
      rsync -a --ignore-existing "$tool_home/" "$HOME_DIR/" 2>/dev/null && ((migrated_count++))
    else
      echo "  ⚠️  Could not migrate $tool_name (cp -rn and rsync unavailable)"
    fi
  done

  # Create marker file
  date -Iseconds > "$marker_file" 2>/dev/null || date > "$marker_file"

  if [[ $migrated_count -gt 0 ]]; then
    echo "✅ Migration complete! Merged $migrated_count tool home directories."
    echo ""
    echo "ℹ️  Old per-tool home directories preserved. Run 'npx @nano-step/ai-sandbox-wrapper clean' to remove them."
  fi
}

migrate_to_unified_home

# Build volume mounts for all whitelisted workspaces
VOLUME_MOUNTS=""
while IFS= read -r ws; do
  VOLUME_MOUNTS="$VOLUME_MOUNTS -v $ws:$ws:delegated"
done < "$WORKSPACES_FILE"

# Config mount registry for all tools
# Maps tool name to space-separated config paths (relative to $HOME)
get_tool_configs() {
  case "$1" in
    amp)       echo ".config/amp .local/share/amp" ;;
    opencode)  echo ".config/opencode .local/share/opencode" ;;
    claude)    echo ".claude" ;;
    openclaw)  echo ".openclaw" ;;
    droid)     echo ".config/droid" ;;
    qoder)     echo ".config/qoder" ;;
    auggie)    echo ".config/auggie" ;;
    codebuddy) echo ".config/codebuddy" ;;
    jules)     echo ".config/jules" ;;
    shai)      echo ".config/shai" ;;
    gemini)    echo ".config/gemini" ;;
    aider)     echo ".config/aider .aider" ;;
    kilo)      echo ".config/kilo" ;;
    codex)     echo ".config/codex" ;;
    qwen)      echo ".config/qwen" ;;
  esac
}

# All known tools (for fallback)
ALL_KNOWN_TOOLS="amp opencode claude openclaw droid qoder auggie codebuddy jules shai gemini aider kilo codex qwen"

# Get list of installed tools from config.json, fallback to all known tools
get_installed_tools() {
  local installed
  if command -v jq &>/dev/null && [[ -f "$AI_SANDBOX_CONFIG" ]]; then
    installed=$(jq -r '.tools.installed[]? // empty' "$AI_SANDBOX_CONFIG" 2>/dev/null)
  fi
  if [[ -z "$installed" ]]; then
    # Fallback: return all known tools in registry
    echo "$ALL_KNOWN_TOOLS"
  else
    echo "$installed"
  fi
}

# Tool config persistence via bind mounts
# Bind-mount host paths directly to ensure changes persist to the host.
TOOL_CONFIG_MOUNTS=""
RG_COMPAT_MOUNT=""

mount_tool_config() {
  local host_path="$1"
  local container_path="$2" # Relative to /home/agent

  if [[ -e "$host_path" || -d "$(dirname "$host_path")" ]]; then
    # Ensure directory exists on host to avoid root-owned directory creation by Docker
    if [[ "$host_path" == */ ]]; then
      mkdir -p "$host_path"
    else
      mkdir -p "$(dirname "$host_path")"
    fi
    TOOL_CONFIG_MOUNTS="$TOOL_CONFIG_MOUNTS -v $host_path:/home/agent/$container_path:delegated"
  fi
}

# Mount configs for all installed tools (unified image)
for tool in $(get_installed_tools); do
  for cfg_path in $(get_tool_configs "$tool"); do
    mount_tool_config "$HOME/$cfg_path" "$cfg_path"
  done
done

# ============================================================================
# OPENCODE PER-PROJECT SQLITE ISOLATION
# Gives each project its own opencode.db / -wal / -shm files to prevent
# concurrent-writer corruption. See openspec/changes/opencode-db-isolation/.
# ============================================================================

# Compute a deterministic project identifier in the form <human>-<8-hex-hash>.
# Resolver priority: git remote origin > git toplevel > realpath of workdir.
# Hash guarantees uniqueness; human name aids debugging.
# Usage: compute_opencode_project_hash "/path/to/workdir"
compute_opencode_project_hash() {
  local workdir="$1"
  local project_id=""

  # 1. git repo with origin remote
  local remote_url
  remote_url=$(git -C "$workdir" config --get remote.origin.url 2>/dev/null || true)
  if [[ -n "$remote_url" ]]; then
    project_id="git:${remote_url}"
  else
    # 2. git repo without origin (use toplevel path)
    local toplevel
    toplevel=$(git -C "$workdir" rev-parse --show-toplevel 2>/dev/null || true)
    if [[ -n "$toplevel" ]]; then
      local real_toplevel
      real_toplevel=$(realpath "$toplevel" 2>/dev/null || echo "$toplevel")
      project_id="gitroot:${real_toplevel}"
    else
      # 3. non-git directory
      local real_workdir
      real_workdir=$(realpath "$workdir" 2>/dev/null || echo "$workdir")
      project_id="path:${real_workdir}"
    fi
  fi

  # Compute 8-char hash from project_id (uniqueness guarantee)
  local hash
  hash=$(printf '%s' "$project_id" | openssl dgst -sha256 -r | cut -c1-8)

  # Extract human-readable name from the SAME project_id source
  # so that subdirectories of one project resolve to the same name+hash.
  local human=""
  case "$project_id" in
    git:*)     human=$(echo "${project_id#git:}" | sed 's|.*/||; s|\.git$||') ;;
    gitroot:*) human=$(basename "${project_id#gitroot:}") ;;
    path:*)    human=$(basename "${project_id#path:}") ;;
  esac
  human=$(echo "$human" | tr ' ' '-' | tr -cd '[:alnum:]_-' | tr '[:upper:]' '[:lower:]' | cut -c1-30)
  [[ -z "$human" ]] && human="workspace"

  # Final identifier: <human-name>-<8-hex-hash>
  # Human name aids debugging; hash guarantees uniqueness across same-name projects.
  echo "${human}-${hash}"
}

# Print the host directory path for a given project hash's SQLite files.
# Usage: opencode_db_dir "abc123..."
opencode_db_dir() {
  local hash="$1"
  echo "$HOME/.ai-sandbox/opencode-dbs/${hash}"
}

# Ensure the per-project SQLite directory and placeholder files exist.
# Idempotent — does NOT clobber existing files.
# Usage: ensure_opencode_db_files "abc123..."
ensure_opencode_db_files() {
  local hash="$1"
  local db_dir
  db_dir=$(opencode_db_dir "$hash")

  mkdir -p "$db_dir"
  chmod 755 "$db_dir" 2>/dev/null || true
  # opencode.db: preserve existing data; only create if missing.
  if [[ ! -f "$db_dir/opencode.db" ]]; then
    touch "$db_dir/opencode.db"
    chmod 644 "$db_dir/opencode.db" 2>/dev/null || true
  fi
  # WAL and SHM are transient SQLite files. Always truncate to 0-bytes so a
  # fresh container never inherits stale content from a prior ungraceful exit.
  # A 0-byte WAL/SHM is valid — SQLite initialises them on first open.
  # Using : > file (shell built-in) avoids a truncate(1) portability concern.
  for f in opencode.db-wal opencode.db-shm; do
    : > "$db_dir/$f"
    chmod 644 "$db_dir/$f" 2>/dev/null || true
  done

  # The file-level bind-mount TARGETS must already exist in the global opencode
  # dir. Docker Desktop (virtiofs) cannot create a mountpoint inside a
  # virtiofs-backed directory mount — runc rejects it as "outside of rootfs".
  # The per-project files above are the mount SOURCE; these are the TARGET
  # placeholders. opencode.db usually exists; -wal/-shm often don't. touch only
  # creates when missing (an empty -wal/-shm is a valid "no active state" and
  # never clobbers the user's real global opencode.db).
  local global_dir="$HOME/.local/share/opencode"
  mkdir -p "$global_dir"
  for f in opencode.db opencode.db-wal opencode.db-shm; do
    [[ -e "$global_dir/$f" ]] || touch "$global_dir/$f"
  done
}

# One-time backup of the pre-existing global SQLite files.
# No-ops if the .initialized marker already exists.
# Usage: ensure_opencode_backup
ensure_opencode_backup() {
  local marker="$HOME/.ai-sandbox/opencode-dbs/.backups/.initialized"

  if [[ -f "$marker" ]]; then
    return 0
  fi

  local timestamp
  timestamp=$(date -u +%Y%m%dT%H%M%SZ)
  local backup_dir="$HOME/.ai-sandbox/opencode-dbs/.backups/${timestamp}"
  mkdir -p "$backup_dir"

  local backed_up=""
  local global_dir="$HOME/.local/share/opencode"
  for f in opencode.db opencode.db-wal opencode.db-shm; do
    if [[ -f "$global_dir/$f" ]]; then
      cp "$global_dir/$f" "$backup_dir/$f"
      backed_up="${backed_up}${f} "
    fi
  done

  # Write marker regardless (even if no files were backed up)
  mkdir -p "$(dirname "$marker")"
  echo "timestamp=$timestamp" > "$marker"
  echo "files=$backed_up" >> "$marker"

  if [[ -n "$backed_up" ]]; then
    echo "→ backed up pre-existing opencode SQLite files to $backup_dir" >&2
  fi
}

# Append per-project SQLite file-level bind mounts to DOCKER_ARGS.
# Implements design.md Decision 2: three file-level overlays placed on top of
# the existing ~/.local/share/opencode directory mount, so opencode reads/writes
# its canonical path (/home/agent/.local/share/opencode/opencode.db) but the
# kernel redirects I/O to the per-project file on the host.
# Placeholder files are created on the host by ensure_opencode_db_files() so
# Docker does not auto-create them as directories.
# Usage: append_opencode_db_mounts "abc123..."
append_opencode_db_mounts() {
  local hash="$1"
  local db_dir
  db_dir=$(opencode_db_dir "$hash")

  DOCKER_ARGS+=(-v "${db_dir}/opencode.db:/home/agent/.local/share/opencode/opencode.db:delegated")
  DOCKER_ARGS+=(-v "${db_dir}/opencode.db-wal:/home/agent/.local/share/opencode/opencode.db-wal:delegated")
  DOCKER_ARGS+=(-v "${db_dir}/opencode.db-shm:/home/agent/.local/share/opencode/opencode.db-shm:delegated")
}

setup_opencode_rg_compat() {
  [[ "$TOOL" != "opencode" ]] && return 0

  local bundled_rg="$HOME/.local/share/opencode/bin/rg"
  local rg_shim_path="$SANDBOX_DIR/shared/rg-linux-shim"

  [[ -f "$bundled_rg" ]] || return 0
  command -v file &>/dev/null || return 0

  local rg_file_info
  rg_file_info=$(file -b "$bundled_rg" 2>/dev/null || true)

  if echo "$rg_file_info" | grep -qi "Mach-O"; then
    mkdir -p "$(dirname "$rg_shim_path")"
    cat > "$rg_shim_path" << 'EOF'
#!/usr/bin/env bash
exec /usr/bin/rg "$@"
EOF
    chmod +x "$rg_shim_path"
    RG_COMPAT_MOUNT="-v $rg_shim_path:/home/agent/.local/share/opencode/bin/rg:ro"
    echo "⚠️  Detected incompatible OpenCode bundled rg (Mach-O). Using /usr/bin/rg in container."
  fi
}

setup_opencode_rg_compat

# Bundle OpenCode default skills (if opencode is installed)
if get_installed_tools | grep -qw "opencode"; then
  AIRUN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  BUNDLED_SKILLS_DIR="$AIRUN_DIR/../skills"
  if [[ -d "$BUNDLED_SKILLS_DIR" ]]; then
    for skill_dir in "$BUNDLED_SKILLS_DIR"/*/; do
      [[ ! -d "$skill_dir" ]] && continue
      skill_name=$(basename "$skill_dir")
      target_dir="$HOME/.config/opencode/skills/$skill_name"
      if [[ ! -d "$target_dir" ]]; then
        mkdir -p "$target_dir"
        cp -r "$skill_dir"* "$target_dir/" 2>/dev/null || true
      fi
    done
  fi
fi

# Ensure required directories exist in HOME_DIR
mkdir -p "$HOME_DIR/.config" "$HOME_DIR/.local/share" "$HOME_DIR/.cache" "$HOME_DIR/.bun"

SHARED_CACHE_MOUNTS="-v $CACHE_DIR/npm:/home/agent/.npm:delegated"
SHARED_CACHE_MOUNTS="$SHARED_CACHE_MOUNTS -v $CACHE_DIR/bun:/home/agent/.bun/install/cache:delegated"
SHARED_CACHE_MOUNTS="$SHARED_CACHE_MOUNTS -v $CACHE_DIR/pip:/home/agent/.cache/pip:delegated"
SHARED_CACHE_MOUNTS="$SHARED_CACHE_MOUNTS -v $CACHE_DIR/playwright-browsers:/opt/playwright-browsers:delegated"

# Shared skills mount (from host, read-only)
HOST_SKILLS_DIR="$HOME/.config/agents/skills"
if [[ -d "$HOST_SKILLS_DIR" ]]; then
  SHARED_CACHE_MOUNTS="$SHARED_CACHE_MOUNTS -v $HOST_SKILLS_DIR:/home/agent/.config/agents/skills:ro"
  SHARED_CACHE_MOUNTS="$SHARED_CACHE_MOUNTS -v $HOST_SKILLS_DIR:/home/agent/.config/opencode/skills:ro"
fi

# Host Chrome for Playwright MCP (via CDP - Chrome DevTools Protocol)
# NOTE: macOS Chrome binary (Mach-O) cannot run inside a Linux container.
# Instead, we launch Chrome on the host with --remote-debugging-port and
# connect from the container via CDP. Each container gets its own port and
# its own MCP entry; entries are sweep-cleaned on every start.
HOST_CHROME_CDP=false
HOST_CHROME_CDP_PORT=19222
PLAYWRIGHT_MCP_NAME=""
CHROME_DEVTOOLS_MCP_NAME=""
# Portable symlink-resolving SCRIPT_DIR (macOS readlink has no -f).
# Needed because ai-run is typically invoked via the ~/bin/ai-run symlink.
_pmcp_resolve_script_dir() {
  local src="${BASH_SOURCE[0]}" dir
  while [[ -L "$src" ]]; do
    dir="$(cd -P "$(dirname "$src")" && pwd)"
    src="$(readlink "$src")"
    [[ "$src" != /* ]] && src="$dir/$src"
  done
  cd -P "$(dirname "$src")/.." && pwd
}
SCRIPT_DIR="$(_pmcp_resolve_script_dir)"
unset -f _pmcp_resolve_script_dir
# shellcheck source=lib/playwright-mcp-config.sh
[[ -f "$SCRIPT_DIR/lib/playwright-mcp-config.sh" ]] && source "$SCRIPT_DIR/lib/playwright-mcp-config.sh"

if [[ "$TOOL" == "opencode" ]] && command -v jq &>/dev/null && [[ -f "$AI_SANDBOX_CONFIG" ]] && declare -f pmcp::sanitize_name >/dev/null; then
  PLAYWRIGHT_HOST_CHROME=$(jq -r '.mcp.chromePath // empty' "$AI_SANDBOX_CONFIG" 2>/dev/null)
  if [[ -n "$PLAYWRIGHT_HOST_CHROME" ]] && [[ -f "$PLAYWRIGHT_HOST_CHROME" ]]; then
    if ! jq -e '.mcp.installed // [] | any(. == "playwright" or . == "chrome-devtools")' "$AI_SANDBOX_CONFIG" &>/dev/null; then
      echo ""
      echo "ℹ️  Host Chrome configured but no browser MCP installed — skipping."
      echo "   Run setup.sh and select playwright-mcp or chrome-devtools-mcp to enable AI browser control."
      PLAYWRIGHT_HOST_CHROME=""
    fi
    # Ask user whether to open host Chrome browser
    if [[ -t 0 ]] && [[ -n "$PLAYWRIGHT_HOST_CHROME" ]]; then
      echo ""
      echo "🌐 Host Chrome browser is configured: $PLAYWRIGHT_HOST_CHROME"
      printf "   Open browser for AI agent? [y/N] "
      read -r -t 10 OPEN_CHROME_ANSWER || OPEN_CHROME_ANSWER=""
      echo ""
      if [[ ! "$OPEN_CHROME_ANSWER" =~ ^[Yy]$ ]]; then
        echo "   ⏭️  Skipping host Chrome (using container Chromium if available)"
        PLAYWRIGHT_HOST_CHROME=""
      fi
    fi
  fi
  if [[ -n "$PLAYWRIGHT_HOST_CHROME" ]] && [[ -f "$PLAYWRIGHT_HOST_CHROME" ]]; then
    HOST_CHROME_CDP=true
    echo "🌐 Host Chrome CDP mode: $PLAYWRIGHT_HOST_CHROME"

    # CONTAINER_NAME has the form "--name foo" or is empty. Extract the value
    # for hashing only — the MCP key uses just the port (containers that
    # collide on a port intentionally share the same Chrome / MCP entry).
    CONTAINER_NAME_VALUE="${CONTAINER_NAME#--name }"
    [[ "$CONTAINER_NAME_VALUE" == "$CONTAINER_NAME" ]] && CONTAINER_NAME_VALUE="anon-$$"

    # Deterministic port per container name
    CONTAINER_HASH=$(echo "$CONTAINER_NAME_VALUE" | md5sum | cut -c1-4)
    HOST_CHROME_CDP_PORT=$((19222 + 0x$CONTAINER_HASH % 100))

    # Only register entries for MCP binaries actually present in the image
    # (tracked in $AI_SANDBOX_CONFIG by setup.sh). Otherwise opencode would
    # try to spawn a missing binary and fail. (is_mcp_installed is defined
    # later in the file, so use jq inline here.)
    if jq -e '.mcp.installed // [] | index("playwright") != null' "$AI_SANDBOX_CONFIG" &>/dev/null; then
      PLAYWRIGHT_MCP_NAME="playwright_port_${HOST_CHROME_CDP_PORT}"
    fi
    if jq -e '.mcp.installed // [] | index("chrome-devtools") != null' "$AI_SANDBOX_CONFIG" &>/dev/null; then
      CHROME_DEVTOOLS_MCP_NAME="chrome-devtools_port_${HOST_CHROME_CDP_PORT}"
    fi

    # Reuse-if-alive: probe before launching
    if pmcp::probe_chrome "$HOST_CHROME_CDP_PORT"; then
      echo "  ✅ Chrome already running on port $HOST_CHROME_CDP_PORT (reusing)"
    else
      echo "  🚀 Launching Chrome with remote debugging on port $HOST_CHROME_CDP_PORT..."
      mkdir -p "$SANDBOX_DIR/chrome-profile-$HOST_CHROME_CDP_PORT"
      "$PLAYWRIGHT_HOST_CHROME" \
        --remote-debugging-port="$HOST_CHROME_CDP_PORT" \
        --user-data-dir="$SANDBOX_DIR/chrome-profile-$HOST_CHROME_CDP_PORT" \
        --no-first-run \
        --no-default-browser-check \
        &>/dev/null &
      CHROME_PID=$!
      for i in {1..20}; do
        if pmcp::probe_chrome "$HOST_CHROME_CDP_PORT"; then
          echo "  ✅ Chrome ready (PID: $CHROME_PID, port: $HOST_CHROME_CDP_PORT)"
          echo "  👀 You can watch the browser window to see what the AI is doing"
          break
        fi
        sleep 0.25
      done
      if ! pmcp::probe_chrome "$HOST_CHROME_CDP_PORT"; then
        echo "  ⚠️  Chrome failed to start. Falling back to container Chromium."
        HOST_CHROME_CDP=false
        kill "$CHROME_PID" 2>/dev/null || true
        PLAYWRIGHT_MCP_NAME=""
        CHROME_DEVTOOLS_MCP_NAME=""
      fi
    fi

    # Locked sweep+register on the shared OpenCode config — both
    # playwright-mcp and chrome-devtools-mcp point at the same host Chrome.
    if [[ "$HOST_CHROME_CDP" == "true" ]]; then
      OPENCODE_CONFIG_FILE="$HOME/.config/opencode/opencode.json"
      LOCK_FILE="$HOME/.config/opencode/.playwright.lock"
      mkdir -p "$(dirname "$OPENCODE_CONFIG_FILE")"
      [[ -f "$OPENCODE_CONFIG_FILE" ]] || echo '{}' > "$OPENCODE_CONFIG_FILE"
      pmcp::with_lock "$LOCK_FILE" pmcp::register_host_chrome \
        "$OPENCODE_CONFIG_FILE" "$HOST_CHROME_CDP_PORT" \
        "$PLAYWRIGHT_MCP_NAME" "$CHROME_DEVTOOLS_MCP_NAME"
      rc=$?
      if [[ "$rc" == "99" ]]; then
        echo "  ⚠️  Could not acquire MCP config lock within 5s; skipping registration."
        PLAYWRIGHT_MCP_NAME=""
        CHROME_DEVTOOLS_MCP_NAME=""
      elif [[ "$rc" != "0" ]]; then
        echo "  ⚠️  MCP config update failed (rc=$rc); skipping registration."
        PLAYWRIGHT_MCP_NAME=""
        CHROME_DEVTOOLS_MCP_NAME=""
      fi
    fi
  fi
fi

# Nano-brain mount: writable so container can modify config, write memory, logs, etc.
NANO_BRAIN_MOUNT=""
if [[ -d "$HOME/.nano-brain" ]]; then
  mkdir -p "$HOME/.nano-brain/logs" "$HOME/.nano-brain/memory"
  NANO_BRAIN_MOUNT="-v $HOME/.nano-brain:/home/agent/.nano-brain:delegated"
  echo "ℹ️  Mounted .nano-brain (rw)"
fi


# Project-level config mount (if exists and tool specified)
CONFIG_MOUNT=""
if [[ -n "$TOOL" ]]; then
  PROJECT_CONFIG="$CURRENT_DIR/.$TOOL.json"
  if [[ -f "$PROJECT_CONFIG" ]]; then
    CONFIG_MOUNT="-v $PROJECT_CONFIG:$CURRENT_DIR/.$TOOL.json:delegated"
  fi
fi

# ============================================================================
# NETWORK CONFIGURATION
# ============================================================================

# Note: AI_SANDBOX_CONFIG and has_jq() defined earlier in PATH CONFIGURATION

# Note: init_config, migrate_legacy_to_config, upgrade_config_to_v2 moved to PATH CONFIGURATION section

# Read networks for current workspace (workspace > global > empty)
read_network_config() {
  init_config
  local workspace="$CURRENT_DIR"

  if has_jq; then
    # Try workspace-specific first
    local ws_networks=$(jq -r --arg ws "$workspace" '.networks.workspaces[$ws] // empty | .[]?' "$AI_SANDBOX_CONFIG" 2>/dev/null)
    if [[ -n "$ws_networks" ]]; then
      echo "$ws_networks"
      return
    fi
    # Fall back to global
    jq -r '.networks.global[]?' "$AI_SANDBOX_CONFIG" 2>/dev/null
  else
    # Fallback: grep-based parsing (basic)
    grep -o '"global":\s*\[[^]]*\]' "$AI_SANDBOX_CONFIG" 2>/dev/null | grep -o '"[^"]*"' | tr -d '"' | grep -v global
  fi
}

# Write networks to config (scope: workspace or global)
write_network_config() {
  local scope="$1"  # "workspace" or "global"
  shift
  local networks=("$@")

  init_config

  if has_jq; then
    local json_array=$(printf '%s\n' "${networks[@]}" | jq -R . | jq -s .)

    if [[ "$scope" == "workspace" ]]; then
      jq --arg ws "$CURRENT_DIR" --argjson nets "$json_array" \
        '.networks.workspaces[$ws] = $nets' "$AI_SANDBOX_CONFIG" > "$AI_SANDBOX_CONFIG.tmp" \
        && mv "$AI_SANDBOX_CONFIG.tmp" "$AI_SANDBOX_CONFIG"
    else
      jq --argjson nets "$json_array" \
        '.networks.global = $nets' "$AI_SANDBOX_CONFIG" > "$AI_SANDBOX_CONFIG.tmp" \
        && mv "$AI_SANDBOX_CONFIG.tmp" "$AI_SANDBOX_CONFIG"
    fi
    chmod 600 "$AI_SANDBOX_CONFIG"
  else
    echo "⚠️  jq not found. Please install jq to save network configuration."
    return 1
  fi
}

# Validate networks exist, return only valid ones
validate_networks() {
  local networks=("$@")
  local valid=()

  for net in "${networks[@]}"; do
    [[ -z "$net" ]] && continue
    if docker network inspect "$net" &>/dev/null; then
      valid+=("$net")
    fi
  done

  printf '%s\n' "${valid[@]}"
}

# Discover Docker Compose networks (have com.docker.compose.project label)
discover_compose_networks() {
  docker network ls --filter "label=com.docker.compose.project" --format "{{.Name}}" 2>/dev/null | sort
}

# Discover custom networks (not system, not compose)
discover_custom_networks() {
  local compose_nets=$(discover_compose_networks)
  docker network ls --format "{{.Name}}" 2>/dev/null | while read -r net; do
    # Skip system networks
    [[ "$net" == "bridge" || "$net" == "host" || "$net" == "none" ]] && continue
    # Skip compose networks
    echo "$compose_nets" | grep -q "^${net}$" && continue
    echo "$net"
  done | sort
}

# Get containers in a network
get_network_containers() {
  local network="$1"
  docker network inspect "$network" --format '{{range .Containers}}{{.Name}} {{end}}' 2>/dev/null | xargs | tr ' ' ', '
}

# Ensure shared Docker network exists for cross-container service discovery
# (e.g., agent containers reaching the open-design daemon by name)
ensure_network() {
  local net="${1:-ai-sandbox}"
  if ! docker network inspect "$net" >/dev/null 2>&1; then
    docker network create "$net" >/dev/null 2>&1 || {
      echo "⚠️  WARNING: failed to create Docker network '$net'" >&2
      return 1
    }
  fi
  return 0
}

# Interactive network selection menu (multi-select)
show_network_menu() {
  local compose_nets=()
  local custom_nets=()
  local all_nets=()
  local selected=()

  echo "🔍 Discovering Docker networks..."
  echo ""

  # Gather networks
  while IFS= read -r net; do
    [[ -n "$net" ]] && compose_nets+=("$net")
  done < <(discover_compose_networks)

  while IFS= read -r net; do
    [[ -n "$net" ]] && custom_nets+=("$net")
  done < <(discover_custom_networks)

  # Build combined list: select-all first, then compose, then custom, then "none"
  # Index layout: [0]=select-all, [1..compose_count]=compose, [compose_count+1..custom_end]=custom, [last]=none
  local network_count=$((${#compose_nets[@]} + ${#custom_nets[@]}))
  all_nets=("select-all" "${compose_nets[@]}" "${custom_nets[@]}" "none")

  if [[ $network_count -eq 0 ]]; then
    echo "ℹ️  No Docker networks found (besides system networks)."
    SELECTED_NETWORKS=()
    return 0
  fi

  # Initialize selection array (none is pre-selected)
  local sel=()
  local select_all_idx=0
  local compose_start=1
  local compose_end=$((1 + ${#compose_nets[@]}))
  local custom_start=$compose_end
  local none_idx=$((${#all_nets[@]} - 1))

  for ((i=0; i<${#all_nets[@]}; i++)); do
    if [[ "${all_nets[$i]}" == "none" ]]; then
      sel[$i]=1
    else
      sel[$i]=0
    fi
  done

  local cursor=0

  tput civis
  trap 'tput cnorm' INT TERM EXIT

  while true; do
    clear
    echo "🔗 Network Selection"
    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    echo ""

    # Select All option
    local check="[ ]"
    [[ ${sel[$select_all_idx]} -eq 1 ]] && check="[x]"
    if [[ $cursor -eq $select_all_idx ]]; then
      tput setaf 6
      printf "  ➔ %s Select All\n" "$check"
    else
      printf "    %s Select All\n" "$check"
    fi
    tput sgr0
    echo ""

    # Compose Networks section
    if [[ ${#compose_nets[@]} -gt 0 ]]; then
      echo "Compose Networks:"
      for ((i=compose_start; i<compose_end; i++)); do
        local net="${all_nets[$i]}"
        local containers=$(get_network_containers "$net")
        local check="[ ]"
        [[ ${sel[$i]} -eq 1 ]] && check="[x]"

        if [[ $i -eq $cursor ]]; then
          tput setaf 6
          printf "  ➔ %s %-30s" "$check" "$net"
        else
          printf "    %s %-30s" "$check" "$net"
        fi

        if [[ -n "$containers" ]]; then
          tput setaf 8
          printf " (%s)" "$containers"
        fi
        tput sgr0
        echo ""
      done
      echo ""
    fi

    # Custom Networks section
    if [[ ${#custom_nets[@]} -gt 0 ]]; then
      echo "Other Networks:"
      for ((i=custom_start; i<none_idx; i++)); do
        local net="${all_nets[$i]}"
        local containers=$(get_network_containers "$net")
        local check="[ ]"
        [[ ${sel[$i]} -eq 1 ]] && check="[x]"

        if [[ $i -eq $cursor ]]; then
          tput setaf 6
          printf "  ➔ %s %-30s" "$check" "$net"
        else
          printf "    %s %-30s" "$check" "$net"
        fi

        if [[ -n "$containers" ]]; then
          tput setaf 8
          printf " (%s)" "$containers"
        fi
        tput sgr0
        echo ""
      done
      echo ""
    fi

    # None option
    echo ""
    local check="[ ]"
    [[ ${sel[$none_idx]} -eq 1 ]] && check="[x]"
    if [[ $cursor -eq $none_idx ]]; then
      tput setaf 6
      printf "  ➔ %s None (no network)\n" "$check"
    else
      printf "    %s None (no network)\n" "$check"
    fi
    tput sgr0

    echo ""
    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    echo "↑↓ Move  SPACE Select  a Select All  ENTER Confirm"

    # Read input
    IFS= read -rsn1 key
    if [[ "$key" == $'\x1b' ]]; then
      read -rsn2 -t 1 escape_seq
      case "$escape_seq" in
        '[A') ((cursor > 0)) && ((cursor--)) || true ;;
        '[B') ((cursor < ${#all_nets[@]} - 1)) && ((cursor++)) || true ;;
      esac
    else
      case "$key" in
        ' ')
          # Toggle selection
          if [[ $cursor -eq $select_all_idx ]]; then
            # Toggle select all
            if [[ ${sel[$select_all_idx]} -eq 1 ]]; then
              # Deselect all, select none
              for ((i=0; i<${#all_nets[@]}; i++)); do sel[$i]=0; done
              sel[$none_idx]=1
            else
              # Select all networks, deselect none
              for ((i=0; i<${#all_nets[@]}; i++)); do sel[$i]=1; done
              sel[$none_idx]=0
            fi
          elif [[ $cursor -eq $none_idx ]]; then
            # Selecting "none" clears all others
            for ((i=0; i<${#all_nets[@]}; i++)); do sel[$i]=0; done
            sel[$none_idx]=1
          else
            # Selecting a network clears "none" and updates select-all state
            sel[$none_idx]=0
            if [[ ${sel[$cursor]} -eq 1 ]]; then
              sel[$cursor]=0
              sel[$select_all_idx]=0
            else
              sel[$cursor]=1
              # Check if all networks are now selected
              local all_selected=1
              for ((i=compose_start; i<none_idx; i++)); do
                [[ ${sel[$i]} -eq 0 ]] && all_selected=0 && break
              done
              sel[$select_all_idx]=$all_selected
            fi
          fi
          ;;
        'a'|'A')
          # Quick select all
          for ((i=0; i<${#all_nets[@]}; i++)); do sel[$i]=1; done
          sel[$none_idx]=0
          ;;
        ''|$'\n'|$'\r')
          break
          ;;
        k) ((cursor > 0)) && ((cursor--)) || true ;;
        j) ((cursor < ${#all_nets[@]} - 1)) && ((cursor++)) || true ;;
      esac
    fi
  done

  tput cnorm
  trap - INT TERM EXIT

  # Collect selected networks (exclude select-all and none)
  SELECTED_NETWORKS=()
  for ((i=1; i<${#all_nets[@]}; i++)); do
    if [[ ${sel[$i]} -eq 1 && "${all_nets[$i]}" != "none" && "${all_nets[$i]}" != "select-all" ]]; then
      SELECTED_NETWORKS+=("${all_nets[$i]}")
    fi
  done
}

# Save prompt after selection
show_save_prompt() {
  local options=("This workspace" "Global (all workspaces)" "Don't save")
  local cursor=0

  echo ""

  tput civis
  trap 'tput cnorm' INT TERM EXIT

  while true; do
    # Move cursor up to redraw (3 options + header)
    tput cuu 5 2>/dev/null || true
    tput ed 2>/dev/null || true

    echo "Save selection?"
    for ((i=0; i<${#options[@]}; i++)); do
      if [[ $i -eq $cursor ]]; then
        tput setaf 6
        if [[ $i -eq 0 ]]; then
          printf "  ➔ %s (%s)\n" "${options[$i]}" "$CURRENT_DIR"
        else
          printf "  ➔ %s\n" "${options[$i]}"
        fi
      else
        if [[ $i -eq 0 ]]; then
          printf "    %s (%s)\n" "${options[$i]}" "$CURRENT_DIR"
        else
          printf "    %s\n" "${options[$i]}"
        fi
      fi
      tput sgr0
    done

    IFS= read -rsn1 key
    if [[ "$key" == $'\x1b' ]]; then
      read -rsn2 -t 1 escape_seq
      case "$escape_seq" in
        '[A') ((cursor > 0)) && ((cursor--)) || true ;;
        '[B') ((cursor < 2)) && ((cursor++)) || true ;;
      esac
    else
      case "$key" in
        ''|$'\n'|$'\r') break ;;
        k) ((cursor > 0)) && ((cursor--)) || true ;;
        j) ((cursor < 2)) && ((cursor++)) || true ;;
      esac
    fi
  done

  tput cnorm
  trap - INT TERM EXIT

  SAVE_CHOICE=$cursor  # 0=workspace, 1=global, 2=don't save
}

# Network configuration
NETWORK_OPTIONS=""
HOST_ACCESS_ARGS="--add-host=host.docker.internal:host-gateway"
SELECTED_NETWORKS=()

if [[ "$NETWORK_FLAG" == "true" ]]; then
  if [[ -n "$NETWORK_ARG" ]]; then
    # Direct specification: -n net1,net2
    IFS=',' read -ra SELECTED_NETWORKS <<< "$NETWORK_ARG"
  elif [[ -t 0 ]]; then
    # Interactive mode: show menu
    show_network_menu

    if [[ ${#SELECTED_NETWORKS[@]} -gt 0 ]]; then
      show_save_prompt
      case $SAVE_CHOICE in
        0) write_network_config "workspace" "${SELECTED_NETWORKS[@]}" && echo "✅ Saved for this workspace" ;;
        1) write_network_config "global" "${SELECTED_NETWORKS[@]}" && echo "✅ Saved globally" ;;
        *) echo "ℹ️  Using for this session only" ;;
      esac
    fi
  else
    echo "⚠️  Non-interactive mode. Use -n network1,network2 to specify networks."
  fi
else
  # No flag: use saved config silently
  while IFS= read -r net; do
    [[ -n "$net" ]] && SELECTED_NETWORKS+=("$net")
  done < <(read_network_config)
fi

# Validate and build network options
if [[ ${#SELECTED_NETWORKS[@]} -gt 0 ]]; then
  while IFS= read -r net; do
    [[ -n "$net" ]] && NETWORK_OPTIONS="$NETWORK_OPTIONS --network $net"
  done < <(validate_networks "${SELECTED_NETWORKS[@]}")
fi

# Detect SSH agent socket (prefer agent forwarding over key copying)
get_ssh_agent_socket() {
  # macOS Docker Desktop: special socket path
  if [[ "$(uname)" == "Darwin" ]]; then
    local docker_sock="/run/host-services/ssh-auth.sock"
    # Docker Desktop forwards the host agent to this path inside the VM
    if [[ -n "$SSH_AUTH_SOCK" ]] && [[ -S "$SSH_AUTH_SOCK" ]]; then
      echo "$SSH_AUTH_SOCK"
      return 0
    fi
  fi
  # Standard SSH agent socket (Linux, macOS with regular agent)
  if [[ -n "$SSH_AUTH_SOCK" ]] && [[ -S "$SSH_AUTH_SOCK" ]]; then
    echo "$SSH_AUTH_SOCK"
    return 0
  fi
  return 1
}

# Mount SSH agent socket + known_hosts/config (no private keys)
setup_ssh_agent_forwarding() {
  local agent_sock="$1"

  # Mount the agent socket
  GIT_MOUNTS="$GIT_MOUNTS -v $agent_sock:/ssh-agent"
  SSH_AGENT_ENV="-e SSH_AUTH_SOCK=/ssh-agent"

  # Still need known_hosts and config for host verification
  mkdir -p "$GIT_CACHE_DIR/ssh"

  if [ -f "$HOME/.ssh/known_hosts" ]; then
    cp "$HOME/.ssh/known_hosts" "$GIT_CACHE_DIR/ssh/known_hosts" 2>/dev/null || true
    chmod 600 "$GIT_CACHE_DIR/ssh/known_hosts" 2>/dev/null || true
  fi

  if [ -f "$HOME/.ssh/config" ]; then
    cp "$HOME/.ssh/config" "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
    chmod 600 "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
  fi

  chmod 700 "$GIT_CACHE_DIR/ssh"
  GIT_MOUNTS="$GIT_MOUNTS -v $GIT_CACHE_DIR/ssh:/home/agent/.ssh:ro"

  echo "🔒 SSH agent forwarding active (keys never enter container)"
}

# Git access control (opt-in per workspace)
GIT_MOUNTS=""
SSH_AGENT_ENV=""
GIT_ALLOWED_FILE="$SANDBOX_DIR/git-allowed"
GIT_CACHE_DIR="$GIT_SHARED_DIR"  # V2: Uses shared/git instead of cache/git
touch "$GIT_ALLOWED_FILE" 2>/dev/null || true

# Check if Git access is allowed for this workspace (config.json or legacy file)
is_git_allowed() {
  local ws="$1"
  # Check config.json first
  if has_jq && [[ -f "$AI_SANDBOX_CONFIG" ]]; then
    local allowed=$(jq -r --arg ws "$ws" '.git.allowedWorkspaces | index($ws) // -1' "$AI_SANDBOX_CONFIG" 2>/dev/null)
    [[ "$allowed" -ge 0 ]] && return 0
  fi
  # Fallback to legacy file
  grep -q "^$ws$" "$GIT_ALLOWED_FILE" 2>/dev/null && return 0
  return 1
}

# Check if workspace is in fetch-only mode (config.json)
is_git_fetch_only() {
  local ws="$1"
  if has_jq && [[ -f "$AI_SANDBOX_CONFIG" ]]; then
    local fetch_only=$(jq -r --arg ws "$ws" '.git.fetchOnlyWorkspaces // [] | index($ws) // -1' "$AI_SANDBOX_CONFIG" 2>/dev/null)
    [[ "$fetch_only" -ge 0 ]] && return 0
  fi
  return 1
}

# Apply fetch-only restrictions by adding pushInsteadOf to gitconfig
apply_git_fetch_only() {
  local gitconfig_path="$HOME_DIR/.gitconfig"
  # Ensure .gitconfig exists
  touch "$gitconfig_path"
  # Append push-blocking config (redirects all push URLs to non-existent protocol)
  cat >> "$gitconfig_path" << 'FETCHONLY'

# AI Sandbox: Git fetch-only mode (push disabled)
[url "no-push://blocked"]
	pushInsteadOf = git@
	pushInsteadOf = ssh://
	pushInsteadOf = https://
	pushInsteadOf = http://
FETCHONLY
  echo "🔒 Git fetch-only mode: push operations are blocked"
}

GIT_FETCH_ONLY_MODE=false

# Check --git-fetch flag
if [[ "$GIT_FETCH_ONLY_FLAG" == "true" ]]; then
  GIT_FETCH_ONLY_MODE=true
fi

# Check if workspace is in fetch-only list
if is_git_fetch_only "$CURRENT_DIR"; then
  GIT_FETCH_ONLY_MODE=true
fi

# Check if Git access is allowed for this workspace
if is_git_allowed "$CURRENT_DIR" || is_git_fetch_only "$CURRENT_DIR" || [[ "$GIT_FETCH_ONLY_FLAG" == "true" ]]; then
  # Previously allowed for this workspace
  # Check if saved keys exist for this workspace
  WORKSPACE_MD5=$(echo "$CURRENT_DIR" | md5sum | cut -c1-8)
  SAVED_KEYS_FILE="$GIT_SHARED_DIR/keys/$WORKSPACE_MD5"  # V2: Uses shared/git/keys/

  if [ -f "$SAVED_KEYS_FILE" ]; then
    # Try SSH agent forwarding first (more secure)
    AGENT_SOCK=""
    if AGENT_SOCK=$(get_ssh_agent_socket); then
      echo "📋 Setting up Git credentials (agent forwarding)..."
      setup_ssh_agent_forwarding "$AGENT_SOCK"
      echo "✅ Git credentials ready (agent forwarding)"
    else
      # Fallback: copy key files (less secure)
      echo "📋 Syncing Git credentials..."
      echo "⚠️  SSH agent not detected. Falling back to key file mounting."
      echo "   For better security, start ssh-agent: eval \"\$(ssh-agent -s)\" && ssh-add"

      if [ -d "$GIT_CACHE_DIR/ssh" ]; then
        chmod -R 700 "$GIT_CACHE_DIR/ssh" 2>/dev/null || true
        rm -rf "$GIT_CACHE_DIR/ssh" 2>/dev/null || true
      fi
      mkdir -p "$GIT_CACHE_DIR/ssh"

      # Read saved keys and copy them
      SAVED_KEYS=()
      while IFS= read -r key; do
        [ -n "$key" ] && SAVED_KEYS+=("$key")
      done < "$SAVED_KEYS_FILE"

      for key in "${SAVED_KEYS[@]}"; do
        [ -z "$key" ] && continue
        src_file="$HOME/.ssh/$key"
        dst_file="$GIT_CACHE_DIR/ssh/$key"

        dst_dir=$(dirname "$dst_file")
        mkdir -p "$dst_dir" 2>/dev/null || true
        chmod 700 "$dst_dir" 2>/dev/null || true

        if [ -f "$src_file" ]; then
          cp "$src_file" "$dst_file" 2>/dev/null || true
          chmod 600 "$dst_file" 2>/dev/null || true
        fi
      done

      # Copy filtered SSH config (only hosts needed for this repo)
      if [ -f "$HOME/.ssh/config" ]; then
        SETUP_SSH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/setup-ssh-config"
        if [ -x "$SETUP_SSH" ]; then
          # Join SAVED_KEYS into a comma-separated string for --keys
          KEYS_ARG=$(IFS=,; echo "${SAVED_KEYS[*]}")
          output=$( "$SETUP_SSH" --keys "$KEYS_ARG" 2>&1 ) || true
          TEMP_CONFIG=$(echo "$output" | grep "Config:" | tail -1 | awk '{print $NF}')
          if [ -f "$TEMP_CONFIG" ]; then
            cp "$TEMP_CONFIG" "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
            chmod 600 "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
          else
            cp "$HOME/.ssh/config" "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
            chmod 600 "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
          fi
        else
          cp "$HOME/.ssh/config" "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
          chmod 600 "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
        fi
      fi

      if [ -f "$HOME/.ssh/known_hosts" ]; then
        cp "$HOME/.ssh/known_hosts" "$GIT_CACHE_DIR/ssh/known_hosts" 2>/dev/null || true
        chmod 600 "$GIT_CACHE_DIR/ssh/known_hosts" 2>/dev/null || true
      fi

      # Ensure all directories have correct permissions (recursive)
      chmod 700 "$GIT_CACHE_DIR/ssh"
      find "$GIT_CACHE_DIR/ssh" -type d -exec chmod 700 {} \;
      find "$GIT_CACHE_DIR/ssh" -type f ! -name "config" ! -name "known_hosts" -exec chmod 600 {} \;

      GIT_MOUNTS="$GIT_MOUNTS -v $GIT_CACHE_DIR/ssh:/home/agent/.ssh:ro"
      echo "✅ Git credentials synced"
    fi
  fi

  if [ -f "$HOME/.gitconfig" ]; then
    # Copy gitconfig to HOME_DIR (can't mount file inside mounted directory)
    cp "$HOME/.gitconfig" "$HOME_DIR/.gitconfig" 2>/dev/null || true
  fi

  # Apply fetch-only restrictions if mode is active
  if [[ "$GIT_FETCH_ONLY_MODE" == "true" ]]; then
    apply_git_fetch_only
  fi
else
  # Ask user if they want Git access for this workspace (only in interactive mode)
  if [[ -t 0 ]] && ([ -d "$HOME/.ssh" ] || [ -f "$HOME/.gitconfig" ]); then
    echo ""
    echo "🔐 Git Access Control"
    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    echo "Allow AI tool to access Git credentials for this workspace?"
    echo "Workspace: $CURRENT_DIR"
    echo ""
    echo "  1) Yes, allow once (this session only)"
    echo "  2) Yes, always allow for this workspace"
    echo "  3) No, keep Git disabled (secure default)"
    echo "  4) Fetch only - allow once (no push, this session)"
    echo "  5) Fetch only - always for this workspace (no push)"
    echo ""
    read -p "Choice [1-5]: " git_choice

    case "$git_choice" in
      1|2|4|5)
        # Try SSH agent forwarding first (more secure - no key selection needed)
        AGENT_SOCK=""
        if AGENT_SOCK=$(get_ssh_agent_socket); then
          echo ""
          echo "🔒 SSH Agent Forwarding"
          echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
          setup_ssh_agent_forwarding "$AGENT_SOCK"

          # Copy gitconfig
          if [ -f "$HOME/.gitconfig" ]; then
            cp "$HOME/.gitconfig" "$HOME_DIR/.gitconfig" 2>/dev/null || true
          fi

          if [[ "$git_choice" == "4" || "$git_choice" == "5" ]]; then
            GIT_FETCH_ONLY_MODE=true
          fi

          # Save workspace preference (same logic as before)
          if [ "$git_choice" = "2" ]; then
            echo "$CURRENT_DIR" >> "$GIT_ALLOWED_FILE"
            if has_jq && [[ -f "$AI_SANDBOX_CONFIG" ]]; then
              jq --arg ws "$CURRENT_DIR" '.git.allowedWorkspaces += [$ws] | .git.allowedWorkspaces |= unique' "$AI_SANDBOX_CONFIG" > "$AI_SANDBOX_CONFIG.tmp" \
                && mv "$AI_SANDBOX_CONFIG.tmp" "$AI_SANDBOX_CONFIG"
              chmod 600 "$AI_SANDBOX_CONFIG"
            fi
            echo "✅ Git access enabled and saved for: $CURRENT_DIR"
          elif [ "$git_choice" = "5" ]; then
            if has_jq && [[ -f "$AI_SANDBOX_CONFIG" ]]; then
              jq --arg ws "$CURRENT_DIR" '.git.fetchOnlyWorkspaces = ((.git.fetchOnlyWorkspaces // []) + [$ws] | unique)' "$AI_SANDBOX_CONFIG" > "$AI_SANDBOX_CONFIG.tmp" \
                && mv "$AI_SANDBOX_CONFIG.tmp" "$AI_SANDBOX_CONFIG"
              chmod 600 "$AI_SANDBOX_CONFIG"
            fi
            echo "✅ Git fetch-only access enabled and saved for: $CURRENT_DIR"
          elif [ "$git_choice" = "4" ]; then
            echo "✅ Git fetch-only access enabled for this session"
          else
            echo "✅ Git access enabled for this session"
          fi
        else
          # No SSH agent — fall back to key selection + copy
          echo ""
          echo "⚠️  SSH agent not detected. Using key file mounting."
          echo "   For better security: eval \"\$(ssh-agent -s)\" && ssh-add"
          echo ""
          echo "🔑 SSH Key Selection"
          echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

          # Source the SSH key selector library
          # Resolve symlink to get actual project directory
          SCRIPT_PATH="${BASH_SOURCE[0]}"
          while [ -L "$SCRIPT_PATH" ]; do
            SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)"
            SCRIPT_PATH="$(readlink "$SCRIPT_PATH")"
            [[ $SCRIPT_PATH != /* ]] && SCRIPT_PATH="$SCRIPT_DIR/$SCRIPT_PATH"
          done
          SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)"
          source "$SCRIPT_DIR/../lib/ssh-key-selector.sh"

          # Let user select keys
          if select_ssh_keys; then
            if [ ${#SELECTED_SSH_KEYS[@]} -gt 0 ]; then
              echo "📋 Copying selected credentials to cache..."
              if [ -d "$GIT_CACHE_DIR/ssh" ]; then
                chmod -R 700 "$GIT_CACHE_DIR/ssh" 2>/dev/null || true
                rm -rf "$GIT_CACHE_DIR/ssh" 2>/dev/null || true
              fi
              mkdir -p "$GIT_CACHE_DIR/ssh"

              # Copy selected SSH keys (preserve directory structure exactly)
              for key in "${SELECTED_SSH_KEYS[@]}"; do
                echo "  → Copying $key..."
                src_file="$HOME/.ssh/$key"
                dst_file="$GIT_CACHE_DIR/ssh/$key"

                # Create parent directory with correct permissions
                dst_dir=$(dirname "$dst_file")
                mkdir -p "$dst_dir"
                chmod 700 "$dst_dir"

                # Copy the file and set permissions
                if [ -f "$src_file" ]; then
                  cp "$src_file" "$dst_file"
                  chmod 600 "$dst_file"
                else
                  echo "    ⚠️  Warning: $src_file not found, skipping"
                fi
              done

              # Copy filtered SSH config (only hosts needed for this repo)
              if [ -f "$HOME/.ssh/config" ]; then
                echo "  → Generating filtered SSH config..."
                # Run setup-ssh-config to get filtered config
                SETUP_SSH="$SCRIPT_DIR/setup-ssh-config"
                if [ -x "$SETUP_SSH" ]; then
                  # Join SELECTED_SSH_KEYS into a comma-separated string for --keys
                  KEYS_ARG=$(IFS=,; echo "${SELECTED_SSH_KEYS[*]}")
                  # Run it and capture the filtered config path
                  output=$( "$SETUP_SSH" --keys "$KEYS_ARG" 2>&1 ) || true
                  TEMP_CONFIG=$(echo "$output" | grep "Config:" | tail -1 | awk '{print $NF}')
                  if [ -f "$TEMP_CONFIG" ]; then
                    cp "$TEMP_CONFIG" "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
                    chmod 600 "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
                  else
                    # Fallback to copying full config if setup-ssh-config fails
                    cp "$HOME/.ssh/config" "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
                    chmod 600 "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
                  fi
                else
                  # Fallback: copy full config
                  cp "$HOME/.ssh/config" "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
                  chmod 600 "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
                fi
              fi

              if [ -f "$HOME/.ssh/known_hosts" ]; then
                echo "  → Copying known_hosts..."
                cp "$HOME/.ssh/known_hosts" "$GIT_CACHE_DIR/ssh/known_hosts" 2>/dev/null || true
                chmod 600 "$GIT_CACHE_DIR/ssh/known_hosts" 2>/dev/null || true
              fi

              # Ensure all directories and files have correct permissions (recursive)
              chmod 700 "$GIT_CACHE_DIR/ssh"
              find "$GIT_CACHE_DIR/ssh" -type d -exec chmod 700 {} \;
              find "$GIT_CACHE_DIR/ssh" -type f ! -name "config" ! -name "known_hosts" -exec chmod 600 {} \;

              GIT_MOUNTS="$GIT_MOUNTS -v $GIT_CACHE_DIR/ssh:/home/agent/.ssh:ro"

              # Copy gitconfig
              if [ -f "$HOME/.gitconfig" ]; then
                echo "  → Copying .gitconfig..."
                cp "$HOME/.gitconfig" "$HOME_DIR/.gitconfig" 2>/dev/null || true
              fi

              if [[ "$git_choice" == "4" || "$git_choice" == "5" ]]; then
                GIT_FETCH_ONLY_MODE=true
              fi
              if [ "$git_choice" = "2" ]; then
                # Save workspace and selected keys for future sessions
                echo "$CURRENT_DIR" >> "$GIT_ALLOWED_FILE"
                # Also save to config.json
                if has_jq && [[ -f "$AI_SANDBOX_CONFIG" ]]; then
                  jq --arg ws "$CURRENT_DIR" '.git.allowedWorkspaces += [$ws] | .git.allowedWorkspaces |= unique' "$AI_SANDBOX_CONFIG" > "$AI_SANDBOX_CONFIG.tmp" \
                    && mv "$AI_SANDBOX_CONFIG.tmp" "$AI_SANDBOX_CONFIG"
                  chmod 600 "$AI_SANDBOX_CONFIG"
                fi
                # Save selected keys (one per line for easier parsing)
                WORKSPACE_MD5=$(echo "$CURRENT_DIR" | md5sum | cut -c1-8)
                mkdir -p "$GIT_SHARED_DIR/keys"
                printf "%s\n" "${SELECTED_SSH_KEYS[@]}" > "$GIT_SHARED_DIR/keys/$WORKSPACE_MD5"
                echo "✅ Git access enabled and saved for: $CURRENT_DIR"
              elif [ "$git_choice" = "5" ]; then
                # Save workspace to fetchOnlyWorkspaces
                if has_jq && [[ -f "$AI_SANDBOX_CONFIG" ]]; then
                  jq --arg ws "$CURRENT_DIR" '.git.fetchOnlyWorkspaces = ((.git.fetchOnlyWorkspaces // []) + [$ws] | unique)' "$AI_SANDBOX_CONFIG" > "$AI_SANDBOX_CONFIG.tmp" \
                    && mv "$AI_SANDBOX_CONFIG.tmp" "$AI_SANDBOX_CONFIG"
                  chmod 600 "$AI_SANDBOX_CONFIG"
                fi
                # Save selected keys (one per line for easier parsing)
                WORKSPACE_MD5=$(echo "$CURRENT_DIR" | md5sum | cut -c1-8)
                mkdir -p "$GIT_SHARED_DIR/keys"
                printf "%s\n" "${SELECTED_SSH_KEYS[@]}" > "$GIT_SHARED_DIR/keys/$WORKSPACE_MD5"
                echo "✅ Git fetch-only access enabled and saved for: $CURRENT_DIR"
              elif [ "$git_choice" = "4" ]; then
                echo "✅ Git fetch-only access enabled for this session"
              else
                echo "✅ Git access enabled for this session"
              fi
            else
              echo "⚠️  No SSH keys selected. Git access disabled."
            fi
          else
            echo "⚠️  SSH key selection cancelled. Git access disabled."
          fi
        fi
        ;;
      *)
        # Default: no Git access
        echo "🔒 Git access disabled (secure mode)"
        ;;
    esac

    # Apply fetch-only restrictions if mode is active
    if [[ "$GIT_FETCH_ONLY_MODE" == "true" ]]; then
      apply_git_fetch_only
    fi
    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    echo ""
  fi
fi

# ============================================================================
# DISPLAY & CLIPBOARD CONFIGURATION
# ============================================================================
detect_display_config() {
  local detected_flags=""
  local has_display=false

  # 1. X11 Detection
  if [[ -n "$DISPLAY" ]]; then
    has_display=true
    detected_flags="$detected_flags -e DISPLAY=$DISPLAY"

    # Mount X11 socket (common on Linux/macOS with XQuartz)
    if [[ -d "/tmp/.X11-unix" ]]; then
      detected_flags="$detected_flags -v /tmp/.X11-unix:/tmp/.X11-unix:rw"
    fi

    # Handle XAuthority for authentication
    if [[ -f "$HOME/.Xauthority" ]]; then
      # Mount read-only for security
      # Mount to /tmp/.Xauthority to avoid conflict with /home/agent bind mount
      detected_flags="$detected_flags -e XAUTHORITY=/tmp/.Xauthority -v $HOME/.Xauthority:/tmp/.Xauthority:ro"
    fi

    # Since we have X11, prefer it over Wayland to avoid clipboard issues in mixed environments
    # (Many Go apps like atotto/clipboard work better with xclip/X11 than wl-copy/Wayland in Docker)
    echo "$detected_flags"
    return
  fi

  # 2. Wayland Detection
  if [[ -n "$WAYLAND_DISPLAY" ]]; then
    has_display=true
    detected_flags="$detected_flags -e WAYLAND_DISPLAY=$WAYLAND_DISPLAY"

    # Handle XDG_RUNTIME_DIR (needed for Wayland sockets)
    if [[ -n "$XDG_RUNTIME_DIR" ]]; then
      # Map host runtime dir to container's /run/user/1001 (agent UID)
      # ensuring the container can access the socket
      local container_runtime_dir="/run/user/1001"
      detected_flags="$detected_flags -e XDG_RUNTIME_DIR=$container_runtime_dir -v $XDG_RUNTIME_DIR:$container_runtime_dir:rw"
    fi
  fi

  # Debug log if no display detected (only if debug enabled)
  if [[ "$has_display" == "false" && "${AI_RUN_DEBUG:-}" == "1" ]]; then
    echo "🔧 Debug: No display server detected (DISPLAY/WAYLAND_DISPLAY unset). Clipboard integration disabled." >&2
  fi

  echo "$detected_flags"
}

# Generate container name based on tool and folder
# Format: {tool}-{sanitized_folder_name}-{random_suffix}
generate_container_name() {
  local folder_name=$(basename "$CURRENT_DIR")

  # Sanitize: keep only alphanumeric, hyphens, underscores
  # Replace spaces with hyphens, remove special chars
  folder_name=$(echo "$folder_name" | tr ' ' '-' | tr -cd '[:alnum:]_-' | tr '[:upper:]' '[:lower:]')

  # Limit length (max 50 chars for container name, leaving room for random suffix)
  if [[ ${#folder_name} -gt 40 ]]; then
    folder_name="${folder_name:0:40}"
  fi

  # Remove trailing hyphens/underscores
  folder_name=$(echo "$folder_name" | sed 's/[-_]*$//')

  # If empty after sanitization, use "workspace"
  if [[ -z "$folder_name" ]]; then
    folder_name="workspace"
  fi

  # Generate random 6-character suffix (hex)
  local random_suffix=$(openssl rand -hex 3)

  echo "${TOOL:-shell}-${folder_name}-${random_suffix}"
}

# ============================================================================
# OPENCODE CONTAINER LIFECYCLE HELPERS
# Deterministic naming + single-writer reuse for opencode containers.
# ============================================================================

# Return the deterministic container name for an opencode project hash.
# Usage: opencode_container_name "abc123..."
opencode_container_name() {
  local hash="$1"
  echo "ai-opencode-${hash}"
}

# Print container ID if an opencode container with the given name is running.
# Returns 0 (and prints the ID) if running, 1 if not.
# Usage: opencode_container_running "ai-opencode-abc123"
opencode_container_running() {
  local name="$1"
  local id
  id=$(docker ps -q -f "name=^${name}$" 2>/dev/null)
  if [[ -n "$id" ]]; then
    echo "$id"
    return 0
  fi
  return 1
}

# Print container ID if an opencode container exists but is stopped.
# Returns 0 (and prints the ID) if a stopped container exists, 1 otherwise.
# Usage: opencode_container_stopped "ai-opencode-abc123"
opencode_container_stopped() {
  local name="$1"
  local all_id
  all_id=$(docker ps -aq -f "name=^${name}$" 2>/dev/null)
  if [[ -n "$all_id" ]]; then
    if ! opencode_container_running "$name" >/dev/null; then
      echo "$all_id"
      return 0
    fi
  fi
  return 1
}

# Exec into an already-running opencode container.
# Validates workdir is mounted, prints reuse notice, runs docker exec.
# Returns the exit status of docker exec, or 1 on validation failure.
# Usage: exec_into_opencode_container "ai-opencode-abc123" "/path/to/workdir" args...
exec_into_opencode_container() {
  local name="$1"
  local workdir="$2"
  shift 2
  local tool_args=("$@")

  # NOTE: We previously validated $workdir against the container's bind-mount
  # sources via `docker inspect`, but this was too strict — Docker Desktop on
  # macOS canonicalizes mount source paths (e.g. /host_mnt/Users/...) so the
  # raw $PWD never matched, causing legitimate reuse to fail. We now let
  # `docker exec --workdir` itself fail with a clear error if the path is not
  # mounted in the running container; that's a better signal than our
  # second-guessing.
  echo "→ reusing $name (existing opencode container for this project)" >&2

  local exec_tty_flags=""
  if [[ -t 0 ]] && [[ -t 1 ]]; then
    exec_tty_flags="-it"
  fi

  # shellcheck disable=SC2086
  docker exec $exec_tty_flags --workdir "$workdir" "$name" opencode "${tool_args[@]}"
}

# Remove a stopped opencode container so a fresh one can be launched.
# Usage: cleanup_stopped_opencode_container "ai-opencode-abc123"
cleanup_stopped_opencode_container() {
  local name="$1"
  local stopped_id=""
  stopped_id=$(opencode_container_stopped "$name") || true
  if [[ -n "$stopped_id" ]]; then
    docker rm "$name" >/dev/null 2>&1 || true
  fi
}

# Container name and TTY allocation
CONTAINER_NAME=""
TTY_FLAGS="-it"  # Default to interactive mode

# Check if we have a proper TTY and are in interactive mode
if [[ ! -t 0 ]] || [[ ! -t 1 ]]; then
  # No TTY available or non-interactive mode
  TTY_FLAGS=""
  echo "⚠️  Non-interactive mode detected. Terminal interface may be limited."
elif [[ -n "$CI" ]] || [[ -n "$GITHUB_ACTIONS" ]]; then
  # CI environment - disable interactive features
  TTY_FLAGS=""
  echo "ℹ️  CI environment detected. Running in non-interactive mode."
fi

# Only set container name for interactive mode to avoid conflicts
if [[ -n "$TTY_FLAGS" ]]; then
  CONTAINER_NAME="--name $(generate_container_name)"
fi

# ============================================================================
# OPENCODE SERVER PASSWORD HANDLING
# ============================================================================
OPENCODE_PASSWORD_ENV=""

# Detect if running opencode web or serve command
is_opencode_web_mode() {
  [[ "$TOOL" == "opencode" ]] || return 1
  for arg in "${TOOL_ARGS[@]}"; do
    [[ "$arg" == "web" || "$arg" == "serve" ]] && return 0
  done
  return 1
}

# Resolve password from CLI flags, env vars, or interactive prompt
# Precedence: --password > --password-env > OPENCODE_SERVER_PASSWORD env > interactive/warning
resolve_opencode_password() {
  # 1. CLI --password flag (highest priority)
  if [[ -n "$SERVER_PASSWORD" ]]; then
    OPENCODE_PASSWORD_ENV="-e OPENCODE_SERVER_PASSWORD=$SERVER_PASSWORD"
    return 0
  fi

  # 2. CLI --password-env flag
  if [[ -n "$PASSWORD_ENV_VAR" ]]; then
    local env_value="${!PASSWORD_ENV_VAR:-}"
    if [[ -z "$env_value" ]]; then
      echo "❌ ERROR: Environment variable '$PASSWORD_ENV_VAR' is not set"
      exit 1
    fi
    OPENCODE_PASSWORD_ENV="-e OPENCODE_SERVER_PASSWORD=$env_value"
    return 0
  fi

  # 3. Existing OPENCODE_SERVER_PASSWORD environment variable
  if [[ -n "${OPENCODE_SERVER_PASSWORD:-}" ]]; then
    OPENCODE_PASSWORD_ENV="-e OPENCODE_SERVER_PASSWORD=$OPENCODE_SERVER_PASSWORD"
    return 0
  fi

  # 4. --allow-unsecured flag: skip prompt/warning, no password
  if [[ "$ALLOW_UNSECURED" == "true" ]]; then
    return 0
  fi

  # 5. Interactive mode: show menu
  if [[ -t 0 ]]; then
    echo ""
    echo "🔐 OpenCode Web Server Security"
    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    echo "The web server requires a password for security."
    echo ""
    echo "  1) Generate random password (recommended)"
    echo "  2) Enter custom password"
    echo "  3) No password (⚠️  unsecured - localhost only)"
    echo ""
    read -p "Choice [1-3]: " password_choice

    case "$password_choice" in
      1)
        local generated_password=$(openssl rand -base64 24 | tr -d '/+=' | head -c 24)
        OPENCODE_PASSWORD_ENV="-e OPENCODE_SERVER_PASSWORD=$generated_password"
        echo ""
        echo "🔑 Generated password: $generated_password"
        echo "👤 Username: opencode (default)"
        echo ""
        echo "To connect from another terminal:"
        echo "  OPENCODE_SERVER_PASSWORD='$generated_password' opencode attach http://localhost:4096"
        ;;
      2)
        read -sp "Enter password: " custom_password
        echo ""
        if [[ -n "$custom_password" ]]; then
          OPENCODE_PASSWORD_ENV="-e OPENCODE_SERVER_PASSWORD=$custom_password"
          echo "✅ Custom password set"
          echo "👤 Username: opencode (default)"
          echo ""
          echo "To connect from another terminal:"
          echo "  OPENCODE_SERVER_PASSWORD='<your-password>' opencode attach http://localhost:4096"
        else
          echo "⚠️  Empty password - server will be unsecured"
        fi
        ;;
      *)
        echo "⚠️  No password set - server is unsecured"
        echo "   Only use this for localhost access!"
        ;;
    esac
    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    echo ""
  else
    # 6. Non-interactive: show warning
    echo ""
    echo "🔐 OpenCode Web Server Security"
    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    echo "⚠️  OPENCODE_SERVER_PASSWORD not set - server is unsecured"
    echo "   Set OPENCODE_SERVER_PASSWORD environment variable for security"
    echo "   Or use --allow-unsecured to suppress this warning"
    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    echo ""
  fi
}

# Run password resolution if in web/serve mode
if is_opencode_web_mode; then
  resolve_opencode_password
fi

# ============================================================================
# MCP AUTO-CONFIGURATION FOR OPENCODE
# ============================================================================

# Check if MCP tool is installed (from config.json, set during setup.sh)
is_mcp_installed() {
  local mcp_tool="$1"
  if has_jq && [[ -f "$AI_SANDBOX_CONFIG" ]]; then
    jq -e --arg tool "$mcp_tool" '.mcp.installed | index($tool) != null' "$AI_SANDBOX_CONFIG" &>/dev/null
  else
    return 1
  fi
}

# Check if MCP is already configured in OpenCode config
is_mcp_configured() {
  local mcp_name="$1"
  local config_file="$HOME/.config/opencode/opencode.json"

  if [[ ! -f "$config_file" ]]; then
    return 1
  fi

  if has_jq; then
    jq -e --arg name "$mcp_name" '.mcp[$name] // empty' "$config_file" &>/dev/null
  else
    grep -q "\"$mcp_name\"" "$config_file" 2>/dev/null
  fi
}

# Add MCP configuration to OpenCode config
add_mcp_config() {
  local mcp_name="$1"
  local mcp_command="$2"
  local force="${3:-false}"
  local config_file="$HOME/.config/opencode/opencode.json"

  # Check if already configured and warn user
  if [[ "$force" != "true" ]] && is_mcp_configured "$mcp_name"; then
    echo ""
    echo "  ⚠️  WARNING: '$mcp_name' is already configured!"
    echo "     Reconfiguring will overwrite your existing settings."
    echo "     Any custom credentials or options will be lost."
    read -p "     Are you sure you want to overwrite? [y/N]: " overwrite_choice
    if [[ ! "$overwrite_choice" =~ ^[Yy]$ ]]; then
      echo "     ⏭️  Kept existing configuration"
      return 1
    fi
  fi

  mkdir -p "$(dirname "$config_file")"

  if [[ ! -f "$config_file" ]]; then
    # Create new config with MCP
    echo "{\"mcp\": {\"$mcp_name\": {\"type\": \"local\", \"command\": $mcp_command}}}" > "$config_file"
  elif has_jq; then
    # Add MCP to existing config
    jq --arg name "$mcp_name" --argjson cmd "$mcp_command" \
      '.mcp = (.mcp // {}) | .mcp[$name] = {"type": "local", "command": $cmd}' \
      "$config_file" > "$config_file.tmp" && mv "$config_file.tmp" "$config_file"
  else
    echo "⚠️  jq not found. Please manually add MCP configuration to $config_file"
    return 1
  fi
  chmod 600 "$config_file"
}

# Mark MCP as skipped for this workspace (don't ask again)
mark_mcp_skipped() {
  local skip_file="$SANDBOX_DIR/.mcp-skip-$WORKSPACE_MD5"
  date -Iseconds > "$skip_file" 2>/dev/null || date > "$skip_file"
}

# Check if MCP prompt was skipped for this workspace
is_mcp_skipped() {
  local skip_file="$SANDBOX_DIR/.mcp-skip-$WORKSPACE_MD5"
  [[ -f "$skip_file" ]]
}

# Configure MCP tools for OpenCode
configure_opencode_mcp() {
  # Only run for opencode tool in interactive mode
  [[ "$TOOL" != "opencode" ]] && return 0
  [[ ! -t 0 ]] && return 0

  local config_file="$HOME/.config/opencode/opencode.json"

  # Generate workspace hash for skip tracking
  WORKSPACE_MD5=$(echo "$CURRENT_DIR" | md5sum | cut -c1-8)

  # Check if already skipped for this workspace
  if is_mcp_skipped; then
    return 0
  fi

  # Detect installed MCP tools (from config.json, set during setup.sh)
  local chrome_installed=false
  local playwright_installed=false
  local chrome_configured=false
  local playwright_configured=false

  if is_mcp_installed "chrome-devtools"; then
    chrome_installed=true
    is_mcp_configured "chrome-devtools" && chrome_configured=true
  fi

  if is_mcp_installed "playwright"; then
    playwright_installed=true
    is_mcp_configured "playwright" && playwright_configured=true
  fi

  # Get host Chrome path if using playwright-host
  local PLAYWRIGHT_HOST_CHROME=""
  if command -v jq &>/dev/null && [[ -f "$AI_SANDBOX_CONFIG" ]]; then
    PLAYWRIGHT_HOST_CHROME=$(jq -r '.mcp.chromePath // empty' "$AI_SANDBOX_CONFIG" 2>/dev/null)
  fi

  # If no MCP tools installed in image, return
  if [[ "$chrome_installed" == "false" && "$playwright_installed" == "false" ]]; then
    return 0
  fi

  # If all installed tools are already configured, return silently
  local all_configured=true
  [[ "$chrome_installed" == "true" && "$chrome_configured" == "false" ]] && all_configured=false
  [[ "$playwright_installed" == "true" && "$playwright_configured" == "false" ]] && all_configured=false

  if [[ "$all_configured" == "true" ]]; then
    return 0
  fi

  # Build lists of configured and unconfigured tools
  local unconfigured=()
  local configured=()
  [[ "$chrome_installed" == "true" && "$chrome_configured" == "false" ]] && unconfigured+=("chrome-devtools")
  [[ "$chrome_installed" == "true" && "$chrome_configured" == "true" ]] && configured+=("chrome-devtools")
  [[ "$playwright_installed" == "true" && "$playwright_configured" == "false" ]] && unconfigured+=("playwright")
  [[ "$playwright_installed" == "true" && "$playwright_configured" == "true" ]] && configured+=("playwright")

  # Show prompt
  echo ""
  echo "🔌 MCP Tools Detected"
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

  # Show already configured tools
  if [[ ${#configured[@]} -gt 0 ]]; then
    echo "Already configured:"
    for tool in "${configured[@]}"; do
      case "$tool" in
        chrome-devtools)
          echo "  ✓ Chrome DevTools MCP"
          ;;
        playwright)
          if [[ -n "$PLAYWRIGHT_HOST_CHROME" ]] && [[ -f "$PLAYWRIGHT_HOST_CHROME" ]]; then
            echo "  ✓ Playwright MCP (host Chrome)"
          else
            echo "  ✓ Playwright MCP"
          fi
          ;;
      esac
    done
    echo ""
  fi

  # Show unconfigured tools
  if [[ ${#unconfigured[@]} -gt 0 ]]; then
    echo "Not yet configured:"
    for tool in "${unconfigured[@]}"; do
      case "$tool" in
        chrome-devtools)
          echo "  • Chrome DevTools MCP - browser automation + performance profiling"
          ;;
        playwright)
          if [[ -n "$PLAYWRIGHT_HOST_CHROME" ]] && [[ -f "$PLAYWRIGHT_HOST_CHROME" ]]; then
            echo "  • Playwright MCP (host Chrome) - use your installed Chrome browser"
          else
            echo "  • Playwright MCP - multi-browser automation"
          fi
          ;;
      esac
    done
    echo ""
  fi

  echo "Configure MCP tools in OpenCode?"
  echo "  1) Yes, configure all detected tools"
  echo "  2) Yes, let me choose which ones"
  echo "  3) No, skip (I'll configure manually)"
  echo "  4) No, don't ask again for this workspace"
  echo ""
  read -p "Choice [1-4]: " mcp_choice

  local configured_any=false

  case "$mcp_choice" in
    1)
      # Configure all (both unconfigured and offer to reconfigure configured ones)
      local all_tools=()
      [[ "$chrome_installed" == "true" ]] && all_tools+=("chrome-devtools")
      [[ "$playwright_installed" == "true" ]] && all_tools+=("playwright")

      for tool in "${all_tools[@]}"; do
        case "$tool" in
          chrome-devtools)
            # Skip static entry under host Chrome mode — per-container
            # chrome-devtools_port_<port> entry is registered at runtime.
            if [[ -n "$PLAYWRIGHT_HOST_CHROME" ]] && [[ -f "$PLAYWRIGHT_HOST_CHROME" ]]; then
              echo "  ℹ️  Chrome DevTools MCP entry will be registered per-container at runtime (host Chrome mode)."
              configured_any=true
            else
              if add_mcp_config "chrome-devtools" "[\"chrome-devtools-mcp\", \"--headless\", \"--isolated\", \"--executablePath\", \"$CHROMIUM_PATH\", \"--chrome-arg=--no-sandbox\"]"; then
                echo "  ✓ Configured Chrome DevTools MCP"
                configured_any=true
              fi
            fi
            ;;
          playwright)
            # Check if using host Chrome via CDP
            if [[ -n "$PLAYWRIGHT_HOST_CHROME" ]] && [[ -f "$PLAYWRIGHT_HOST_CHROME" ]]; then
              echo "  ℹ️  Playwright MCP entry will be registered per-container at runtime (host Chrome mode)."
              configured_any=true
            else
              # Use container Chromium
              if add_mcp_config "playwright" '["playwright-mcp", "--headless", "--browser", "chromium"]'; then
                echo "  ✓ Configured Playwright MCP"
                configured_any=true
              fi
            fi
            ;;
        esac
      done
      ;;
    2)
      # Let user choose from all installed tools
      local all_tools=()
      [[ "$chrome_installed" == "true" ]] && all_tools+=("chrome-devtools")
      [[ "$playwright_installed" == "true" ]] && all_tools+=("playwright")

      for tool in "${all_tools[@]}"; do
        local tool_desc=""
        local status_hint=""
        case "$tool" in
          chrome-devtools)
            tool_desc="Chrome DevTools MCP (browser automation + DevTools)"
            is_mcp_configured "chrome-devtools" && status_hint=" [already configured]"
            ;;
          playwright)
            tool_desc="Playwright MCP (multi-browser automation)"
            is_mcp_configured "playwright" && status_hint=" [already configured]"
            ;;
        esac
        read -p "  Configure $tool_desc$status_hint? [y/N]: " tool_choice
        if [[ "$tool_choice" =~ ^[Yy]$ ]]; then
          case "$tool" in
            chrome-devtools)
              if [[ -n "$PLAYWRIGHT_HOST_CHROME" ]] && [[ -f "$PLAYWRIGHT_HOST_CHROME" ]]; then
                echo "    ℹ️  Chrome DevTools MCP entry will be registered per-container at runtime (host Chrome mode)."
                configured_any=true
              else
                if add_mcp_config "chrome-devtools" "[\"chrome-devtools-mcp\", \"--headless\", \"--isolated\", \"--executablePath\", \"$CHROMIUM_PATH\", \"--chrome-arg=--no-sandbox\"]"; then
                  echo "    ✓ Configured"
                fi
              fi
              ;;
            playwright)
              # Check if using host Chrome via CDP
              if [[ -n "$PLAYWRIGHT_HOST_CHROME" ]] && [[ -f "$PLAYWRIGHT_HOST_CHROME" ]]; then
                echo "  ℹ️  Playwright MCP entry will be registered per-container at runtime (host Chrome mode)."
                configured_any=true
              else
                # Use container Chromium
                if add_mcp_config "playwright" '["playwright-mcp", "--headless", "--browser", "chromium"]'; then
                  echo "    ✓ Configured"
                  configured_any=true
                fi
              fi
              ;;
          esac
        else
          echo "    ⏭️  Skipped"
        fi
      done
      ;;
    4)
      mark_mcp_skipped
      echo "ℹ️  Won't ask again for this workspace"
      ;;
    *)
      echo "ℹ️  Skipped."
      ;;
  esac

  # Show config file path for easy editing
  echo ""
  if [[ "$configured_any" == "true" ]]; then
    echo "✅ MCP configuration saved!"
  fi
  echo "📄 Config file: $config_file"
  echo "   Edit this file to customize MCP settings or add credentials."
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
  echo ""
}

# Run MCP configuration check for opencode
configure_opencode_mcp

# ============================================================================
# WEB COMMAND DETECTION AND PORT EXPOSURE
# ============================================================================

# Detect if running opencode web command
detect_opencode_web() {
  [[ "$TOOL" == "opencode" ]] || return 1
  for arg in "${TOOL_ARGS[@]}"; do
    [[ "$arg" == "web" ]] && return 0
  done
  return 1
}

# Parse --port value from TOOL_ARGS (supports --port N and --port=N)
parse_port_from_args() {
  local i=0
  while [[ $i -lt ${#TOOL_ARGS[@]} ]]; do
    local arg="${TOOL_ARGS[$i]}"
    if [[ "$arg" == "--port" ]]; then
      ((i++))
      if [[ $i -lt ${#TOOL_ARGS[@]} ]]; then
        echo "${TOOL_ARGS[$i]}"
        return 0
      fi
    elif [[ "$arg" =~ ^--port=(.+)$ ]]; then
      echo "${BASH_REMATCH[1]}"
      return 0
    fi
    ((i++))
  done
  return 1
}

# Check if --hostname is already in TOOL_ARGS
has_hostname_arg() {
  for arg in "${TOOL_ARGS[@]}"; do
    [[ "$arg" == "--hostname" || "$arg" =~ ^--hostname= ]] && return 0
  done
  return 1
}

# Check if port is in use (lsof with netstat fallback)
check_port_in_use() {
  local port="$1"
  if command -v lsof &>/dev/null; then
    lsof -i ":$port" &>/dev/null && return 0
  elif command -v netstat &>/dev/null; then
    netstat -tuln 2>/dev/null | grep -q ":$port " && return 0
  else
    return 2
  fi

  docker ps --format "{{.Ports}}" 2>/dev/null | grep -q ":$port->" && return 0
  return 1
}

# Get process info for port
get_port_process_info() {
  local port="$1"
  if command -v lsof &>/dev/null; then
    lsof -i ":$port" -sTCP:LISTEN 2>/dev/null | awk 'NR==2 {print $1 " (PID: " $2 ")"}'
  elif command -v netstat &>/dev/null; then
    echo "unknown process"
  else
    echo "unknown"
  fi
}

# Initialize port list (bash 3.x compatible - no associative arrays)
EXPOSE_PORTS_LIST=""
WEB_PORT=""

# Helper: add port to list if not already present
add_port_to_list() {
  local port="$1"
  local source="$2"
  if [[ ! " $EXPOSE_PORTS_LIST " =~ " $port " ]]; then
    EXPOSE_PORTS_LIST="$EXPOSE_PORTS_LIST $port"
    if [[ "${AI_RUN_DEBUG:-}" == "1" ]]; then
      echo "🔧 Debug: Added port $port from $source"
    fi
  fi
}

# Parse --expose flag
if [[ -n "$EXPOSE_ARG" ]]; then
  IFS=',' read -ra EXPOSE_PORTS <<< "$EXPOSE_ARG"
  for port in "${EXPOSE_PORTS[@]}"; do
    port=$(echo "$port" | tr -d ' ')
    if [[ "$port" =~ ^[0-9]+$ ]] && [ "$port" -ge 1 ] && [ "$port" -le 65535 ]; then
      add_port_to_list "$port" "--expose"
    else
      echo "⚠️  WARNING: Invalid port number in --expose: $port (skipped)"
    fi
  done
fi

# Web command detection and auto-exposure
WEB_DETECTED=false
if detect_opencode_web; then
  WEB_DETECTED=true
  WEB_PORT=$(parse_port_from_args)
  if [[ -z "$WEB_PORT" ]]; then
    WEB_PORT=4096
  fi

  add_port_to_list "$WEB_PORT" "auto-detected"
  echo "🌐 Detected web command. Auto-exposing port $WEB_PORT."

  if ! has_hostname_arg; then
    TOOL_ARGS+=("--hostname" "0.0.0.0")
  fi
fi

# Handle legacy PORT environment variable
if [[ -n "${PORT:-}" ]]; then
  echo "⚠️  WARNING: PORT environment variable is deprecated. Use --expose flag instead."
  IFS=',' read -ra PORTS <<< "$PORT"
  for port in "${PORTS[@]}"; do
    port=$(echo "$port" | tr -d ' ')
    if [[ "$port" =~ ^[0-9]+$ ]] && [ "$port" -ge 1 ] && [ "$port" -le 65535 ]; then
      add_port_to_list "$port" "PORT env"
    else
      echo "⚠️  WARNING: Invalid port number in PORT: $port (skipped)"
    fi
  done
fi

# Trim leading space from port list
EXPOSE_PORTS_LIST=$(echo "$EXPOSE_PORTS_LIST" | sed 's/^ //')

# Port conflict detection
PORT_CHECK_AVAILABLE=true
if ! command -v lsof &>/dev/null && ! command -v netstat &>/dev/null; then
  echo "⚠️  WARNING: Cannot check port availability (lsof/netstat not found)"
  PORT_CHECK_AVAILABLE=false
fi

if [[ "$PORT_CHECK_AVAILABLE" == "true" && -n "$EXPOSE_PORTS_LIST" ]]; then
  for port in $EXPOSE_PORTS_LIST; do
    if check_port_in_use "$port"; then
      PROCESS_INFO=$(get_port_process_info "$port")
      echo "❌ ERROR: Port $port is already in use by $PROCESS_INFO"
      exit 1
    fi
  done
fi

# Build PORT_MAPPINGS from EXPOSE_PORTS_LIST
if [[ -n "$EXPOSE_PORTS_LIST" ]]; then
  PORT_BIND="${PORT_BIND:-localhost}"
  BIND_ADDR="127.0.0.1"

  if [[ "$PORT_BIND" == "all" ]]; then
    BIND_ADDR="0.0.0.0"
    echo "⚠️  WARNING: Ports will be accessible from network (PORT_BIND=all)"
  fi

  for port in $EXPOSE_PORTS_LIST; do
    PORT_MAPPINGS="$PORT_MAPPINGS -p $BIND_ADDR:$port:$port"
  done

  echo "🔌 Port mappings: $EXPOSE_PORTS_LIST"

  if [[ "$WEB_DETECTED" == "true" ]]; then
    echo "🌐 Web UI available at http://localhost:$WEB_PORT"
  fi
fi

# Debug output (only in verbose mode)
if [[ "${AI_RUN_DEBUG:-}" == "1" ]]; then
  echo "🔧 Debug: TTY_FLAGS='$TTY_FLAGS'"
  echo "🔧 Debug: CONTAINER_NAME='$CONTAINER_NAME'"
  echo "🔧 Debug: IMAGE='$IMAGE'"
  echo "🔧 Debug: SHELL_MODE='$SHELL_MODE'"
  echo "🔧 Debug: TOOL_ARGS='${TOOL_ARGS[@]}'"
  echo "🔧 Debug: PORT='${PORT:-}'"
  echo "🔧 Debug: PORT_BIND='${PORT_BIND:-localhost}'"
  echo "🔧 Debug: PORT_MAPPINGS='$PORT_MAPPINGS'"
  echo "🔧 Debug: WEB_DETECTED='$WEB_DETECTED'"
  echo "🔧 Debug: EXPOSE_PORTS_LIST='$EXPOSE_PORTS_LIST'"
  echo "🔧 Debug: RG_COMPAT_MOUNT='$RG_COMPAT_MOUNT'"
fi

is_nano_brain_command() {
  if [[ "$TOOL" == "nano-brain" ]]; then
    return 0
  fi

  if [[ "$TOOL" == "npx" ]]; then
    for arg in "${TOOL_ARGS[@]}"; do
      if [[ "$arg" == "nano-brain" ]]; then
        return 0
      fi
    done
  fi

  return 1
}

NANO_BRAIN_AUTOREPAIR_SCRIPT='
set -e
ORIG_CMD=("$@")
REPAIR_PATTERN="(tree-sitter|native binding|Cannot find module.*tree-sitter|compiled against a different Node.js version|Exec format error|invalid ELF header|Native bindings not available)"

echo "🔎 nano-brain preflight: checking runtime cache paths..."
mkdir -p /home/agent/.npm /home/agent/.cache/node-gyp 2>/dev/null || true

run_with_capture() {
  local err_file
  err_file=$(mktemp)

  set +e
  "${ORIG_CMD[@]}" 2>"$err_file"
  local exit_code=$?
  set -e

  if [[ $exit_code -ne 0 ]] && grep -Eqi "$REPAIR_PATTERN" "$err_file"; then
    cat "$err_file" >&2
    echo "⚠️  Detected nano-brain native module issue."
    echo "🔧 Running automatic repair (rebuilding native modules for container arch)..."
    rm -rf /home/agent/.npm/_npx /home/agent/.cache/node-gyp 2>/dev/null || true
    npm cache clean --force >/dev/null 2>&1 || true
    # Root cause: host macOS prebuilds are wrong arch for Linux container.
    # Fix: delete wrong-arch prebuilds and reinstall to compile from source.
    local NB_DIR
    NB_DIR=$(dirname "$(dirname "$(readlink -f "$(which npx)")")")/lib/node_modules/nano-brain 2>/dev/null || true
    if [[ -z "$NB_DIR" || ! -d "$NB_DIR" ]]; then
      # npx cache location
      NB_DIR=$(find /home/agent/.npm/_npx -maxdepth 4 -name "nano-brain" -type d 2>/dev/null | head -1)
    fi
    if [[ -n "$NB_DIR" && -d "$NB_DIR/node_modules" ]]; then
      echo "  📦 Rebuilding tree-sitter in $NB_DIR..."
      for pkg in tree-sitter-typescript tree-sitter-javascript tree-sitter-python; do
        local prebuild_dir="$NB_DIR/node_modules/$pkg/prebuilds/linux-arm64"
        if [[ -d "$prebuild_dir" ]]; then
          rm -rf "$prebuild_dir"
        fi
      done
      (cd "$NB_DIR" && npm rebuild tree-sitter tree-sitter-typescript tree-sitter-javascript tree-sitter-python 2>/dev/null) || true
    fi
    echo "🔁 Retrying nano-brain command once..."
    set +e
    "${ORIG_CMD[@]}"
    local retry_code=$?
    set -e
    rm -f "$err_file"
    return $retry_code
  fi

  if [[ $exit_code -eq 0 ]] && grep -Eqi "(\\[treesitter\\] Native bindings not available|symbol graph disabled|tree-sitter-typescript\\.node|No such file or directory)" "$err_file"; then
    if [[ "${AI_RUN_DEBUG:-}" == "1" ]]; then
      echo "ℹ️  nano-brain: non-fatal tree-sitter warning captured." >&2
      cat "$err_file" >&2
    else
      grep -Eiv "(\\[treesitter\\] Native bindings not available|symbol graph disabled|tree-sitter-typescript\\.node|No such file or directory)" "$err_file" >&2 || true
    fi
    rm -f "$err_file"
    return 0
  fi

  cat "$err_file" >&2
  rm -f "$err_file"
  return $exit_code
}

run_with_capture
'

NANO_BRAIN_SHELL_HOOK=$(cat <<'EOF'
nano_brain_shell_wrapper() {
  local ORIG_CMD=("$@")
  local REPAIR_PATTERN="(tree-sitter|native binding|Cannot find module.*tree-sitter|compiled against a different Node.js version|Exec format error|invalid ELF header|Native bindings not available)"
  local WARN_PATTERN="(\\[treesitter\\] Native bindings not available|symbol graph disabled|tree-sitter-typescript\\.node|No such file or directory)"

  local err_file
  err_file=$(mktemp)

  set +e
  "${ORIG_CMD[@]}" 2>"$err_file"
  local exit_code=$?
  set -e

  if [[ $exit_code -ne 0 ]] && grep -Eqi "$REPAIR_PATTERN" "$err_file"; then
    cat "$err_file" >&2
    echo "⚠️  Detected nano-brain native module issue."
    echo "🔧 Running automatic repair (rebuilding native modules for container arch)..."
    rm -rf /home/agent/.npm/_npx /home/agent/.cache/node-gyp 2>/dev/null || true
    npm cache clean --force >/dev/null 2>&1 || true
    # Root cause: host macOS prebuilds are wrong arch for Linux container.
    # Fix: delete wrong-arch prebuilds and reinstall to compile from source.
    local NB_DIR
    NB_DIR=$(dirname "$(dirname "$(readlink -f "$(which npx)")")")/lib/node_modules/nano-brain 2>/dev/null || true
    if [[ -z "$NB_DIR" || ! -d "$NB_DIR" ]]; then
      NB_DIR=$(find /home/agent/.npm/_npx -maxdepth 4 -name "nano-brain" -type d 2>/dev/null | head -1)
    fi
    if [[ -n "$NB_DIR" && -d "$NB_DIR/node_modules" ]]; then
      echo "  📦 Rebuilding tree-sitter in $NB_DIR..."
      for pkg in tree-sitter-typescript tree-sitter-javascript tree-sitter-python; do
        local prebuild_dir="$NB_DIR/node_modules/$pkg/prebuilds/linux-arm64"
        if [[ -d "$prebuild_dir" ]]; then
          rm -rf "$prebuild_dir"
        fi
      done
      (cd "$NB_DIR" && npm rebuild tree-sitter tree-sitter-typescript tree-sitter-javascript tree-sitter-python 2>/dev/null) || true
    fi
    echo "🔁 Retrying nano-brain command once..."
    "${ORIG_CMD[@]}"
    local retry_code=$?
    rm -f "$err_file"
    return $retry_code
  fi

  if [[ $exit_code -eq 0 ]] && grep -Eqi "$WARN_PATTERN" "$err_file"; then
    if [[ "${AI_RUN_DEBUG:-}" == "1" ]]; then
      echo "ℹ️  nano-brain: non-fatal tree-sitter warning captured." >&2
      cat "$err_file" >&2
    else
      grep -Eiv "$WARN_PATTERN" "$err_file" >&2 || true
    fi
    rm -f "$err_file"
    return 0
  fi

  cat "$err_file" >&2
  rm -f "$err_file"
  return $exit_code
}

npx() {
  if [[ "${1:-}" == "nano-brain" ]]; then
    nano_brain_shell_wrapper command npx "$@"
    return $?
  fi
  command npx "$@"
}

export -f nano_brain_shell_wrapper npx
EOF
)

# Prepare command based on mode
ENTRYPOINT_OVERRIDE=""
if [[ -n "$TOOL" && "$SHELL_MODE" != "true" ]]; then
  if [[ "$TOOL" == "opencode" && -n "$OPENCODE_PROJECT_HASH" ]]; then
    # DB isolation is handled via file-level bind mounts (append_opencode_db_mounts).
    # Run opencode directly — no bash wrapper or symlink needed.
    ENTRYPOINT_OVERRIDE="--entrypoint opencode"
    DOCKER_COMMAND=("${TOOL_ARGS[@]}")
  else
    # Direct tool execution: override entrypoint to the tool
    ENTRYPOINT_OVERRIDE="--entrypoint $TOOL"
    DOCKER_COMMAND=("${TOOL_ARGS[@]}")
  fi
elif [[ "$SHELL_MODE" == "true" || -z "$TOOL" ]]; then
  # Shell mode (explicit --shell or no tool specified)
  ENTRYPOINT_OVERRIDE="--entrypoint bash"
  # Build welcome message with installed tools
  INSTALLED_TOOLS_MSG=""
  if command -v jq &>/dev/null && [[ -f "$SANDBOX_DIR/config.json" ]]; then
    INSTALLED_TOOLS_MSG=$(jq -r '.tools.installed // [] | join(", ")' "$SANDBOX_DIR/config.json" 2>/dev/null || echo "")
  fi
  if [[ -n "$TOOL" ]]; then
    # Shell mode with specific tool (ai-run claude --shell)
    DOCKER_COMMAND=(
      "-c"
      "echo ''; echo '🚀 AI Tool Container - Interactive Shell'; echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'; echo ''; echo \"Tool available: ${TOOL}\"; echo 'Run the tool: ${TOOL}'; echo 'Exit container: exit or Ctrl+D'; echo ''; echo \"Additional tools:\"; echo '  - specify (spec-kit): Spec-driven development'; echo '  - uipro (ux-ui-promax): UI/UX design intelligence'; echo '  - openspec: OpenSpec workflow'; echo ''; echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'; echo ''; exec bash"
    )
  else
    # Shell mode without tool (ai-run with no args)
    DOCKER_COMMAND=(
      "-c"
      "echo ''; echo '🚀 AI Sandbox - Interactive Shell'; echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'; echo ''; echo 'Installed tools: ${INSTALLED_TOOLS_MSG:-unknown}'; echo ''; echo 'Run any tool by name: claude, opencode, gemini, etc.'; echo 'Exit: exit or Ctrl+D'; echo ''; echo 'Enhancement tools: specify, uipro, openspec, rtk'; echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'; echo ''; exec bash"
    )
  fi
else
  # Fallback: direct mode with image's default entrypoint
  DOCKER_COMMAND=("${TOOL_ARGS[@]}")
fi

# Nano-brain targeted preflight + auto-repair wrapper
if [[ "$SHELL_MODE" == "true" ]] && [[ "$NANO_BRAIN_AUTO_REPAIR" == "true" ]] && [[ "${DOCKER_COMMAND[0]:-}" == "-c" ]]; then
  # Separate hook from following command with newline so bash parses them
  # as distinct statements. Without this, the hook's trailing `export -f`
  # line gets joined with the next `echo` call, producing:
  #   export -f nano_brain_shell_wrapper npx echo ''; echo '...'
  # which makes bash try to `export -f echo` (a builtin, not a function)
  # and `export -f ''` (empty name), emitting two harmless but ugly errors:
  #   bash: line 68: export: echo: not a function
  #   bash: line 68: export: : not a function
  DOCKER_COMMAND[1]="$NANO_BRAIN_SHELL_HOOK"$'\n'"${DOCKER_COMMAND[1]}"
fi

if [[ "$SHELL_MODE" != "true" ]] && is_nano_brain_command; then
  if [[ "$NANO_BRAIN_AUTO_REPAIR" == "true" ]]; then
    ENTRYPOINT_OVERRIDE="--entrypoint bash"
    DOCKER_COMMAND=("-lc" "$NANO_BRAIN_AUTOREPAIR_SCRIPT" "nano-brain-wrapper" "$TOOL" "${TOOL_ARGS[@]}")
  else
    echo "ℹ️  nano-brain auto-repair disabled"
  fi
fi

# Detect platform architecture (avoid slow emulation)
PLATFORM="${AI_RUN_PLATFORM:-}"
if [[ -z "$PLATFORM" ]]; then
  case "$(uname -m)" in
    x86_64)
      PLATFORM="linux/amd64"
      ;;
    aarch64|arm64)
      PLATFORM="linux/arm64"
      ;;
    *)
      PLATFORM="linux/$(uname -m)"
      ;;
  esac
fi

# Terminal size for TUI apps (important for opencode, aider, etc.)
TERMINAL_SIZE=""
if [[ -n "$TTY_FLAGS" ]]; then
  # Get current terminal size
  TERM_COLS=$(tput cols 2>/dev/null || echo "120")
  TERM_LINES=$(tput lines 2>/dev/null || echo "40")
  TERMINAL_SIZE="-e COLUMNS=$TERM_COLS -e LINES=$TERM_LINES"
fi

# Ensure sandbox directories are writable on host before mounting
# (Docker Desktop UID/GID mapping). These two dirs are the bind-mount roots
# (home -> /home/agent, shared/git), so chmod'ing the roots is what the mount
# actually needs. Do NOT recurse: a recursive walk has to stat all ~600k
# entries under home (the .cache/go/.npm trees dominate) and cost ~15-18s
# every run for a pure no-op -- those files already carry owner-write and
# the deep caches are container-owned. Non-recursive keeps it instant.
mkdir -p "$HOME_DIR" "$GIT_SHARED_DIR"
chmod u+w "$HOME_DIR" "$GIT_SHARED_DIR" 2>/dev/null || true

if [[ ! -f "$CACHE_DIR/playwright-browsers/.seeded" ]]; then
  if docker run --rm "$IMAGE" test -d /opt/playwright-browsers 2>/dev/null; then
    echo "🔄 Seeding shared Playwright browser cache..."
    docker run --rm -v "$CACHE_DIR/playwright-browsers":/export "$IMAGE" \
      cp -a /opt/playwright-browsers/. /export/ 2>/dev/null && \
      touch "$CACHE_DIR/playwright-browsers/.seeded" && \
      echo "✅ Playwright browsers cached" || \
      echo "⚠️  Playwright browser seeding failed (will retry next run)"
  else
    touch "$CACHE_DIR/playwright-browsers/.seeded"
  fi
fi

# Resolve the latest chromium binary path from shared cache
# The /opt/chromium symlink inside the image may point to a stale version
CHROMIUM_PATH="/opt/chromium"
LATEST_CHROMIUM=$(ls -d "$CACHE_DIR/playwright-browsers"/chromium-*/chrome-linux/chrome 2>/dev/null | sort -V | tail -1)
if [[ -n "$LATEST_CHROMIUM" ]]; then
  # Convert host cache path to container path
  CHROMIUM_PATH="/opt/playwright-browsers/$(echo "$LATEST_CHROMIUM" | grep -oE 'chromium-[^/]+')/chrome-linux/chrome"
fi

# Detect display configuration (clipboard integration)
DISPLAY_FLAGS=$(detect_display_config)

# ============================================================================
# OPENCLAW DOCKER-COMPOSE MODE
# ============================================================================
if [[ "$TOOL" == "openclaw" ]]; then
  OPENCLAW_REPO_DIR="$HOME/.ai-sandbox/tools/openclaw/repo"

  if [[ ! -d "$OPENCLAW_REPO_DIR" ]]; then
    echo "❌ ERROR: OpenClaw repository not found at $OPENCLAW_REPO_DIR"
    echo "   Run: npx @nano-step/ai-sandbox-wrapper setup"
    exit 1
  fi

  cd "$OPENCLAW_REPO_DIR"

  OPENCLAW_COMPOSE_FILE="$OPENCLAW_REPO_DIR/docker-compose.yml"
  OPENCLAW_OVERRIDE_FILE="$OPENCLAW_REPO_DIR/docker-compose.override.yml"

  echo "🔄 Generating OpenClaw docker-compose override..."

  cat > "$OPENCLAW_OVERRIDE_FILE" <<EOF
services:
  openclaw-gateway:
    environment:
      HOME: /home/node
      OPENCLAW_GATEWAY_TOKEN: \${OPENCLAW_GATEWAY_TOKEN:-}
    volumes:
      - $HOME/.openclaw:/home/node/.openclaw
EOF

  for workspace in "${WORKSPACES[@]}"; do
    echo "      - $workspace:$workspace" >> "$OPENCLAW_OVERRIDE_FILE"
  done

  cat >> "$OPENCLAW_OVERRIDE_FILE" <<EOF
    ports:
      - "18789:18789"
      - "18790:18790"
    working_dir: $CURRENT_DIR
EOF

  if [[ -n "$NETWORK_OPTIONS" ]]; then
    echo "    networks:" >> "$OPENCLAW_OVERRIDE_FILE"
    for net in ${DOCKER_NETWORKS//,/ }; do
      echo "      - $net" >> "$OPENCLAW_OVERRIDE_FILE"
    done
    echo "" >> "$OPENCLAW_OVERRIDE_FILE"
    echo "networks:" >> "$OPENCLAW_OVERRIDE_FILE"
    for net in ${DOCKER_NETWORKS//,/ }; do
      echo "  $net:" >> "$OPENCLAW_OVERRIDE_FILE"
      echo "    external: true" >> "$OPENCLAW_OVERRIDE_FILE"
    done
  fi

  echo "🚀 Starting OpenClaw with docker-compose..."
  echo "🌐 Gateway: http://localhost:18789"
  echo "🌐 Bridge: http://localhost:18790"
  echo ""

  exec docker compose -f "$OPENCLAW_COMPOSE_FILE" -f "$OPENCLAW_OVERRIDE_FILE" \
    --env-file "$ENV_FILE" \
    up --remove-orphans
fi

# ============================================================================
# OPENCODE DB ISOLATION: container reuse + deterministic naming
# Must run BEFORE DOCKER_ARGS assembly so CONTAINER_NAME can be overridden.
# ============================================================================
OPENCODE_PROJECT_HASH=""
OPENCODE_SKIP_RM=false  # When true, omit --rm from docker run

# OPENCODE_DB_ISOLATION=0 disables per-project DB isolation for opencode.
# Container falls back to legacy behavior: global mount, random container name,
# --rm flag. Useful for users who prefer the unified global session DB or want
# to roll back without downgrading the package version.
if [[ "$TOOL" == "opencode" && "${OPENCODE_DB_ISOLATION:-1}" != "0" ]]; then
  # 1. One-time backup of pre-existing global SQLite files
  ensure_opencode_backup

  # 2. Compute project hash and deterministic container name
  OPENCODE_PROJECT_HASH=$(compute_opencode_project_hash "$CURRENT_DIR")
  OPENCODE_CONTAINER_NAME=$(opencode_container_name "$OPENCODE_PROJECT_HASH")

  # 3. If a running container exists, exec into it and exit
  local_running_id=""
  local_running_id=$(opencode_container_running "$OPENCODE_CONTAINER_NAME") || true
  if [[ -n "$local_running_id" ]]; then
    # Check WAL/SHM BEFORE ensure_opencode_db_files() recreates them.
    # SQLite deletes these files after a clean database close; the host now has
    # new inodes at those paths, but the running container's bind mounts still
    # point to the old deleted inodes (visible as nlink=0 inside the container).
    # Exec-ing into it gives SQLite a stale WAL/SHM → "unable to open database file".
    _oc_db_dir=$(opencode_db_dir "$OPENCODE_PROJECT_HASH")
    if [[ ! -f "$_oc_db_dir/opencode.db-wal" ]] || [[ ! -f "$_oc_db_dir/opencode.db-shm" ]]; then
      echo "→ per-project WAL/SHM missing (SQLite checkpoint), restarting container to restore mounts..." >&2
      docker rm -f "$OPENCODE_CONTAINER_NAME" >/dev/null 2>&1 || true
      # Fall through to step 4 below for fresh run
    else
      exec_into_opencode_container "$OPENCODE_CONTAINER_NAME" "$CURRENT_DIR" "${TOOL_ARGS[@]}"
      exit $?
    fi
  fi

  # 4. Ensure per-project SQLite placeholders exist on host
  ensure_opencode_db_files "$OPENCODE_PROJECT_HASH"

  # 5. If a stopped container exists, clean it up before fresh run
  cleanup_stopped_opencode_container "$OPENCODE_CONTAINER_NAME"

  # 6. Override CONTAINER_NAME (deterministic, regardless of TTY)
  CONTAINER_NAME="--name $OPENCODE_CONTAINER_NAME"

  # 7. Mark that --rm should be omitted for opencode
  OPENCODE_SKIP_RM=true
fi

# Build docker run arguments as an array (handles paths with spaces correctly)
DOCKER_ARGS=()
if [[ "$OPENCODE_SKIP_RM" == "true" ]]; then
  DOCKER_ARGS+=($CONTAINER_NAME $TTY_FLAGS)
else
  DOCKER_ARGS+=($CONTAINER_NAME --rm $TTY_FLAGS)
fi
DOCKER_ARGS+=(--init)
DOCKER_ARGS+=(--platform "$PLATFORM")
DOCKER_ARGS+=($ENTRYPOINT_OVERRIDE)
DOCKER_ARGS+=($VOLUME_MOUNTS)
DOCKER_ARGS+=($CONFIG_MOUNT)
DOCKER_ARGS+=($TOOL_CONFIG_MOUNTS)
DOCKER_ARGS+=($RG_COMPAT_MOUNT)
DOCKER_ARGS+=($GIT_MOUNTS)
DOCKER_ARGS+=($SSH_AGENT_ENV)
# Default to ai-sandbox network for service discovery if user didn't specify.
# Only add --network if creation succeeded; otherwise Docker uses its default bridge.
if [[ -z "$NETWORK_OPTIONS" ]]; then
  if ensure_network "ai-sandbox" >/dev/null 2>&1; then
    DOCKER_ARGS+=(--network ai-sandbox)
  fi
fi
# Add DNS servers for custom networks (required for external npm registry access)
# Only add if user specified a custom network or ai-sandbox was created
if [[ -n "$NETWORK_OPTIONS" ]] || [[ -z "$NETWORK_OPTIONS" ]]; then
  DOCKER_ARGS+=(--dns 8.8.8.8)
  DOCKER_ARGS+=(--dns 1.1.1.1)
fi
DOCKER_ARGS+=($NETWORK_OPTIONS)
DOCKER_ARGS+=($DISPLAY_FLAGS)
DOCKER_ARGS+=($HOST_ACCESS_ARGS)
DOCKER_ARGS+=($PORT_MAPPINGS)
DOCKER_ARGS+=($OPENCODE_PASSWORD_ENV)
DOCKER_ARGS+=(-v "$HOME_DIR":/home/agent)
# Auto-mount open-design data volume read-only so agents can read generated artifacts
if docker volume inspect ai-open-design-data >/dev/null 2>&1; then
  DOCKER_ARGS+=(-v "ai-open-design-data:/workspace/.od:ro")
fi
DOCKER_ARGS+=($SHARED_CACHE_MOUNTS)
DOCKER_ARGS+=($NANO_BRAIN_MOUNT)
# Opencode DB isolation: overlay per-project SQLite files AFTER all directory mounts
if [[ -n "$OPENCODE_PROJECT_HASH" ]]; then
  append_opencode_db_mounts "$OPENCODE_PROJECT_HASH"
fi
DOCKER_ARGS+=(-w "$CURRENT_DIR")
DOCKER_ARGS+=(--env-file "$ENV_FILE")
DOCKER_ARGS+=(-e TERM="$TERM")
DOCKER_ARGS+=(-e COLORTERM="$COLORTERM")
[[ -n "${TERM_PROGRAM:-}" ]] && DOCKER_ARGS+=(-e TERM_PROGRAM="$TERM_PROGRAM")
if [[ -n "${PLAYWRIGHT_MCP_NAME:-}" ]]; then
  DOCKER_ARGS+=(-e "PLAYWRIGHT_MCP_NAME=$PLAYWRIGHT_MCP_NAME")
  DOCKER_ARGS+=(-e "PLAYWRIGHT_PORT=$HOST_CHROME_CDP_PORT")
fi
if [[ -n "${CHROME_DEVTOOLS_MCP_NAME:-}" ]]; then
  DOCKER_ARGS+=(-e "CHROME_DEVTOOLS_MCP_NAME=$CHROME_DEVTOOLS_MCP_NAME")
fi
DOCKER_ARGS+=($TERMINAL_SIZE)

DOCKER_ARGS+=("$IMAGE")
DOCKER_ARGS+=("${DOCKER_COMMAND[@]}")

# Auto-start open-design daemon if image exists but container not running
if docker image inspect ai-open-design:latest >/dev/null 2>&1; then
  if ! docker ps --format '{{.Names}}' | grep -q "^ai-open-design$"; then
    if [[ -t 0 && -t 1 ]]; then
      printf "🎨 Open Design daemon is not running. Start it? [Y/n] "
      read -r OD_ANSWER
      if [[ ! "$OD_ANSWER" =~ ^[Nn]$ ]]; then
        OD_ENV_FILE="$HOME/.ai-sandbox/env"
        OD_NETWORK="ai-sandbox"
        OD_VOLUME="ai-open-design-data"
        # Auto-init if token missing
        if ! grep -q "^OD_API_TOKEN=" "$OD_ENV_FILE" 2>/dev/null; then
          mkdir -p "$(dirname "$OD_ENV_FILE")"
          touch "$OD_ENV_FILE"
          chmod 600 "$OD_ENV_FILE"
          OD_TOKEN="$(openssl rand -hex 32)"
          echo "OD_API_TOKEN=$OD_TOKEN" >> "$OD_ENV_FILE"
          echo "OD_DAEMON_URL=http://ai-open-design:7456" >> "$OD_ENV_FILE"
          echo "✅ Generated OD_API_TOKEN"
        fi
        # Ensure network and volume
        docker network inspect "$OD_NETWORK" >/dev/null 2>&1 || docker network create "$OD_NETWORK" >/dev/null
        docker volume inspect "$OD_VOLUME" >/dev/null 2>&1 || docker volume create "$OD_VOLUME" >/dev/null
        # Remove stopped container if exists
        if docker ps -a --format '{{.Names}}' | grep -q "^ai-open-design$"; then
          docker rm ai-open-design >/dev/null 2>&1 || true
        fi
        docker run -d --name ai-open-design --network "$OD_NETWORK" \
          --restart unless-stopped -v "$OD_VOLUME:/app/.od" \
          -p "127.0.0.1:7456:7456" \
          --env-file "$OD_ENV_FILE" ai-open-design:latest >/dev/null
        echo "✅ Open Design daemon started (http://localhost:7456)"
      fi
    fi
  fi
fi

# Execute docker run with proper argument handling
if [[ "$OPENCODE_SKIP_RM" == "true" ]]; then
  # Opencode: run without --rm. Handle "name already in use" race condition.
  # Docker returns exit code 125 for daemon errors including name conflicts.
  # We run docker run directly (no output capture) to preserve TTY allocation,
  # then check if the failure was a name conflict by seeing if the container is now running.
  if docker run "${DOCKER_ARGS[@]}"; then
    exit_code=0
  else
    exit_code=$?
  fi
  if [[ $exit_code -eq 125 ]]; then
    # Check if the container is now running (another invocation won the race)
    _oc_race_check=""
    _oc_race_check=$(opencode_container_running "$OPENCODE_CONTAINER_NAME") || true
    if [[ -n "$_oc_race_check" ]]; then
      echo "→ container name race detected, retrying via exec..." >&2
      exec_into_opencode_container "$OPENCODE_CONTAINER_NAME" "$CURRENT_DIR" "${TOOL_ARGS[@]}"
      exit $?
    fi
  fi
  exit $exit_code
else
  docker run "${DOCKER_ARGS[@]}"
fi
