#!/bin/bash
# FreeLLMAPI — 飞牛 fnOS 原生启停（Node 直装，无 Docker）
# 成功条件：进程存活且 TCP 端口已监听（避免 DB 初始化未完成时 checkport 误判失败）
set -u

LOG_FILE="${TRIM_PKGVAR}/app.log"
PID_FILE="${TRIM_PKGVAR}/app.pid"
APP_DIR="${TRIM_APPDEST}/server"
ENTRY="${APP_DIR}/server/dist/index.js"
ENV_FILE="${TRIM_PKGETC}/app.env"
SETTINGS_CONF="${TRIM_PKGETC}/settings.conf"
NODE_BIN=""
# NAS 机械盘首次建库可能较慢
START_WAIT_SECS="${FREEAPI_START_WAIT_SECS:-45}"

fail_ui() {
  local msg="$1"
  if [ -n "${TRIM_TEMP_LOGFILE:-}" ]; then
    echo "$msg" >"$TRIM_TEMP_LOGFILE"
  fi
  log_msg "ERROR $msg"
}

log_msg() {
  mkdir -p "${TRIM_PKGVAR}" 2>/dev/null || true
  echo "$(date '+%Y-%m-%d %H:%M:%S') - $*" >>"$LOG_FILE" 2>/dev/null || true
}

resolve_node() {
  local cand
  for cand in \
    /var/apps/nodejs_v22/target/bin/node \
    /var/apps/nodejs_v20/target/bin/node \
    /var/apps/nodejs/target/bin/node \
    /usr/local/bin/node \
    /usr/bin/node; do
    if [ -x "$cand" ]; then
      NODE_BIN="$cand"
      export PATH="$(dirname "$cand"):${PATH:-}"
      return 0
    fi
  done
  for cand in \
    /var/apps/nodejs_v22/target/bin \
    /var/apps/nodejs_v20/target/bin \
    /var/apps/nodejs/target/bin; do
    if [ -x "$cand/node" ]; then
      NODE_BIN="$cand/node"
      export PATH="$cand:${PATH:-}"
      return 0
    fi
  done
  if command -v node >/dev/null 2>&1; then
    NODE_BIN="$(command -v node)"
    return 0
  fi
  return 1
}

load_env() {
  local injected=0
  local wizard_from_conf="" port_from_settings="" port_from_app=""

  # settings.conf: system wizard fields (wizard_port) + our PORT=
  if [ -f "$SETTINGS_CONF" ]; then
    wizard_from_conf="$(grep -E '^wizard_port=' "$SETTINGS_CONF" 2>/dev/null | tail -1 | cut -d= -f2- | tr -d '[:space:]"'"'"'')"
    port_from_settings="$(grep -E '^PORT=' "$SETTINGS_CONF" 2>/dev/null | tail -1 | cut -d= -f2- | tr -d '[:space:]"'"'"'')"
  fi

  if [ -f "$ENV_FILE" ]; then
    injected="$(grep -cE '^[A-Za-z_][A-Za-z0-9_]*=' "$ENV_FILE" 2>/dev/null || echo 0)"
    set -a
    # shellcheck disable=SC1090
    source "$ENV_FILE"
    set +a
    port_from_app="${PORT:-}"
    log_msg "Injected env (${injected}) from ${ENV_FILE}"
  fi

  # Port priority: wizard_port > settings PORT > app.env PORT > TRIM_SERVICE_PORT > 3001
  # (TRIM_SERVICE_PORT is fixed from manifest and must NOT override user wizard)
  if [ -n "${wizard_port:-}" ]; then
    export PORT="$wizard_port"
  elif [ -n "$wizard_from_conf" ]; then
    export PORT="$wizard_from_conf"
  elif [ -n "$port_from_settings" ]; then
    export PORT="$port_from_settings"
  elif [ -n "$port_from_app" ]; then
    export PORT="$port_from_app"
  else
    export PORT="${TRIM_SERVICE_PORT:-3001}"
  fi
  case "$PORT" in
    ''|*[!0-9]*) export PORT=3001 ;;
  esac

  # Server reads HOST (not HOST_BIND). Keep HOST_BIND for older notes / syno parity.
  export HOST_BIND="${HOST_BIND:-0.0.0.0}"
  export HOST="${HOST:-${HOST_BIND}}"
  export NODE_ENV="${NODE_ENV:-production}"
  if [ -z "${FREEAPI_DB_PATH:-}" ]; then
    if [ -n "${TRIM_DATA_SHARE_PATHS:-}" ]; then
      local p first=""
      first="${TRIM_DATA_SHARE_PATHS%%:*}"
      DATA_DIR="$first"
      IFS=':' read -r -a _shares <<< "${TRIM_DATA_SHARE_PATHS}"
      for p in "${_shares[@]}"; do
        case "$p" in
          */data|*/data/) DATA_DIR="$p"; break ;;
        esac
      done
    else
      DATA_DIR="${TRIM_PKGVAR}/data"
    fi
    export FREEAPI_DB_PATH="${DATA_DIR}/freeapi.db"
  fi
  mkdir -p "$(dirname "$FREEAPI_DB_PATH")" "${TRIM_PKGVAR}" 2>/dev/null || true
}

check_process() {
  local pid="$1"
  [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null
}

# True when something accepts TCP on 127.0.0.1:PORT (what fnOS checkport expects).
port_ready() {
  local port="${1:-$PORT}"
  # Prefer raw TCP — HTTP status codes must not matter
  if command -v nc >/dev/null 2>&1; then
    nc -z -w 1 127.0.0.1 "$port" 2>/dev/null && return 0
  fi
  if (echo >/dev/tcp/127.0.0.1/"$port") >/dev/null 2>&1; then
    return 0
  fi
  if command -v ss >/dev/null 2>&1; then
    ss -ltn 2>/dev/null | grep -qE "[:.]${port}[[:space:]]" && return 0
  fi
  if command -v netstat >/dev/null 2>&1; then
    netstat -ltn 2>/dev/null | grep -qE "[:.]${port}[[:space:]]" && return 0
  fi
  return 1
}

# Best-effort: PID currently listening on PORT (for orphan adoption).
find_listen_pid() {
  local port="${1:-$PORT}" pid=""
  if command -v ss >/dev/null 2>&1; then
    pid="$(ss -ltnp 2>/dev/null | grep -E "[:.]${port}[[:space:]]" | head -1 | sed -n 's/.*pid=\([0-9][0-9]*\).*/\1/p')"
  fi
  if [ -z "$pid" ] && command -v fuser >/dev/null 2>&1; then
    pid="$(fuser "${port}/tcp" 2>/dev/null | tr -cs '0-9' '\n' | grep -E '^[0-9]+$' | head -1)"
  fi
  if [ -z "$pid" ] && command -v lsof >/dev/null 2>&1; then
    pid="$(lsof -tiTCP:"${port}" -sTCP:LISTEN 2>/dev/null | head -1)"
  fi
  # Match our entry if possible
  if [ -z "$pid" ] && command -v pgrep >/dev/null 2>&1; then
    pid="$(pgrep -f "server/dist/index.js" 2>/dev/null | head -1)"
  fi
  echo "$pid"
}

read_pidfile() {
  if [ -f "$PID_FILE" ]; then
    head -n 1 "$PID_FILE" | tr -d '[:space:]'
  fi
}

status_app() {
  local pid
  pid="$(read_pidfile)"
  if [ -n "$pid" ] && check_process "$pid"; then
    # Prefer port-ready when possible; PID-only still counts as running for stop/status
    return 0
  fi
  # Orphan: port up but pidfile stale (node wrapper / lost pidfile)
  if [ -n "${PORT:-}" ] && port_ready "$PORT"; then
    pid="$(find_listen_pid "$PORT")"
    if [ -n "$pid" ] && check_process "$pid"; then
      echo "$pid" >"$PID_FILE"
      return 0
    fi
    return 0
  fi
  rm -f "$PID_FILE"
  return 1
}

# Pull latest first-run code from app.log into easy-to-find text files (NAS UI).
publish_setup_code() {
  local code="" dest p
  code="$(grep -E 'First-run setup code:' "$LOG_FILE" 2>/dev/null | tail -n 1 | sed -E 's/.*First-run setup code:[[:space:]]*//' | tr -d '[:space:]')"
  # Redaction may turn the code into [redacted-token]; ignore that
  case "$code" in
    ''|*'['*|*'redacted'*) return 0 ;;
  esac
  [ "${#code}" -ge 6 ] || return 0
  # Prefer paths users can open in 文件管理:
  #   @appdata/freellmapi/setup-code.txt
  #   @appshare/freellmapi/setup-code.txt  (data-share)
  #   …/data/setup-code.txt
  for dest in \
    "${TRIM_PKGVAR}/setup-code.txt" \
    "$(dirname "${FREEAPI_DB_PATH:-${TRIM_PKGVAR}/data/freeapi.db}")/setup-code.txt"; do
    {
      echo "FreeLLMAPI first-run setup code"
      echo ""
      echo "$code"
      echo ""
      echo "Paste into the dashboard「启动代码」field, then create the account."
    } >"$dest" 2>/dev/null || true
  done
  if [ -n "${TRIM_DATA_SHARE_PATHS:-}" ]; then
    IFS=':' read -r -a _shares <<< "${TRIM_DATA_SHARE_PATHS}"
    for p in "${_shares[@]}"; do
      [ -n "$p" ] || continue
      case "$p" in
        */data|*/data/) dest="$(dirname "$p")/setup-code.txt" ;;
        *) dest="${p%/}/setup-code.txt" ;;
      esac
      {
        echo "FreeLLMAPI first-run setup code"
        echo ""
        echo "$code"
        echo ""
        echo "Paste into the dashboard「启动代码」field, then create the account."
      } >"$dest" 2>/dev/null || true
    done
  fi
  log_msg "Setup code written to ${TRIM_PKGVAR}/setup-code.txt (also try @appshare/freellmapi/)"
}

# Running AND accepting connections (required before telling fnOS start succeeded).
service_ready() {
  local pid
  pid="$(read_pidfile)"
  if [ -n "$pid" ] && check_process "$pid" && port_ready "$PORT"; then
    return 0
  fi
  if port_ready "$PORT"; then
    pid="$(find_listen_pid "$PORT")"
    if [ -n "$pid" ]; then
      echo "$pid" >"$PID_FILE"
    fi
    return 0
  fi
  return 1
}

start_app() {
  if ! resolve_node; then
    fail_ui "未找到 Node.js。请先在「应用中心」安装 Node.js 20 或 22，再启用本应用。"
    exit 1
  fi
  load_env
  if [ -z "${ENCRYPTION_KEY:-}" ]; then
    fail_ui "缺少 ENCRYPTION_KEY。请重新安装本应用以生成密钥。"
    exit 1
  fi
  if service_ready; then
    log_msg "Already running pid=$(read_pidfile) port=$PORT"
    return 0
  fi
  # PID alive but still booting (DB migrate) — wait instead of double-start
  if status_app && ! port_ready "$PORT"; then
    log_msg "Process alive, waiting for port $PORT..."
  else
    if [ ! -f "$ENTRY" ]; then
      fail_ui "缺少程序入口: $ENTRY（请安装对应架构的 .fpk）"
      exit 1
    fi
    if [ ! -d "$APP_DIR/node_modules" ]; then
      fail_ui "缺少 node_modules（安装包不完整）"
      exit 1
    fi

    log_msg "Starting FreeLLMAPI on port $PORT db=$FREEAPI_DB_PATH node=$NODE_BIN host=$HOST"
    cd "$APP_DIR" || {
      fail_ui "无法进入目录: $APP_DIR"
      exit 1
    }

    # Clear stale pid; do not kill foreign listeners blindly
    rm -f "$PID_FILE"
    # Ensure child is in its own session so package stop/start tracking stays stable
    nohup "$NODE_BIN" "$ENTRY" >>"$LOG_FILE" 2>&1 &
    echo "$!" >"$PID_FILE"
    disown "$!" 2>/dev/null || true
  fi

  local i pid
  i=0
  while [ "$i" -lt "$START_WAIT_SECS" ]; do
    sleep 1
    i=$((i + 1))
    if service_ready; then
      pid="$(read_pidfile)"
      log_msg "Started OK pid=${pid:-?} port=$PORT (${i}s)"
      publish_setup_code
      return 0
    fi
    pid="$(read_pidfile)"
    if [ -n "$pid" ] && ! check_process "$pid" && ! port_ready "$PORT"; then
      break
    fi
  done

  {
    echo "进程启动后退出或端口 ${PORT} 未就绪。常见原因：未装 Node.js、端口被占用、数据目录无写权限、架构不匹配。"
    echo "日志: $LOG_FILE"
    tail -n 40 "$LOG_FILE" 2>/dev/null || true
  } >"${TRIM_TEMP_LOGFILE:-/dev/null}"
  log_msg "ERROR process exited or port not ready after ${START_WAIT_SECS}s"
  exit 1
}

stop_app() {
  log_msg "Stopping FreeLLMAPI..."
  load_env 2>/dev/null || true
  local pid
  pid="$(read_pidfile)"
  if [ -z "$pid" ] || ! check_process "$pid"; then
    pid="$(find_listen_pid "${PORT:-3001}")"
  fi
  if [ -z "$pid" ] || ! check_process "$pid"; then
    rm -f "$PID_FILE"
    return 0
  fi
  kill -TERM "$pid" 2>>"$LOG_FILE" || true
  local count=0
  while check_process "$pid" && [ "$count" -lt 15 ]; do
    sleep 1
    count=$((count + 1))
  done
  if check_process "$pid"; then
    kill -KILL "$pid" 2>>"$LOG_FILE" || true
  fi
  rm -f "$PID_FILE"
}

case "${1:-}" in
  start) start_app; exit 0 ;;
  stop) stop_app; exit 0 ;;
  status)
    load_env 2>/dev/null || true
    if status_app; then exit 0; fi
    exit 3
    ;;
  *)
    echo "Unknown command: ${1:-}" >"${TRIM_TEMP_LOGFILE:-/dev/null}"
    exit 1
    ;;
esac
