#!/bin/sh
# naixi — iKuai app manager CLI
# POSIX shell, zero deps, runs on iKuai BusyBox.
#
# Subcommands:
#   naixi install <file.ipkg|url>   Smart install / upgrade / downgrade
#   naixi uninstall <name>          Remove by name
#   naixi list                      List installed apps (origin/boot/size/path)
#   naixi boot [enable|disable] X   Boot auto-start management
#   naixi clean <name>              Wipe app data/cache (keep config)
#   naixi update                    Self-update from dl.naixi.net
#   naixi version                   Show version
#   naixi self-install              Register self in appmarket
#   naixi self-uninstall            Remove self
NAIXI_VERSION="0.6.2"
DL_BASE="https://dl.naixi.net/ikuai"
APPMARKET_DB="/etc/mnt/ikuai/appmarket.db"
DOCKER_DB="/etc/mnt/ikuai/docker.db"
IK_DIR="/usr/ikuai"
SELF_NAME="naixi"

set -eu

log()  { echo "[$SELF_NAME] $*"; }
warn() { echo "[$SELF_NAME] WARNING: $*" >&2; }
err()  { echo "[$SELF_NAME] ERROR: $*" >&2; exit 1; }

# ─── json parse (awk fallback, no jq needed) ──────────────────────
json_str() {
  awk -F'"' "/\"$1\"/{print \$4; exit}" "$2" 2>/dev/null
}

# ─── version compare: echo gt|lt|eq ───────────────────────────────
# Each dot-separated numeric segment is zero-padded to 4 digits, then
# concatenated and compared as integers.  0.5.0 → 000000050000 etc.
ver_num() {
  v="$1"; out=""; oldifs="$IFS"; IFS=.
  for seg in $v; do
    seg=$(echo "$seg" | tr -cd '0-9')
    [ -z "$seg" ] && seg=0
    while [ ${#seg} -lt 4 ]; do seg="0$seg"; done
    out="$out$seg"
  done
  IFS="$oldifs"
  echo "$out"
}
ver_cmp() {
  a=$(ver_num "$1"); b=$(ver_num "$2")
  [ "$a" -gt "$b" ] 2>/dev/null && { echo gt; return; }
  [ "$a" -lt "$b" ] 2>/dev/null && { echo lt; return; }
  echo eq
}

# ─── resolve workdisk + app path helpers ──────────────────────────
get_workdisk() {
  sqlite3 "$DOCKER_DB" "select workdisk from global" 2>/dev/null \
    || err "cannot read docker.db"
}
app_path_of() {  # $1 = app name → echo full path from DB install_path
  IP=$(sqlite3 "$APPMARKET_DB" "SELECT install_path FROM applist WHERE name='$1'" 2>/dev/null)
  if [ -z "$IP" ]; then
    # Not in DB — fallback to workdisk
    WD=$(get_workdisk)
    echo "/etc/disk_user/$WD/apps/$1"
  elif [ "$IP" = "/etc/log" ]; then
    echo "/etc/log/apps/$1"
  else
    echo "/etc/disk_user/$IP/apps/$1"
  fi
}

# ─── self-install: register naixi itself in appmarket.db ──────────
self_install() {
  WORKDISK=$(get_workdisk)
  APPS_DIR="/etc/disk_user/$WORKDISK/apps"
  APP_PATH="$APPS_DIR/$SELF_NAME"
  APPMARKET_DIR="/var/appmarket"

  if [ ! -d "$APP_PATH/.pkg/scripts" ]; then
    mkdir -p "$APP_PATH/.pkg/scripts" "$APP_PATH/.pkg/app/config" "$APP_PATH/log"
    ln -sf .pkg/app/config "$APP_PATH/config"
    cat > "$APP_PATH/.pkg/scripts/start.sh" <<'EOF'
#!/bin/sh
echo "0" > "$(dirname "$0")/../../log/run.status"
exit 0
EOF
    chmod +x "$APP_PATH/.pkg/scripts/start.sh"
    cat > "$APP_PATH/.pkg/scripts/stop.sh" <<'EOF'
#!/bin/sh
exit 0
EOF
    chmod +x "$APP_PATH/.pkg/scripts/stop.sh"
  fi

  cur=100000
  [ -f "/etc/mnt/ikuai/appmarket/.max_appid" ] && cur=$(cat "/etc/mnt/ikuai/appmarket/.max_appid" 2>/dev/null || echo 100000)
  dbmax=$(sqlite3 "$APPMARKET_DB" "select max(appid) from applist" 2>/dev/null || echo 0)
  [ "$dbmax" -gt "$cur" ] 2>/dev/null && cur=$dbmax
  APPID=$((cur + 1))
  echo "$APPID" > "/etc/mnt/ikuai/appmarket/.max_appid" 2>/dev/null || true

  mkdir -p "$APPMARKET_DIR"
  [ ! -e "$APPMARKET_DIR/$SELF_NAME" ] && ln -s "$APP_PATH" "$APPMARKET_DIR/$SELF_NAME"

  TIMESTAMP=$(date +%s)
  SIZE=$(du -sk "$APP_PATH" 2>/dev/null | cut -f1)
  [ -z "$SIZE" ] && SIZE=0
  sqlite3 "$APPMARKET_DB" "DELETE FROM applist WHERE name='$SELF_NAME'" 2>/dev/null || true
  sqlite3 "$APPMARKET_DB" "INSERT INTO applist (
    enabled,name,appid,timestamp,display_name,location,ico,type,size,
    restart,current_version,maintainer,maintainer_url,distributor,distributor_url,
    origin,update_time,install_path,category,sub_category,desc,readme,changelog
  ) VALUES (
    'yes','$SELF_NAME',$APPID,$TIMESTAMP,'Naixi','','',0,$SIZE,
    1,'$NAIXI_VERSION','Nyarime','https://naixi.net','Nyarime','https://naixi.net',
    0,$TIMESTAMP,'$WORKDISK',7,0,'TmFpeGkg5byV5aiB55qE5Zmo','',''
  )" 2>/dev/null || err "failed to write appmarket.db"

  echo "0" > "$APP_PATH/log/run.status"
  log "self-registered (appid=$APPID, origin=0, restart=1)"
}

self_uninstall() {
  APP_PATH=$(app_path_of "$SELF_NAME")
  sqlite3 "$APPMARKET_DB" "DELETE FROM applist WHERE name='$SELF_NAME'" 2>/dev/null || true
  rm -rf "$APP_PATH" "/var/appmarket/$SELF_NAME" "$IK_DIR/www/static/appmarket/$SELF_NAME"
  log "self-unregistered"
}

# ─── common: download url → local file ────────────────────────────
download_pkg() {  # $1=url  echo=local path
  URL="$1"
  TMPFILE="/tmp/naixi-dl-$$.ipkg"
  if command -v curl >/dev/null 2>&1; then
    curl -fsSL "$URL" -o "$TMPFILE" || err "download failed: $URL"
  else
    wget -qO "$TMPFILE" "$URL" || err "download failed: $URL"
  fi
  echo "$TMPFILE"
}

cache_bust_url() {
  case "$1" in
    *\?*) printf '%s&v=%s\n' "$1" "$(date +%s)" ;;
    *)    printf '%s?v=%s\n' "$1" "$(date +%s)" ;;
  esac
}

# ─── common: run start.sh in background (busybox: no nohup) ───────
start_app() {  # $1 = APP_PATH
  SAP="$1"
  SCRIPTS_DIR="$SAP/.pkg/scripts"
  [ -d "$SCRIPTS_DIR" ] || { warn "no scripts dir"; return; }
  cd "$SCRIPTS_DIR"
  [ -x "./PRE_INST.sh" ] && ./PRE_INST.sh >/dev/null 2>&1 || true
  log "starting..."
  if [ -x "./start.sh" ]; then
    ./start.sh >> "$SAP/log/run.log" 2>&1 &
    START_PID=$!
    i=0; START_RC=""
    while [ "$i" -lt 60 ]; do
      if ! kill -0 "$START_PID" 2>/dev/null; then
        wait "$START_PID" 2>/dev/null && START_RC=0 || START_RC=$?
        break
      fi
      sleep 1; i=$((i + 1))
    done
    if [ -n "$START_RC" ]; then
      if [ "$START_RC" = "0" ]; then
        log "started (start.sh ok, log: $SAP/log/run.log)"
      else
        warn "start.sh exited with code $START_RC (check $SAP/log/run.log)"
      fi
    else
      log "started in background (pid=$START_PID, log: $SAP/log/run.log)"
    fi
  else
    warn "no start.sh found"
  fi
  [ -x "./POST_INST.sh" ] && ./POST_INST.sh >/dev/null 2>&1 || true
  cd /
}

# ─── install <ipkg|url>  (smart: upgrade / downgrade / fresh) ─────
do_install() {
  IPKG="$1"
  shift 2>/dev/null || true
  NAIXI_ORIGIN=0
  NAIXI_RESTART=1
  NAIXI_TARGET=""    # "" = auto, "system" = /etc/log, "disk" = workdisk
  NAIXI_NOCACHE=0
  for flag in "$@"; do
    case "$flag" in
      -official|--official) NAIXI_ORIGIN=0 ;;
      -sideload|--sideload) NAIXI_ORIGIN=3 ;;
      -origin=*) NAIXI_ORIGIN="${flag#*=}" ;;
      -norestart) NAIXI_RESTART=0 ;;
      -system) NAIXI_TARGET="system" ;;
      -disk)    NAIXI_TARGET="disk" ;;
      -nocache|--nocache) NAIXI_NOCACHE=1 ;;
      *) err "unknown install option: $flag" ;;
    esac
  done

  # 路径计算 — 对齐 appmarket.sh 逻辑:
  #   用户盘: /etc/disk_user/<workdisk>/apps/<name>  (install_path=<workdisk>)
  #   系统盘: /etc/log/apps/<name>                    (install_path=/etc/log)
  APPMARKET_DIR="/var/appmarket"
  MAX_APPID_FILE="/etc/mnt/ikuai/appmarket/.max_appid"

  # 判断 workdisk 是否可用
  WORKDISK=$(sqlite3 "$DOCKER_DB" "select workdisk from global" 2>/dev/null || echo "")
  DISK_OK=0
  if [ -n "$WORKDISK" ] && [ -d "/etc/disk_user/$WORKDISK" ]; then
    DISK_OK=1
  fi

  case "$NAIXI_TARGET" in
    system)
      INSTALL_PATH="/etc/log"
      APPS_DIR="/etc/log/apps"
      log "target: /etc/log (system)"
      ;;
    disk)
      [ "$DISK_OK" = "1" ] || err "no valid workdisk available (try: naixi install <pkg> -system)"
      INSTALL_PATH="$WORKDISK"
      APPS_DIR="/etc/disk_user/$WORKDISK/apps"
      log "target: /etc/disk_user/$WORKDISK (disk)"
      ;;
    *)
      # 默认: 有 disk 用 disk，没有 fallback /etc/log
      if [ "$DISK_OK" = "1" ]; then
        INSTALL_PATH="$WORKDISK"
        APPS_DIR="/etc/disk_user/$WORKDISK/apps"
        log "target: /etc/disk_user/$WORKDISK (disk)"
      else
        INSTALL_PATH="/etc/log"
        APPS_DIR="/etc/log/apps"
        log "target: /etc/log (system, no workdisk available)"
      fi
      ;;
  esac

  # URL → download
  case "$IPKG" in
    http://*|https://*)
      [ "$NAIXI_NOCACHE" = "1" ] && IPKG=$(cache_bust_url "$IPKG")
      log "downloading $IPKG..."
      IPKG=$(download_pkg "$IPKG")
      NAIXI_CLEANUP="$IPKG"
      ;;
  esac
  [ -f "$IPKG" ] || err "file not found: $IPKG"

  TMP=$(mktemp -d 2>/dev/null)
  [ -z "$TMP" ] && TMP="/tmp/naixi-$$" && mkdir -p "$TMP"
  trap 'rm -rf "$TMP" ${NAIXI_CLEANUP:-}' EXIT

  log "extracting..."
  tar xzf "$IPKG" -C "$TMP" 2>/dev/null || err "extraction failed"

  PKG_NAME=$(ls "$TMP")
  [ -d "$TMP/$PKG_NAME" ] || err "bad package structure"
  SRC="$TMP/$PKG_NAME"
  MANIFEST="$SRC/manifest.json"
  [ -f "$MANIFEST" ] || err "manifest.json not found"

  VERSION=$(json_str version "$MANIFEST")
  DISPLAY_NAME=$(json_str display_name "$MANIFEST")
  [ -z "$DISPLAY_NAME" ] && DISPLAY_NAME=$(json_str name "$MANIFEST")
  DESCRIPTION=$(json_str description "$MANIFEST")
  TYPE=$(json_str type "$MANIFEST")
  [ -z "$TYPE" ] && TYPE=0
  MAINTAINER=$(json_str maintainer "$MANIFEST")
  MAINTAINER_URL=$(json_str maintainer_url "$MANIFEST")
  DISTRIBUTOR=$(json_str distributor "$MANIFEST")
  DISTRIBUTOR_URL=$(json_str distributor_url "$MANIFEST")

  APP_PATH="$APPS_DIR/$PKG_NAME"

  # ── detect existing install ────────────────────────────────────
  OLD_ROW=$(sqlite3 "$APPMARKET_DB" \
    "SELECT origin|0,restart|0,enabled,current_version FROM applist WHERE name='$PKG_NAME'" 2>/dev/null || echo "")
  IS_UPGRADE=0
  if [ -n "$OLD_ROW" ] && [ "$(echo "$OLD_ROW" | wc -l)" -ge 1 ] && [ -d "$APP_PATH/.pkg" ]; then
    IS_UPGRADE=1
    OLD_ORIGIN=$(echo "$OLD_ROW" | head -1 | cut -d'|' -f1)
    OLD_RESTART=$(echo "$OLD_ROW" | head -1 | cut -d'|' -f2)
    OLD_ENABLED=$(echo "$OLD_ROW" | head -1 | cut -d'|' -f3)
    OLD_VER=$(echo "$OLD_ROW" | head -1 | cut -d'|' -f4)
    # Carry over origin/restart (user may have toggled them via naixi boot)
    [ -n "$OLD_ORIGIN" ] && NAIXI_ORIGIN="$OLD_ORIGIN"
    [ -n "$OLD_RESTART" ] && NAIXI_RESTART="$OLD_RESTART"

    case $(ver_cmp "$VERSION" "$OLD_VER") in
      gt) log "upgrade: $PKG_NAME $OLD_VER → $VERSION" ;;
      lt) warn "降级安装: $PKG_NAME $OLD_VER → $VERSION"
          warn "如安装后异常可用: naixi clean $PKG_NAME" ;;
      eq) log "reinstall: $PKG_NAME $VERSION" ;;
    esac
  else
    log "name=$PKG_NAME version=$VERSION type=$TYPE ($DISPLAY_NAME)"
  fi

  # appid (reuse old on upgrade)
  APPID=""
  if [ "$IS_UPGRADE" = "1" ]; then
    APPID=$(sqlite3 "$APPMARKET_DB" "SELECT appid FROM applist WHERE name='$PKG_NAME'" 2>/dev/null || echo "")
  fi
  if [ -z "$APPID" ]; then
    cur=100000
    [ -f "$MAX_APPID_FILE" ] && cur=$(cat "$MAX_APPID_FILE" 2>/dev/null || echo 100000)
    dbmax=$(sqlite3 "$APPMARKET_DB" "select max(appid) from applist" 2>/dev/null || echo 0)
    [ "$dbmax" -gt "$cur" ] 2>/dev/null && cur=$dbmax
    APPID=$((cur + 1))
    echo "$APPID" > "$MAX_APPID_FILE" 2>/dev/null || true
  fi

  log "installing..."

  # Stop existing
  if [ -x "$APP_PATH/.pkg/scripts/stop.sh" ]; then
    cd "$APP_PATH/.pkg/scripts"
    ./stop.sh >> "$APP_PATH/log/run.log" 2>&1 2>/dev/null || true
    cd /
  fi

  # ── file deployment ────────────────────────────────────────────
  mkdir -p "$APP_PATH/.pkg" "$APP_PATH/log" "$APP_PATH/.tmp" "$APP_PATH/.backup"

  if [ "$IS_UPGRADE" = "1" ]; then
    # UPGRADE: preserve user data/config/cache/.env, only update program files
    log "preserving user data (data/config/cache/.env)..."
    SAVEDIR=$(mktemp -d 2>/dev/null)
    [ -d "$APP_PATH/.pkg/app/data" ]   && mv "$APP_PATH/.pkg/app/data"   "$SAVEDIR/data"
    [ -d "$APP_PATH/.pkg/app/cache" ]  && mv "$APP_PATH/.pkg/app/cache"  "$SAVEDIR/cache"
    [ -d "$APP_PATH/.pkg/app/config" ] && mv "$APP_PATH/.pkg/app/config" "$SAVEDIR/config"
    [ -f "$APP_PATH/.pkg/app/.env" ]   && mv "$APP_PATH/.pkg/app/.env"   "$SAVEDIR/.env"
    [ -f "$APP_PATH/.pkg/app/environment" ] && mv "$APP_PATH/.pkg/app/environment" "$SAVEDIR/environment"

    cp -rf "$SRC/"* "$APP_PATH/.pkg/" 2>/dev/null || err "copy failed"

    # Restore user data (takes priority over package defaults)
    [ -d "$SAVEDIR/data" ]   && { rm -rf "$APP_PATH/.pkg/app/data";   mv "$SAVEDIR/data"   "$APP_PATH/.pkg/app/data"; }
    [ -d "$SAVEDIR/cache" ]  && { rm -rf "$APP_PATH/.pkg/app/cache";  mv "$SAVEDIR/cache"  "$APP_PATH/.pkg/app/cache"; }
    [ -d "$SAVEDIR/config" ] && { rm -rf "$APP_PATH/.pkg/app/config"; mv "$SAVEDIR/config" "$APP_PATH/.pkg/app/config"; }
    [ -f "$SAVEDIR/.env" ]   && mv "$SAVEDIR/.env" "$APP_PATH/.pkg/app/.env"
    [ -f "$SAVEDIR/environment" ] && mv "$SAVEDIR/environment" "$APP_PATH/.pkg/app/environment"
    rm -rf "$SAVEDIR"
  else
    # FRESH: full copy
    cp -rf "$SRC/"* "$APP_PATH/.pkg/" 2>/dev/null || err "copy failed"
  fi

  # Ensure script perms (start.sh must be +x for boot auto-start)
  [ -d "$APP_PATH/.pkg/scripts" ] && chmod +x "$APP_PATH/.pkg/scripts/"*.sh 2>/dev/null || true
  [ -d "$APP_PATH/.pkg/app/bin" ] && chmod +x "$APP_PATH/.pkg/app/bin/"* 2>/dev/null || true

  # Symlinks (idempotent)
  if [ -d "$APP_PATH/.pkg/app/config" ]; then
    ln -sf .pkg/app/config "$APP_PATH/config"
    touch "$APP_PATH/config/environment"
    [ ! -e "$APP_PATH/.pkg/app/environment" ] && touch "$APP_PATH/.pkg/app/environment"
  fi
  [ -d "$APP_PATH/.pkg/app/data" ]  && ln -sf .pkg/app/data  "$APP_PATH/data"
  [ -d "$APP_PATH/.pkg/app/cache" ] && ln -sf .pkg/app/cache "$APP_PATH/cache"

  mkdir -p "$APPMARKET_DIR"
  [ ! -e "$APPMARKET_DIR/$PKG_NAME" ] && ln -s "$APP_PATH" "$APPMARKET_DIR/$PKG_NAME"

  # Icons
  APP_ICO=""
  if [ -e "$APP_PATH/.pkg/ui/ico/app.png" ]; then
    mkdir -p "$IK_DIR/www/static/appmarket/$PKG_NAME/ico"
    cp -f "$APP_PATH/.pkg/ui/ico/app.png" "$IK_DIR/www/static/appmarket/$PKG_NAME/ico/"
    APP_ICO="/static/appmarket/$PKG_NAME/ico/app.png"
  fi
  if [ -d "$APP_PATH/.pkg/ui/images" ]; then
    mkdir -p "$IK_DIR/www/static/appmarket/$PKG_NAME/images"
    cp -rf "$APP_PATH/.pkg/ui/images/"* "$IK_DIR/www/static/appmarket/$PKG_NAME/images/" 2>/dev/null || true
  fi

  # Inject HOST_IP into .env
  ENV_FILE="$APP_PATH/.pkg/app/.env"
  [ -f "$ENV_FILE" ] || ENV_FILE="$APP_PATH/.pkg/app/environment"
  if [ -f "$ENV_FILE" ]; then
    if [ "$TYPE" = "1" ]; then
      HOST_IP_INJECT=$(ip -o -4 addr show doc_app_default 2>/dev/null | awk '{print $4}' | cut -d/ -f1 | head -1)
    else
      HOST_IP_INJECT=$(ip -o -4 addr show lan1 2>/dev/null | awk '{print $4}' | cut -d/ -f1 | head -1)
    fi
    [ -z "$HOST_IP_INJECT" ] && HOST_IP_INJECT=$(sqlite3 /etc/mnt/ikuai/config.db "select ip_mask from lan_config limit 1" 2>/dev/null | cut -d/ -f1)
    [ -z "$HOST_IP_INJECT" ] && HOST_IP_INJECT=$(ip -o -4 addr show 2>/dev/null | grep -v '127.0.0.1' | awk '{print $4}' | cut -d/ -f1 | head -1)
    if [ -n "$HOST_IP_INJECT" ]; then
      grep -v '^HOST_IP=' "$ENV_FILE" > "$ENV_FILE.tmp" 2>/dev/null && mv "$ENV_FILE.tmp" "$ENV_FILE"
      echo "HOST_IP=$HOST_IP_INJECT" >> "$ENV_FILE"
      log "injected HOST_IP=$HOST_IP_INJECT (type=$TYPE)"
    fi
  fi

  # Size
  SIZE=$(du -sk "$APP_PATH" 2>/dev/null | cut -f1)
  [ -z "$SIZE" ] && SIZE=0

  # Register / update DB
  TIMESTAMP=$(date +%s)
  DESC_B64=$(printf '%s' "$DESCRIPTION" | base64 | tr -d '\n')
  README=""
  [ -e "$APP_PATH/.pkg/readme" ] && README=$(cat "$APP_PATH/.pkg/readme" | base64 | tr -d '\n')
  CHANGELOG=""
  [ -e "$APP_PATH/.pkg/changelog" ] && CHANGELOG=$(cat "$APP_PATH/.pkg/changelog" | base64 | tr -d '\n')

  if [ "$IS_UPGRADE" = "1" ]; then
    # UPDATE preserves appid/enabled/origin/restart
    sqlite3 "$APPMARKET_DB" "UPDATE applist SET
      display_name='$DISPLAY_NAME', ico='$APP_ICO', type=$TYPE, size=$SIZE,
      current_version='$VERSION', update_time=$TIMESTAMP,
      maintainer='$MAINTAINER', maintainer_url='$MAINTAINER_URL',
      distributor='$DISTRIBUTOR', distributor_url='$DISTRIBUTOR_URL',
      desc='$DESC_B64', readme='$README', changelog='$CHANGELOG'
      WHERE name='$PKG_NAME'" 2>/dev/null || err "failed to update appmarket.db"
    log "updated (appid=$APPID, origin=$NAIXI_ORIGIN, restart=$NAIXI_RESTART)"
  else
    sqlite3 "$APPMARKET_DB" "DELETE FROM applist WHERE name='$PKG_NAME'" 2>/dev/null || true
    sqlite3 "$APPMARKET_DB" "INSERT INTO applist (
      enabled,name,appid,timestamp,display_name,location,ico,type,size,
      restart,current_version,maintainer,maintainer_url,distributor,distributor_url,
      origin,update_time,install_path,category,sub_category,desc,readme,changelog
    ) VALUES (
      'yes','$PKG_NAME',$APPID,$TIMESTAMP,'$DISPLAY_NAME','','$APP_ICO',$TYPE,$SIZE,
      $NAIXI_RESTART,'$VERSION','$MAINTAINER','$MAINTAINER_URL','$DISTRIBUTOR','$DISTRIBUTOR_URL',
      $NAIXI_ORIGIN,$TIMESTAMP,'$INSTALL_PATH',7,0,'$DESC_B64','$README','$CHANGELOG'
    )" 2>/dev/null || err "failed to write appmarket.db"
    log "registered (appid=$APPID, origin=$NAIXI_ORIGIN, restart=$NAIXI_RESTART)"
  fi

  # Start
  start_app "$APP_PATH"

  echo "0" > "$APP_PATH/log/run.status" 2>/dev/null || true
  log "done: $PKG_NAME v$VERSION (appid=$APPID)"
}

# ─── uninstall <name> ─────────────────────────────────────────────
do_uninstall() {
  NAME="$1"
  [ "$NAME" = "$SELF_NAME" ] && err "cannot uninstall naixi itself (use: naixi self-uninstall)"
  APP_PATH=$(app_path_of "$NAME")
  [ -d "$APP_PATH/.pkg" ] || err "app not found: $NAME"

  log "uninstalling $NAME..."
  SCRIPTS_DIR="$APP_PATH/.pkg/scripts"
  if [ -d "$SCRIPTS_DIR" ]; then
    cd "$SCRIPTS_DIR"
    [ -x "./PRE_UNINST.sh" ] && ./PRE_UNINST.sh >/dev/null 2>&1 || true
    [ -x "./stop.sh" ] && ./stop.sh >> "$APP_PATH/log/run.log" 2>&1 || true
    [ -x "./POST_UNINST.sh" ] && ./POST_UNINST.sh >/dev/null 2>&1 || true
    cd /
  fi

  sqlite3 "$APPMARKET_DB" "DELETE FROM applist WHERE name='$NAME'" 2>/dev/null || true
  rm -rf "$APP_PATH" "/var/appmarket/$NAME" "$IK_DIR/www/static/appmarket/$NAME"
  log "done: $NAME removed"
}

# ─── list (enhanced: origin/boot/size/path) ───────────────────────
do_list() {
  WD=$(get_workdisk)
  sqlite3 -header -column "$APPMARKET_DB" \
    "SELECT name AS NAME,
       current_version AS VER,
       CASE type WHEN 0 THEN 'native' ELSE 'docker' END AS TYPE,
       CASE WHEN enabled='yes' THEN 'on' ELSE 'off' END AS STATE,
       CASE WHEN enabled='yes' AND restart=1 AND origin=0 THEN 'yes' ELSE 'no' END AS BOOT,
       origin AS ORIGIN,
       CASE WHEN size>0 THEN (size||'K') ELSE '-' END AS SIZE,
       CASE install_path WHEN '/etc/log' THEN '/etc/log' ELSE install_path END AS DISK
     FROM applist ORDER BY name" 2>/dev/null \
    || echo "no apps installed"
  echo ""
  echo "Path: /etc/disk_user/<DISK>/apps/<NAME>  or  /etc/log/apps/<NAME> (system)"
}

# ─── boot auto-start management ───────────────────────────────────
# iKuai appmarket boot() starts apps where: enabled='yes' AND origin=0
# AND restart=1 AND integrity OK.  Disabling enabled='' fully prevents
# auto-start regardless of restart — this is the user-facing "off" switch.
do_boot() {
  SUB="${1:-}"
  [ "$#" -gt 0 ] && shift || true
  case "$SUB" in
    ""|list)
      sqlite3 -header -column "$APPMARKET_DB" \
        "SELECT name AS NAME,
          CASE WHEN enabled='yes' THEN 'enabled' ELSE 'disabled' END AS STATE,
          restart AS RESTART,
          origin AS ORIGIN,
          CASE WHEN enabled='yes' AND restart=1 AND origin=0 THEN 'BOOT' ELSE '-' END AS AUTOSTART
         FROM applist ORDER BY name" 2>/dev/null || echo "no apps"
      ;;
    enable)
      NAME="$1"
      [ -n "$NAME" ] || err "usage: naixi boot enable <name>"
      ROW=$(sqlite3 "$APPMARKET_DB" "SELECT name FROM applist WHERE name='$NAME'" 2>/dev/null)
      [ -n "$ROW" ] || err "app not found: $NAME"
      sqlite3 "$APPMARKET_DB" "UPDATE applist SET restart=1 WHERE name='$NAME'" 2>/dev/null
      log "$NAME: boot ON (restart=1 → auto-start on reboot)"
      ;;
    disable)
      NAME="$1"
      [ -n "$NAME" ] || err "usage: naixi boot disable <name>"
      [ "$NAME" = "$SELF_NAME" ] && err "cannot disable naixi's own boot (naixi always auto-starts)"
      ROW=$(sqlite3 "$APPMARKET_DB" "SELECT name FROM applist WHERE name='$NAME'" 2>/dev/null)
      [ -n "$ROW" ] || err "app not found: $NAME"
      sqlite3 "$APPMARKET_DB" "UPDATE applist SET restart=0 WHERE name='$NAME'" 2>/dev/null
      log "$NAME: boot OFF (restart=0 → no auto-start; still runnable manually)"
      ;;
    status)
      NAME="$1"
      [ -n "$NAME" ] || err "usage: naixi boot status <name>"
      sqlite3 "$APPMARKET_DB" \
        "SELECT name,enabled,restart,origin FROM applist WHERE name='$NAME'" 2>/dev/null \
        || err "app not found: $NAME"
      ;;
    *)
      err "usage: naixi boot [enable|disable|status|list] [name]"
      ;;
  esac
}

# ─── clean <name>: wipe data/cache, keep config + program ─────────
do_clean() {
  NAME="$1"
  APP_PATH=$(app_path_of "$NAME")
  [ -d "$APP_PATH/.pkg" ] || err "app not found: $NAME"
  log "cleaning data/cache for $NAME (config preserved)..."

  # Stop first
  SCRIPTS_DIR="$APP_PATH/.pkg/scripts"
  if [ -x "$SCRIPTS_DIR/stop.sh" ]; then
    cd "$SCRIPTS_DIR"
    ./stop.sh >> "$APP_PATH/log/run.log" 2>&1 2>/dev/null || true
    cd /
  fi

  # Wipe data + cache contents
  rm -rf "$APP_PATH/.pkg/app/data/"* 2>/dev/null || true
  rm -rf "$APP_PATH/.pkg/app/cache/"* 2>/dev/null || true

  log "done: $NAME data/cache wiped. Restart with: naixi boot enable $NAME && <reboot or manual start>"
}

# ─── log <name>: tail app run.log ─────────────────────────────────
do_log() {
  NAME="$1"
  LINES="${2:-50}"   # default 50 lines, naixi log <name> [N]
  APP_PATH=$(app_path_of "$NAME")
  [ -d "$APP_PATH/.pkg" ] || err "app not found: $NAME"

  RUNLOG="$APP_PATH/log/run.log"
  [ -f "$RUNLOG" ] || err "no run.log for $NAME (app may never have started)"

  # Also check for app-specific logs (e.g. ttyd.log, adguardhome log)
  EXTRA=""
  for f in "$APP_PATH/log/"*.log; do
    [ -f "$f" ] || continue
    case "$(basename "$f")" in
      run.log|install.log) ;;
      *) EXTRA="$EXTRA $(basename "$f")" ;;
    esac
  done 2>/dev/null

  echo "── $NAME run.log (last ${LINES} lines) ──"
  tail -n "$LINES" "$RUNLOG" 2>/dev/null || err "cannot read $RUNLOG"

  # Show run.status
  [ -f "$APP_PATH/log/run.status" ] && echo "── status: $(cat "$APP_PATH/log/run.status" 2>/dev/null) (0=running) ──"

  # Hint extra logs
  if [ -n "$EXTRA" ]; then
    echo "── other logs:$EXTRA ──"
    for f in "$APP_PATH/log/"*.log; do
      [ -f "$f" ] || continue
      bn=$(basename "$f")
      case "$bn" in run.log|install.log) continue ;; esac
      echo "── $bn (last ${LINES}) ──"
      tail -n "$LINES" "$f" 2>/dev/null
    done
  fi
}

# ─── move system/disk <name> ──────────────────────────────────────
# 在 /etc/log (system) 和 /etc/disk_user/<workdisk> (disk) 之间迁移
do_move() {
  DEST="$1"
  NAME="$2"
  [ -n "$DEST" ] && [ -n "$NAME" ] || err "usage: naixi move <system|disk> <name>"

  ROW=$(sqlite3 "$APPMARKET_DB" "SELECT install_path FROM applist WHERE name='$NAME'" 2>/dev/null)
  [ -n "$ROW" ] || err "app not found: $NAME"
  OLD_IP=$(echo "$ROW" | head -1)

  # 当前路径
  if [ "$OLD_IP" = "/etc/log" ]; then
    OLD_BASE="/etc/log/apps"
  else
    OLD_BASE="/etc/disk_user/$OLD_IP/apps"
  fi
  OLD_PATH="$OLD_BASE/$NAME"

  # 目标路径
  case "$DEST" in
    system)
      NEW_IP="/etc/log"
      NEW_BASE="/etc/log/apps"
      ;;
    disk)
      WD=$(sqlite3 "$DOCKER_DB" "select workdisk from global" 2>/dev/null || echo "")
      [ -n "$WD" ] && [ -d "/etc/disk_user/$WD" ] || err "no valid workdisk available"
      NEW_IP="$WD"
      NEW_BASE="/etc/disk_user/$WD/apps"
      ;;
    *)
      err "usage: naixi move <system|disk> <name>"
      ;;
  esac
  NEW_PATH="$NEW_BASE/$NAME"

  [ "$OLD_IP" = "$NEW_IP" ] && { log "$NAME already at $NEW_IP"; return 0; }
  [ -d "$OLD_PATH/.pkg" ] || err "old path missing: $OLD_PATH"

  log "moving $NAME: $OLD_IP → $NEW_IP"

  # Stop
  SCRIPTS_DIR="$OLD_PATH/.pkg/scripts"
  if [ -x "$SCRIPTS_DIR/stop.sh" ]; then
    cd "$SCRIPTS_DIR"
    ./stop.sh >> "$OLD_PATH/log/run.log" 2>&1 2>/dev/null || true
    cd /
  fi

  # Copy
  mkdir -p "$NEW_BASE"
  cp -a "$OLD_PATH" "$NEW_PATH" || err "copy failed"
  # Ensure perms
  [ -d "$NEW_PATH/.pkg/scripts" ] && chmod +x "$NEW_PATH/.pkg/scripts/"*.sh 2>/dev/null || true
  [ -d "$NEW_PATH/.pkg/app/bin" ] && chmod +x "$NEW_PATH/.pkg/app/bin/"* 2>/dev/null || true

  # Rebuild symlinks
  if [ -d "$NEW_PATH/.pkg/app/config" ]; then
    ln -sf .pkg/app/config "$NEW_PATH/config"
  fi
  [ -d "$NEW_PATH/.pkg/app/data" ]  && ln -sf .pkg/app/data  "$NEW_PATH/data"
  [ -d "$NEW_PATH/.pkg/app/cache" ] && ln -sf .pkg/app/cache "$NEW_PATH/cache"

  # Update /var/appmarket symlink
  rm -f "/var/appmarket/$NAME"
  ln -s "$NEW_PATH" "/var/appmarket/$NAME"

  # Update DB
  sqlite3 "$APPMARKET_DB" "UPDATE applist SET install_path='$NEW_IP' WHERE name='$NAME'" 2>/dev/null

  # Remove old
  rm -rf "$OLD_PATH"

  log "moved to $NEW_IP, restarting..."

  # Start at new location
  start_app "$NEW_PATH"
  echo "0" > "$NEW_PATH/log/run.status"
  log "done: $NAME now at $NEW_PATH"
}

# ─── update: self-update from dl ──────────────────────────────────
do_update() {
  ARCH=$(uname -m)
  case "$ARCH" in
    x86_64|amd64)   ARCH=amd64 ;;
    aarch64|arm64)  ARCH=arm64 ;;
    *) err "unsupported arch: $ARCH" ;;
  esac

  LATEST=$(curl -fsSL "$DL_BASE/naixi/latest?t=$(date +%s)" 2>/dev/null | tr -d '[:space:]')
  if [ -z "$LATEST" ]; then
    warn "cannot fetch latest version, using self-version"
    LATEST="$NAIXI_VERSION"
  fi

  CUR="$NAIXI_VERSION"
  # Check DB version if registered
  DBVER=$(sqlite3 "$APPMARKET_DB" "SELECT current_version FROM applist WHERE name='$SELF_NAME'" 2>/dev/null || echo "")
  [ -n "$DBVER" ] && CUR="$DBVER"

  case $(ver_cmp "$LATEST" "$CUR") in
    gt) log "update available: $CUR → $LATEST" ;;
    eq) log "already up to date ($CUR)"; return 0 ;;
    lt) warn "local ($CUR) newer than dl latest ($LATEST)?"; return 0 ;;
  esac

  URL="$DL_BASE/naixi/naixi-$LATEST-$ARCH.ipkg"
  log "downloading $URL..."
  do_install "$URL" -nocache
  log "naixi updated to $LATEST"
}

# ─── main dispatch ────────────────────────────────────────────────
CMD="${1:-help}"
[ "$#" -gt 0 ] && shift || true

case "$CMD" in
  install)
    [ "$#" -ge 1 ] || err "usage: naixi install <file.ipkg|url> [opts]"
    if [ "${1:-}" = "-nocache" ] || [ "${1:-}" = "--nocache" ]; then
      [ "$#" -ge 2 ] || err "usage: naixi install -nocache <url> [opts]"
      NOCACHE_OPT="$1"
      shift
      INSTALL_URL="$1"
      shift
      do_install "$INSTALL_URL" "$NOCACHE_OPT" "$@"
    else
      do_install "$@"
    fi
    ;;
  uninstall|remove|rm)
    [ "$#" -ge 1 ] || err "usage: naixi uninstall <name>"
    do_uninstall "$1"
    ;;
  list|ls)
    do_list
    ;;
  log)
    [ "$#" -ge 1 ] || err "usage: naixi log <name> [lines]"
    do_log "$1" "${2:-50}"
    ;;
  boot|autostart)
    do_boot "$@"
    ;;
  clean|reset)
    [ "$#" -ge 1 ] || err "usage: naixi clean <name>"
    do_clean "$1"
    ;;
  move)
    [ "$#" -ge 2 ] || err "usage: naixi move <system|disk> <name>"
    do_move "$1" "$2"
    ;;
  self-install)
    self_install
    ;;
  self-uninstall)
    self_uninstall
    ;;
  version|-v)
    echo "$NAIXI_VERSION"
    ;;
  update|self-update)
    do_update
    ;;
  help|--help|-h)
    cat <<EOF
Naixi v$NAIXI_VERSION — iKuai app manager

Usage:
  naixi install <file.ipkg|url> [opts]   Smart install / upgrade / downgrade
  naixi uninstall <name>                 Remove an app
  naixi list                             List installed apps
  naixi log <name> [N]                   Show last N lines of app log (default 50)
  naixi boot                             Show boot auto-start status
  naixi boot enable <name>               Enable auto-start on reboot
  naixi boot disable <name>              Disable auto-start on reboot
  naixi clean <name>                     Wipe app data/cache (keep config)
  naixi move system <name>               Move app to /etc/log partition
  naixi move disk <name>                 Move app to user disk
  naixi update                           Self-update from dl.naixi.net
  naixi version                          Show version

Install options:
  -official    origin=0 (default, auto-start on reboot)
  -sideload    origin=3 (NOT auto-started on boot)
  -norestart   restart=0
  -system      Install to /etc/log (system log partition)
  -disk        Install to user disk (workdisk)
  -nocache     Bypass CDN cache for this package download
  (default: disk if workdisk exists, else /etc/log)

Notes:
  - Upgrade preserves user data/config/cache. Downgrade allowed (with warning).
EOF
    ;;
  *)
    err "unknown command: $CMD (try: naixi help)"
    ;;
esac
