#!/usr/bin/env bash
#
# OpenConnector Control Script (appctl.sh)
# One-stop management for OpenConnector Node backend on local/Linux servers.
#
# Usage: ./appctl.sh <command> [options]
#
# Commands:
#   start         Start OpenConnector API server (background)
#   stop          Stop API server
#   restart       Restart API server
#   status        Show server status, PID, memory, DB info
#   logs [N]      Tail last N lines of log (default: 100, 0 = follow)
#   dev           Start API + web console together (foreground, like npm run dev)
#   build         Full build: install + provider registry + web + typecheck
#   build:web     Build only the web console (vite, output: dist/web/)
#   fix-check     Run lint + format + typecheck (npm run fix-check)
#   env           Show current environment configuration
#   data          Manage runtime data: ./appctl.sh data reset|rotate-key [...]
#   backup        Backup the runtime database (sqlite file copy / pg_dump / mysqldump)
#   health        Probe /health endpoint
#   help          Show this help message
#

set -euo pipefail

# ── Configuration ──────────────────────────────────────────────

# Project root (where package.json + src/ live)
INSTALL_DIR="$(cd "$(dirname "$0")" && pwd)"

APP_NAME="open-connector"
APP_ENTRY="src/server/index.ts"
HEALTH_PATH="/health"

# ── Load environment overrides ─────────────────────────────────
#
# Mirror src/server/load-env.ts precedence: shell env wins over .env.
# Source order: .env → .env.local (last wins); all are no-ops if missing.
load_env_file() {
  local file="$1"
  [[ -f "$file" ]] || return 0
  # Only export KEY=VALUE lines; preserve quoting. Skip comments / blank lines.
  set -a
  # shellcheck disable=SC1090
  . "$file"
  set +a
}

load_env_file "${INSTALL_DIR}/.env"
load_env_file "${INSTALL_DIR}/.env.local"

# System-level defaults (optional)
if [[ -f /etc/default/open-connector ]]; then
  load_env_file /etc/default/open-connector
fi

# Default environment. `: "${VAR:=default}"` only sets if unset OR empty.
: "${PORT:=3000}"
: "${HOST:=127.0.0.1}"
: "${OOMOL_CONNECT_ORIGIN:=http://localhost:${PORT}}"
: "${OOMOL_CONNECT_DATA_DIR:=${INSTALL_DIR}/data}"
: "${OOMOL_CONNECT_DB_DRIVER:=sqlite}"
# OOMOL_CONNECT_DATABASE_URL, OOMOL_CONNECT_DB_POOL_MAX, ENCRYPTION_KEY, tokens: no defaults.

# Derived paths
PID_FILE="${INSTALL_DIR}/${APP_NAME}.pid"
LOG_FILE="${INSTALL_DIR}/${APP_NAME}.log"
BACKUP_DIR="${INSTALL_DIR}/backups"

# ── Helpers ────────────────────────────────────────────────────

color_red='\033[0;31m'
color_green='\033[0;32m'
color_yellow='\033[0;33m'
color_cyan='\033[0;36m'
color_dim='\033[2m'
color_reset='\033[0m'

log_info()  { echo -e "${color_cyan}[INFO]${color_reset}  $*"; }
log_ok()    { echo -e "${color_green}[OK]${color_reset}    $*"; }
log_warn()  { echo -e "${color_yellow}[WARN]${color_reset}  $*"; }
log_error() { echo -e "${color_red}[ERROR]${color_reset} $*"; }

# Strip the password part of a database URL for safe display.
mask_url() {
  local url="$1"
  # postgresql://user:pass@host:port/db  →  postgresql://user:***@host:port/db
  printf '%s' "$url" | sed -E 's#(://[^:]+):[^@]+@#\1:***@#'
}

# Find node binary. Project requires Node 22+ for native TS stripping.
find_node() {
  if [[ -n "${NODE_BIN:-}" ]] && [[ -x "$NODE_BIN" ]]; then
    echo "$NODE_BIN"
    return
  fi
  local candidates=(
    /opt/homebrew/bin/node
    /usr/local/bin/node
    /usr/bin/node
  )
  for c in "${candidates[@]}"; do
    if [[ -x "$c" ]]; then
      echo "$c"
      return
    fi
  done
  if command -v node >/dev/null 2>&1; then
    command -v node
    return
  fi
  log_error "Node.js not found. Install Node.js 22+ or set NODE_BIN."
  exit 1
}

# Strip ANSI escapes so logs stay clean.
node_version_str() {
  "$(find_node)" --version 2>/dev/null || echo "unknown"
}

# PID from pidfile; falls back to lsof-on-port lookup.
get_pid() {
  if [[ -f "$PID_FILE" ]]; then
    local pid
    pid=$(cat "$PID_FILE" 2>/dev/null || true)
    if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
      echo "$pid"
      return 0
    fi
    rm -f "$PID_FILE"
  fi
  if command -v lsof >/dev/null 2>&1; then
    local pid
    pid=$(lsof -ti :"$PORT" 2>/dev/null | head -1 || true)
    if [[ -n "$pid" ]]; then
      echo "$pid"
      return 0
    fi
  fi
  return 1
}

is_running() {
  get_pid >/dev/null 2>&1
}

# Wait until /health responds 200 or timeout.
wait_for_start() {
  local max_wait=30
  local elapsed=0
  while (( elapsed < max_wait )); do
    if curl -sf "http://localhost:${PORT}${HEALTH_PATH}" >/dev/null 2>&1; then
      return 0
    fi
    sleep 1
    ((elapsed += 1))
  done
  return 1
}

# Portable ps field extraction. macOS and Linux have different layouts.
ps_field() {
  local pid="$1"
  local field="$2"  # etime | rss
  if [[ "$(uname)" == "Darwin" ]]; then
    # macOS: etime unsupported, approximate with lstart; rss in KB.
    if [[ "$field" == "etime" ]]; then
      ps -p "$pid" -o lstart= 2>/dev/null || echo "unknown"
    else
      ps -p "$pid" -o rss= 2>/dev/null | awk '{printf "%.1f MB", $1/1024}' || echo "unknown"
    fi
  else
    if [[ "$field" == "etime" ]]; then
      ps -p "$pid" -o etime= 2>/dev/null | tr -d ' ' || echo "unknown"
    else
      ps -p "$pid" -o rss= 2>/dev/null | awk '{printf "%.1f MB", $1/1024}' || echo "unknown"
    fi
  fi
}

ensure_dirs() {
  mkdir -p "$OOMOL_CONNECT_DATA_DIR"
}

# Project uses native TS execution; the entry always lives at src/server/index.ts.
resolve_app_entry() {
  if [[ -f "${INSTALL_DIR}/${APP_ENTRY}" ]]; then
    return 0
  fi
  log_error "Server entry not found: ${INSTALL_DIR}/${APP_ENTRY}"
  exit 1
}

# Ensure dependencies are installed and provider registry is fresh.
# Skipped in dev mode (dev-local.ts runs ensure-generated itself).
ensure_generated() {
  cd "$INSTALL_DIR"
  if [[ ! -d node_modules ]]; then
    log_info "Installing dependencies (npm install)..."
    npm install
    log_ok "Dependencies installed"
  fi
  log_info "Ensuring provider registry is fresh..."
  "$(find_node)" scripts/ensure-generated.ts
}

# ── Commands ───────────────────────────────────────────────────

cmd_start() {
  if is_running; then
    local pid
    pid=$(get_pid)
    log_warn "Already running (PID: ${pid}, port: ${PORT})"
    return 0
  fi

  ensure_dirs

  # Mirror `npm start`: ensure provider registry is fresh before booting the server.
  # ensure-generated.ts is a no-op when the registry is already up to date.
  ensure_generated

  local node_bin
  node_bin=$(find_node)
  resolve_app_entry

  log_info "Starting OpenConnector..."
  log_info "  Node:      ${node_bin} ($(node_version_str))"
  log_info "  Entry:     ${APP_ENTRY}"
  log_info "  Port:      ${PORT}"
  log_info "  Host:      ${HOST}"
  log_info "  Origin:    ${OOMOL_CONNECT_ORIGIN}"
  log_info "  Driver:    ${OOMOL_CONNECT_DB_DRIVER}"
  if [[ -n "${OOMOL_CONNECT_DATABASE_URL:-}" ]]; then
    log_info "  Database:  $(mask_url "$OOMOL_CONNECT_DATABASE_URL")"
  else
    log_info "  Database:  ${OOMOL_CONNECT_DATA_DIR}/connect.sqlite"
  fi

  cd "$INSTALL_DIR"
  nohup "$node_bin" "$APP_ENTRY" >> "$LOG_FILE" 2>&1 &
  local pid=$!
  echo "$pid" > "$PID_FILE"

  if wait_for_start; then
    log_ok "Started (PID: ${pid})"
    log_ok "Health:  http://localhost:${PORT}${HEALTH_PATH}"
    log_ok "API:     ${OOMOL_CONNECT_ORIGIN}/v1/actions"
  else
    log_error "Failed to start (no response at :${PORT}${HEALTH_PATH} after 30s)."
    log_error "Last 20 log lines:"
    cmd_logs 20 || true
    rm -f "$PID_FILE"
    exit 1
  fi
}

cmd_stop() {
  if ! is_running; then
    log_warn "Not running"
    rm -f "$PID_FILE"
    return 0
  fi

  local pid
  pid=$(get_pid)
  log_info "Stopping (PID: ${pid})..."
  kill "$pid" 2>/dev/null || true

  # The server registers SIGINT/SIGTERM handlers that close the DB pool first.
  local max_wait=10
  local elapsed=0
  while (( elapsed < max_wait )); do
    if ! kill -0 "$pid" 2>/dev/null; then
      log_ok "Stopped"
      rm -f "$PID_FILE"
      return 0
    fi
    sleep 1
    ((elapsed += 1))
  done

  log_warn "Graceful shutdown timed out, force killing..."
  kill -9 "$pid" 2>/dev/null || true
  rm -f "$PID_FILE"
  log_ok "Force stopped"
}

cmd_restart() {
  log_info "Restarting..."
  cmd_stop
  sleep 1
  cmd_start
}

cmd_status() {
  echo -e "${color_cyan}━━ OpenConnector Status ━━${color_reset}"
  echo ""

  if is_running; then
    local pid
    pid=$(get_pid)
    echo -e "  Status:    ${color_green}RUNNING${color_reset}"
    echo "  PID:       ${pid}"
    echo "  Port:      ${PORT}"
    echo "  Started:   $(ps_field "$pid" etime)"
    echo "  Memory:    $(ps_field "$pid" rss)"
    # Live health check
    if curl -sf "http://localhost:${PORT}${HEALTH_PATH}" >/dev/null 2>&1; then
      echo -e "  Health:    ${color_green}OK${color_reset}"
    else
      echo -e "  Health:    ${color_red}no response${color_reset}"
    fi
  else
    echo -e "  Status:    ${color_red}STOPPED${color_reset}"
  fi

  echo ""
  echo -e "${color_cyan}━━ Configuration ━━${color_reset}"
  echo "  Install:   ${INSTALL_DIR}"
  echo "  Node:      $(find_node) ($(node_version_str))"
  echo "  Entry:     ${APP_ENTRY}"
  echo "  Origin:    ${OOMOL_CONNECT_ORIGIN}"
  echo "  Driver:    ${OOMOL_CONNECT_DB_DRIVER}"
  if [[ -n "${OOMOL_CONNECT_DATABASE_URL:-}" ]]; then
    echo "  Database:  $(mask_url "$OOMOL_CONNECT_DATABASE_URL")"
  else
    local db_file="${OOMOL_CONNECT_DATA_DIR}/connect.sqlite"
    if [[ -f "$db_file" ]]; then
      local db_size
      db_size=$(du -h "$db_file" 2>/dev/null | cut -f1)
      echo "  Database:  ${db_file} (${db_size})"
    else
      echo "  Database:  ${db_file} (not found)"
    fi
  fi
  echo "  Data dir:  ${OOMOL_CONNECT_DATA_DIR}"
  echo "  Log:       ${LOG_FILE}"
  echo "  PID file:  ${PID_FILE}"

  echo ""
  echo -e "${color_cyan}━━ Disk ━━${color_reset}"
  df -h "$INSTALL_DIR" 2>/dev/null | tail -1 \
    | awk '{printf "  Install:  %s used / %s total (%s avail)\n", $3, $2, $4}' || true
  df -h "$OOMOL_CONNECT_DATA_DIR" 2>/dev/null | tail -1 \
    | awk '{printf "  Data:     %s used / %s total (%s avail)\n", $3, $2, $4}' || true
}

cmd_logs() {
  local lines="${1:-100}"
  if [[ ! -f "$LOG_FILE" ]]; then
    log_warn "No log file at ${LOG_FILE}"
    return
  fi
  if (( lines > 0 )); then
    tail -n "$lines" "$LOG_FILE"
  else
    tail -f "$LOG_FILE"
  fi
}

cmd_health() {
  log_info "Probing http://localhost:${PORT}${HEALTH_PATH} ..."
  if curl -sf "http://localhost:${PORT}${HEALTH_PATH}"; then
    echo ""
    log_ok "Healthy"
  else
    log_error "No response (status=$?)"
    return 1
  fi
}

# Foreground dev mode: API + web console, mirrors `npm run dev`.
cmd_dev() {
  if is_running; then
    local pid
    pid=$(get_pid)
    log_warn "Background instance is running (PID: ${pid}). Stop it first: ./appctl.sh stop"
    return 1
  fi
  log_info "Starting dev mode (API + web) in foreground..."
  log_info "  Press Ctrl-C to stop both."
  cd "$INSTALL_DIR"
  exec "$(find_node)" scripts/dev-local.ts
}

# Full build: install deps + provider registry + web console + typecheck.
# Use this before deploying: dist/web/ must exist for the server to serve the UI.
cmd_build() {
  cd "$INSTALL_DIR"
  log_info "Full build (deps + registry + web + typecheck)..."
  ensure_generated
  log_info "Building web console..."
  if [[ ! -d web ]]; then
    log_error "No web/ directory found."
    exit 1
  fi
  npm run build --workspace web
  log_ok "Web build complete: dist/web/"
  log_info "Running typecheck..."
  npm run typecheck
  log_ok "Build complete. Artifacts:"
  log_ok "  dist/web/                # static console served by the Node backend"
  log_ok "  src/providers/registry.generated.ts  # provider registry"
}

cmd_build_web() {
  log_info "Building web console..."
  cd "$INSTALL_DIR"
  if [[ ! -d web ]]; then
    log_error "No web/ directory found."
    exit 1
  fi
  npm run build --workspace web
  log_ok "Web build complete: dist/web/"
}

cmd_fix_check() {
  log_info "Running lint + format + typecheck (npm run fix-check)..."
  cd "$INSTALL_DIR"
  npm run fix-check
  log_ok "fix-check passed"
}

cmd_env() {
  echo -e "${color_cyan}━━ OpenConnector Environment ━━${color_reset}"
  echo ""
  echo "  INSTALL_DIR=${INSTALL_DIR}"
  echo "  NODE_BIN=$(find_node) ($(node_version_str))"
  echo "  APP_ENTRY=${APP_ENTRY}"
  echo "  PORT=${PORT}"
  echo "  HOST=${HOST}"
  echo "  OOMOL_CONNECT_ORIGIN=${OOMOL_CONNECT_ORIGIN}"
  echo "  OOMOL_CONNECT_DATA_DIR=${OOMOL_CONNECT_DATA_DIR}"
  echo "  OOMOL_CONNECT_DB_DRIVER=${OOMOL_CONNECT_DB_DRIVER}"
  if [[ -n "${OOMOL_CONNECT_DATABASE_URL:-}" ]]; then
    echo "  OOMOL_CONNECT_DATABASE_URL=$(mask_url "$OOMOL_CONNECT_DATABASE_URL")"
  else
    echo "  OOMOL_CONNECT_DATABASE_URL=(unset)"
  fi
  if [[ -n "${OOMOL_CONNECT_DB_POOL_MAX:-}" ]]; then
    echo "  OOMOL_CONNECT_DB_POOL_MAX=${OOMOL_CONNECT_DB_POOL_MAX}"
  fi
  echo "  OOMOL_CONNECT_ENCRYPTION_KEY=$([[ -n "${OOMOL_CONNECT_ENCRYPTION_KEY:-}" ]] && echo '<set>' || echo '<unset>')"
  echo "  OOMOL_CONNECT_RUNTIME_TOKEN=$([[ -n "${OOMOL_CONNECT_RUNTIME_TOKEN:-}" ]] && echo '<set>' || echo '<unset>')"
  echo "  PID_FILE=${PID_FILE}"
  echo "  LOG_FILE=${LOG_FILE}"
  echo ""
  echo -e "${color_cyan}━━ Env file sources ━━${color_reset}"
  for f in "${INSTALL_DIR}/.env" "${INSTALL_DIR}/.env.local" /etc/default/open-connector; do
    if [[ -f "$f" ]]; then
      echo -e "  ${color_green}✓${color_reset} $f"
    else
      echo -e "  ${color_dim}·${color_reset} $f (absent)"
    fi
  done
}

# Thin wrapper around scripts/runtime-data.ts.
# Usage: ./appctl.sh data reset --yes
#        ./appctl.sh data rotate-key --plain
#        ./appctl.sh data rotate-key
cmd_data() {
  if [[ $# -eq 0 ]]; then
    log_error "Missing subcommand. Usage: ./appctl.sh data reset|rotate-key [...]"
    echo ""
    echo "Delegates to scripts/runtime-data.ts. Common forms:"
    echo "  ./appctl.sh data reset --yes                # Wipe all runtime rows"
    echo "  ./appctl.sh data rotate-key --plain         # Re-encrypt secrets as plaintext"
    echo "  OOMOL_CONNECT_NEW_ENCRYPTION_KEY=new ./appctl.sh data rotate-key"
    return 1
  fi
  case "$1" in
    reset|rotate-key)
      cd "$INSTALL_DIR"
      log_info "Running: node scripts/runtime-data.ts $*"
      "$(find_node)" scripts/runtime-data.ts "$@"
      ;;
    *)
      log_error "Unknown data subcommand: $1 (expected reset | rotate-key)"
      return 1
      ;;
  esac
}

# Driver-aware backup. sqlite = file copy; pg/mysql = SQL dump via pg_dump/mysqldump.
cmd_backup() {
  local timestamp
  timestamp=$(date +%Y%m%d_%H%M%S)
  mkdir -p "$BACKUP_DIR"

  local driver="${OOMOL_CONNECT_DB_DRIVER}"
  case "$driver" in
    sqlite)
      local db_file="${OOMOL_CONNECT_DATA_DIR}/connect.sqlite"
      if [[ ! -f "$db_file" ]]; then
        log_error "SQLite file not found: ${db_file}"
        return 1
      fi
      local was_running=false
      if is_running; then
        was_running=true
        log_info "Pausing server for consistent backup..."
        cmd_stop
      fi
      cp "$db_file" "${BACKUP_DIR}/connect_${timestamp}.sqlite"
      [[ -f "${db_file}-wal" ]] && cp "${db_file}-wal" "${BACKUP_DIR}/connect_${timestamp}.sqlite-wal"
      [[ -f "${db_file}-shm" ]] && cp "${db_file}-shm" "${BACKUP_DIR}/connect_${timestamp}.sqlite-shm"
      if $was_running; then cmd_start >/dev/null; fi
      log_ok "SQLite backup: ${BACKUP_DIR}/connect_${timestamp}.sqlite ($(du -h "${BACKUP_DIR}/connect_${timestamp}.sqlite" | cut -f1))"
      ;;
    postgres)
      if ! command -v pg_dump >/dev/null 2>&1; then
        log_error "pg_dump not found. Install PostgreSQL client tools to back up postgres backend."
        return 1
      fi
      log_info "Dumping PostgreSQL via pg_dump..."
      pg_dump --no-owner --clean --if-exists \
        "${OOMOL_CONNECT_DATABASE_URL}" \
        -f "${BACKUP_DIR}/connect_${timestamp}.sql"
      log_ok "PostgreSQL backup: ${BACKUP_DIR}/connect_${timestamp}.sql ($(du -h "${BACKUP_DIR}/connect_${timestamp}.sql" | cut -f1))"
      ;;
    mysql)
      if ! command -v mysqldump >/dev/null 2>&1; then
        log_error "mysqldump not found. Install MySQL client tools to back up mysql backend."
        return 1
      fi
      log_info "Dumping MySQL via mysqldump..."
      mysqldump --single-transaction --no-tablespaces \
        "${OOMOL_CONNECT_DATABASE_URL}" \
        > "${BACKUP_DIR}/connect_${timestamp}.sql"
      log_ok "MySQL backup: ${BACKUP_DIR}/connect_${timestamp}.sql ($(du -h "${BACKUP_DIR}/connect_${timestamp}.sql" | cut -f1))"
      ;;
    *)
      log_error "Unknown driver: ${driver}"
      return 1
      ;;
  esac

  # Retention: keep last 7 backups.
  local count
  count=$(ls -1 "${BACKUP_DIR}"/connect_*.{sqlite,sql} 2>/dev/null | wc -l | tr -d ' ')
  if (( count > 7 )); then
    local to_remove=$(( count - 7 ))
    ls -1t "${BACKUP_DIR}"/connect_*.{sqlite,sql} 2>/dev/null | tail -n "$to_remove" | xargs rm -f
    log_info "Cleaned ${to_remove} old backup(s), kept last 7"
  fi
}

cmd_help() {
  cat <<'HELP'
OpenConnector Control Script (appctl.sh)

Usage: ./appctl.sh <command> [options]

Commands:
  start          Start API server in background
  stop           Stop API server
  restart        Restart API server
  status         Show status: PID, memory, DB info, disk usage
  logs [N]       Tail last N lines of log (default: 100, 0 = follow)
  health         Probe /health endpoint
  build         Full build: install deps + provider registry + web console + typecheck
  build:web     Build only the web console (vite, output: dist/web/)
  fix-check      Lint + format + typecheck (npm run fix-check)
  data           Manage runtime data:
                   ./appctl.sh data reset --yes
                   ./appctl.sh data rotate-key --plain
                   OOMOL_CONNECT_NEW_ENCRYPTION_KEY=... ./appctl.sh data rotate-key
  env            Show current environment (secrets masked)
  backup         Backup runtime database (sqlite/pg/mysql aware)
  help           Show this help message

Environment:
  Loaded from .env, then .env.local (install dir), then /etc/default/open-connector.
  Shell env always wins. Key variables:

  PORT                          HTTP port (default: 3000)
  HOST                          Bind address (default: 127.0.0.1)
  OOMOL_CONNECT_ORIGIN          Public origin for OAuth redirects
  OOMOL_CONNECT_DATA_DIR        Transit files + SQLite path (default: ./data)
  OOMOL_CONNECT_DB_DRIVER       sqlite | postgres | mysql (default: sqlite)
  OOMOL_CONNECT_DATABASE_URL    Required for postgres / mysql
  OOMOL_CONNECT_DB_POOL_MAX     Pool size (default: 10)
  OOMOL_CONNECT_ENCRYPTION_KEY  Secret for AES-GCM credential encryption
  OOMOL_CONNECT_ADMIN_TOKEN    Admin UI token
  OOMOL_CONNECT_RUNTIME_TOKEN  /v1/* API token
  NODE_BIN                      Override Node.js binary path

Examples:
  ./appctl.sh start                            # Start on configured port
  ./appctl.sh status                           # Health + DB + disk snapshot
  ./appctl.sh logs 0                           # Follow logs in real-time
  ./appctl.sh dev                              # Foreground API + web (like npm run dev)
  ./appctl.sh data reset --yes                 # Wipe all runtime rows
  ./appctl.sh backup                           # Driver-aware DB backup
  PORT=8080 ./appctl.sh start                  # Override port for one run
  OOMOL_CONNECT_DB_DRIVER=postgres \
    OOMOL_CONNECT_DATABASE_URL=postgres://... \
    ./appctl.sh start                          # Run against PostgreSQL

Directory layout:
  ./                            INSTALL_DIR (this script + src/ + package.json)
  ./data/                       OOMOL_CONNECT_DATA_DIR (transit files + SQLite)
  ./backups/                    Database backups (retention: last 7)
  ./open-connector.pid          PID file (background mode)
  ./open-connector.log          Log file (background mode)
HELP
}

# ── Main ────────────────────────────────────────────────────────

cmd="${1:-help}"
shift || true

case "$cmd" in
  start)        cmd_start ;;
  stop)         cmd_stop ;;
  restart)      cmd_restart ;;
  status)       cmd_status ;;
  logs)         cmd_logs "${1:-100}" ;;
  health)       cmd_health ;;
  dev)          cmd_dev ;;
  build:web)    cmd_build_web ;;
  build)        cmd_build ;;
  fix-check)    cmd_fix_check ;;
  data)         cmd_data "$@" || exit 1 ;;
  env)          cmd_env ;;
  backup)       cmd_backup || exit 1 ;;
  help|--help|-h)
                cmd_help ;;
  *)
    log_error "Unknown command: ${cmd}"
    echo ""
    cmd_help
    exit 1
    ;;
esac
