#!/usr/bin/env bash
set -Eeuo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DEFAULT_APP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
APP_DIR="${NTERMINAL_APP_DIR:-$DEFAULT_APP_DIR}"
DEFAULT_RUNTIME_DIR="${HOME:-$APP_DIR}/.nterminal"
ENV_FILE="${NTERMINAL_ENV_FILE:-$DEFAULT_RUNTIME_DIR/.env}"

COMMAND="${1:-help}"
if [[ $# -gt 0 ]]; then
  shift
fi

die() {
  echo "error: $*" >&2
  exit 1
}

info() {
  echo "$*"
}

trim() {
  local value="$1"
  value="${value#"${value%%[![:space:]]*}"}"
  value="${value%"${value##*[![:space:]]}"}"
  printf '%s' "$value"
}

strip_quotes() {
  local value="$1"
  if [[ "$value" == \"*\" && "$value" == *\" ]]; then
    value="${value:1:${#value}-2}"
  elif [[ "$value" == \'*\' && "$value" == *\' ]]; then
    value="${value:1:${#value}-2}"
  fi
  printf '%s' "$value"
}

load_env_file() {
  [[ -f "$ENV_FILE" ]] || return 0
  local line key value
  while IFS= read -r line || [[ -n "$line" ]]; do
    line="$(trim "$line")"
    [[ -z "$line" || "$line" == \#* ]] && continue
    [[ "$line" == export\ * ]] && line="$(trim "${line#export }")"
    [[ "$line" == *=* ]] || continue
    key="$(trim "${line%%=*}")"
    value="$(strip_quotes "$(trim "${line#*=}")")"
    [[ "$key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue
    if [[ -z "${!key+x}" ]]; then
      export "$key=$value"
    fi
  done < "$ENV_FILE"
}

resolve_path() {
  local value="$1"
  if [[ "$value" == /* ]]; then
    printf '%s' "$value"
  else
    printf '%s/%s' "$APP_DIR" "$value"
  fi
}

load_env_file

DEFAULT_STATE_PATH="$DEFAULT_RUNTIME_DIR/state.json"
DEFAULT_PID_PATH="$DEFAULT_RUNTIME_DIR/nterminal.pid"
DEFAULT_MONITOR_PID_PATH="$DEFAULT_RUNTIME_DIR/nterminal.monitor.pid"
DEFAULT_LOG_PATH="$DEFAULT_RUNTIME_DIR/nterminal.log"

HOST="${NTERMINAL_HOST:-127.0.0.1}"
PORT="${NTERMINAL_PORT:-3107}"
NODE_BIN="${NTERMINAL_NODE_BIN:-node}"
NPM_BIN="${NTERMINAL_NPM_BIN:-npm}"
PID_PATH="$(resolve_path "${NTERMINAL_PID_PATH:-${NTERMINAL_RUNTIME_PID_PATH:-$DEFAULT_PID_PATH}}")"
MONITOR_PID_PATH="$(resolve_path "${NTERMINAL_MONITOR_PID_PATH:-${NTERMINAL_RUNTIME_MONITOR_PID_PATH:-$DEFAULT_MONITOR_PID_PATH}}")"
LOG_PATH="$(resolve_path "${NTERMINAL_LOG_PATH:-${NTERMINAL_RUNTIME_LOG_PATH:-$DEFAULT_LOG_PATH}}")"
STATE_PATH="$(resolve_path "${NTERMINAL_STATE_PATH:-${NTERMINAL_RUNTIME_STATE_PATH:-$DEFAULT_STATE_PATH}}")"
HEALTH_TIMEOUT_SECONDS="${NTERMINAL_HEALTH_TIMEOUT_SECONDS:-15}"
STOP_TIMEOUT_SECONDS="${NTERMINAL_STOP_TIMEOUT_SECONDS:-20}"
FATAL_STARTUP_EXIT_CODE="${NTERMINAL_FATAL_STARTUP_EXIT_CODE:-78}"
RUNTIME_ENTRY="$APP_DIR/dist/server/index.js"
LAUNCHD_LABEL="${NTERMINAL_LAUNCHD_LABEL:-com.nterminal.local}"
UPDATE_LOCK_PATH="${NTERMINAL_UPDATE_LOCK_PATH:-}"
if [[ -n "$UPDATE_LOCK_PATH" && "$UPDATE_LOCK_PATH" != /* ]]; then
  UPDATE_LOCK_PATH="$(resolve_path "$UPDATE_LOCK_PATH")"
fi

cleanup_update_lock() {
  if [[ -n "$UPDATE_LOCK_PATH" ]]; then
    rm -f "$UPDATE_LOCK_PATH"
  fi
}

if [[ -n "$UPDATE_LOCK_PATH" ]]; then
  trap cleanup_update_lock EXIT
fi

export NTERMINAL_APP_DIR="$APP_DIR"
export NTERMINAL_ENV_FILE="$ENV_FILE"
export NTERMINAL_STATE_PATH="$STATE_PATH"
export NTERMINAL_PID_PATH="$PID_PATH"
export NTERMINAL_MONITOR_PID_PATH="$MONITOR_PID_PATH"
export NTERMINAL_LOG_PATH="$LOG_PATH"

health_host() {
  case "$HOST" in
    0.0.0.0)
      printf '127.0.0.1'
      ;;
    ::|\[::\])
      printf '[::1]'
      ;;
    *)
      printf '%s' "$HOST"
      ;;
  esac
}

health_url() {
  printf 'http://%s:%s/api/auth/session' "$(health_host)" "$PORT"
}

public_url() {
  printf 'http://%s:%s' "$HOST" "$PORT"
}

ensure_app_dir() {
  [[ -d "$APP_DIR" ]] || die "app directory not found: $APP_DIR"
}

missing_env_message() {
  printf '.env not found at %s; run nterminal onboarding' "$ENV_FILE"
}

ensure_env_file() {
  [[ -f "$ENV_FILE" ]] || die "$(missing_env_message)"
}

ensure_runtime() {
  if [[ -f "$RUNTIME_ENTRY" ]]; then
    return 0
  fi
  die "dist/server/index.js not found; reinstall nterminal"
}

ensure_directories() {
  mkdir -p "$(dirname "$PID_PATH")" "$(dirname "$MONITOR_PID_PATH")" "$(dirname "$LOG_PATH")"
  : >> "$LOG_PATH"
  chmod 600 "$LOG_PATH" 2>/dev/null || true
}

validate_required_env() {
  local missing=0
  for key in NTERMINAL_SESSION_SECRET; do
    local value="${!key:-}"
    if [[ -z "$value" || "$value" == replace-with-generate-secrets-output ]]; then
      echo "missing or placeholder env: $key"
      missing=1
    fi
  done
  if [[ "${NTERMINAL_ROLE:-}" != "main" && "${NTERMINAL_ROLE:-}" != "secondary" ]]; then
    echo "missing or invalid env: NTERMINAL_ROLE (expected main or secondary)"
    missing=1
  fi
  if [[ "${NTERMINAL_ROLE:-}" == "secondary" ]]; then
    local agent_token="${NTERMINAL_AGENT_TOKEN:-}"
    if (( ${#agent_token} < 32 )); then
      echo "missing or too-short env: NTERMINAL_AGENT_TOKEN"
      missing=1
    fi
  fi
  return "$missing"
}

doctor_runtime_state() {
  local role="${NTERMINAL_ROLE:-}"
  if [[ "$role" != "main" && "$role" != "secondary" ]]; then
    return 1
  fi
  if [[ ! -f "$STATE_PATH" ]]; then
    echo "fail state: state file not found at $STATE_PATH"
    return 1
  fi
  if [[ "$role" == "secondary" ]]; then
    echo "ok state: $STATE_PATH"
    return 0
  fi
  if "$NODE_BIN" -e 'const fs = require("node:fs"); const state = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.exit(state && typeof state.passwordCredential === "object" && state.passwordCredential ? 0 : 2);' "$STATE_PATH" >/dev/null 2>&1; then
    echo "ok state: main password credential present"
    return 0
  fi
  echo "fail state: main state has no passwordCredential at $STATE_PATH"
  return 1
}

prepare_start() {
  ensure_app_dir
  ensure_env_file
  ensure_runtime
  validate_required_env || die "generate real secrets before starting"
  ensure_directories
}

read_pid() {
  [[ -f "$PID_PATH" ]] || return 1
  read_pid_file "$PID_PATH"
}

read_monitor_pid() {
  [[ -f "$MONITOR_PID_PATH" ]] || return 1
  read_pid_file "$MONITOR_PID_PATH"
}

read_pid_file() {
  local pid_path="$1"
  [[ -f "$pid_path" ]] || return 1
  local pid
  pid="$(tr -d '[:space:]' < "$pid_path")"
  [[ "$pid" =~ ^[0-9]+$ ]] || return 1
  printf '%s' "$pid"
}

is_running() {
  local pid="$1"
  kill -0 "$pid" 2>/dev/null
}

parent_pid() {
  local pid="$1"
  if [[ -r "/proc/$pid/status" ]]; then
    awk '/^PPid:/ { print $2; exit }' "/proc/$pid/status" 2>/dev/null
    return
  fi
  if command -v ps >/dev/null 2>&1; then
    ps -o ppid= -p "$pid" 2>/dev/null | tr -d '[:space:]'
    return
  fi
  return 1
}

pid_controls_app() {
  local pid="$1"
  is_running "$pid" || return 1

  local cmdline=""
  if [[ -r "/proc/$pid/cmdline" ]]; then
    cmdline="$(tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null || true)"
    local cwd=""
    cwd="$(readlink "/proc/$pid/cwd" 2>/dev/null || true)"
    if [[ "$cmdline" == *"$RUNTIME_ENTRY"* ]]; then
      return 0
    fi
    if [[ "$cwd" == "$APP_DIR" && "$cmdline" == *"dist/server/index.js"* ]]; then
      return 0
    fi
    return 1
  fi

  if command -v ps >/dev/null 2>&1; then
    cmdline="$(ps -p "$pid" -o command= 2>/dev/null || true)"
    [[ "$cmdline" == *"$RUNTIME_ENTRY"* ]]
    return
  fi

  return 1
}

monitor_controls_app() {
  local pid="$1"
  is_running "$pid" || return 1

  local cmdline=""
  if [[ -r "/proc/$pid/cmdline" ]]; then
    cmdline="$(tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null || true)"
    [[ "$cmdline" == *"$SCRIPT_DIR/nterminalctl"* && "$cmdline" == *" supervise"* ]]
    return
  fi

  if command -v ps >/dev/null 2>&1; then
    cmdline="$(ps -p "$pid" -o command= 2>/dev/null || true)"
    [[ "$cmdline" == *"$SCRIPT_DIR/nterminalctl"* && "$cmdline" == *" supervise"* ]]
    return
  fi

  return 1
}

current_pid() {
  local pid
  pid="$(read_pid || true)"
  if [[ -n "$pid" && "$(pid_controls_app "$pid" && echo yes || echo no)" == yes ]]; then
    printf '%s' "$pid"
    return 0
  fi

  # npm can replace the package directory while the old Node process is still
  # listening. If the pid file is missing, recover by recognizing the port owner
  # as this package runtime before deciding the app is stopped.
  pid="$(port_owner_pid || true)"
  if [[ -n "$pid" && "$(pid_controls_app "$pid" && echo yes || echo no)" == yes ]]; then
    printf '%s' "$pid"
    return 0
  fi
  return 1
}

current_monitor_pid() {
  local pid
  pid="$(read_monitor_pid || true)"
  if [[ -n "$pid" && "$(monitor_controls_app "$pid" && echo yes || echo no)" == yes ]]; then
    printf '%s' "$pid"
    return 0
  fi

  local child_pid parent
  child_pid="$(current_pid || true)"
  if [[ -n "$child_pid" ]]; then
    parent="$(parent_pid "$child_pid" || true)"
    if [[ -n "$parent" && "$(monitor_controls_app "$parent" && echo yes || echo no)" == yes ]]; then
      printf '%s' "$parent"
      return 0
    fi
  fi
  return 1
}

launchd_domain() {
  [[ -n "$LAUNCHD_LABEL" ]] || return 1
  printf 'gui/%s/%s' "$(id -u)" "$LAUNCHD_LABEL"
}

launchd_print() {
  command -v launchctl >/dev/null 2>&1 || return 1
  local domain
  domain="$(launchd_domain)" || return 1
  launchctl print "$domain" 2>/dev/null
}

launchd_controls_app() {
  local output
  output="$(launchd_print || true)"
  [[ -n "$output" ]] || return 1
  case "$output" in
    *"working directory = $APP_DIR"*|*"cd $APP_DIR "*) return 0 ;;
    *) return 1 ;;
  esac
}

launchd_agent_plist() {
  local home_dir="${HOME:-}"
  [[ -n "$home_dir" ]] || return 1
  printf '%s/Library/LaunchAgents/%s.plist' "$home_dir" "$LAUNCHD_LABEL"
}

unload_launchd_if_controlled() {
  launchd_controls_app || return 0
  local domain
  domain="$(launchd_domain)" || return 0
  launchctl bootout "$domain" >/dev/null 2>&1 || launchctl remove "$LAUNCHD_LABEL" >/dev/null 2>&1 || true
  rm -f "$PID_PATH"
  info "NTerminal launchd agent unloaded label=$LAUNCHD_LABEL"
}

remove_owned_launchd_plist() {
  local plist
  plist="$(launchd_agent_plist || true)"
  [[ -n "$plist" && -f "$plist" ]] || return 0
  if grep -F "$APP_DIR" "$plist" >/dev/null 2>&1; then
    rm -f "$plist"
    info "NTerminal launchd plist removed path=$plist"
  fi
}

remove_stale_pid() {
  local pid
  pid="$(read_pid || true)"
  if [[ -n "$pid" && ! "$(pid_controls_app "$pid" && echo yes || echo no)" == yes ]]; then
    rm -f "$PID_PATH"
  fi
}

remove_stale_monitor_pid() {
  local pid
  pid="$(read_monitor_pid || true)"
  if [[ -n "$pid" && ! "$(monitor_controls_app "$pid" && echo yes || echo no)" == yes ]]; then
    rm -f "$MONITOR_PID_PATH"
  fi
}

node_health_check() {
  "$NODE_BIN" -e '
const http = require("node:http");
const url = process.argv[1];
const request = http.get(url, { timeout: 1000 }, (response) => {
  response.resume();
  process.exit(response.statusCode && response.statusCode >= 200 && response.statusCode < 500 ? 0 : 1);
});
request.on("timeout", () => request.destroy());
request.on("error", () => process.exit(1));
' "$(health_url)" >/dev/null 2>&1
}

start_daemon() {
  if command -v setsid >/dev/null 2>&1; then
    nohup setsid bash "$SCRIPT_DIR/nterminalctl" supervise >> "$LOG_PATH" 2>&1 &
  else
    nohup bash "$SCRIPT_DIR/nterminalctl" supervise >> "$LOG_PATH" 2>&1 &
  fi
  local monitor_pid="$!"
  disown "$monitor_pid" 2>/dev/null || true
  echo "$monitor_pid" > "$MONITOR_PID_PATH"
  chmod 600 "$MONITOR_PID_PATH" 2>/dev/null || true

  local pid=""
  local deadline=$((SECONDS + HEALTH_TIMEOUT_SECONDS))
  while (( SECONDS <= deadline )); do
    if ! is_running "$monitor_pid"; then
      return 1
    fi
    pid="$(current_pid || true)"
    [[ -n "$pid" ]] && return 0
    sleep 0.25
  done
  return 1
}

stop_supervised_child() {
  local pid="$1"
  [[ -n "$pid" ]] || return 0
  if ! is_running "$pid"; then
    rm -f "$PID_PATH"
    return 0
  fi
  kill -TERM "$pid" 2>/dev/null || true
  local deadline=$((SECONDS + STOP_TIMEOUT_SECONDS))
  while (( SECONDS <= deadline )); do
    if ! is_running "$pid"; then
      rm -f "$PID_PATH"
      return 0
    fi
    sleep 0.25
  done
  kill -KILL "$pid" 2>/dev/null || true
  rm -f "$PID_PATH"
}

cmd_supervise() {
  ensure_app_dir
  ensure_env_file
  ensure_runtime
  validate_required_env || die "generate real secrets before starting"
  ensure_directories
  echo "$$" > "$MONITOR_PID_PATH"
  chmod 600 "$MONITOR_PID_PATH" 2>/dev/null || true

  local child_pid=""
  local stop_requested=0
  supervisor_shutdown() {
    stop_requested=1
    trap - TERM INT
    stop_supervised_child "$child_pid"
    rm -f "$MONITOR_PID_PATH"
    exit 0
  }
  trap supervisor_shutdown TERM INT

  while true; do
    cd "$APP_DIR"
    "$NODE_BIN" "$RUNTIME_ENTRY" >> "$LOG_PATH" 2>&1 &
    child_pid="$!"
    echo "$child_pid" > "$PID_PATH"
    chmod 600 "$PID_PATH" 2>/dev/null || true

    set +e
    wait "$child_pid"
    local status="$?"
    set -e

    rm -f "$PID_PATH"
    if (( stop_requested == 1 )); then
      rm -f "$MONITOR_PID_PATH"
      exit 0
    fi
    if (( status == FATAL_STARTUP_EXIT_CODE )); then
      printf '[%s] NTerminal process exited with fatal startup status=%s; supervisor will not restart it\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$status" >> "$LOG_PATH"
      rm -f "$MONITOR_PID_PATH"
      exit "$status"
    fi

    printf '[%s] NTerminal process exited status=%s; restarting in 2s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$status" >> "$LOG_PATH"
    sleep 2
  done
}

start_launchd() {
  local domain
  domain="$(launchd_domain)" || return 1
  launchctl kickstart -k "$domain"

  local pid
  local deadline=$((SECONDS + HEALTH_TIMEOUT_SECONDS))
  while (( SECONDS <= deadline )); do
    pid="$(current_pid || true)"
    if [[ -n "$pid" ]] && wait_for_health "$pid"; then
      info "NTerminal started via launchd pid=$pid url=$(public_url) log=$LOG_PATH"
      return 0
    fi
    sleep 0.25
  done

  echo "NTerminal failed to become healthy via launchd. Last log lines:" >&2
  tail -n 40 "$LOG_PATH" >&2 || true
  return 1
}

wait_for_health() {
  local pid="$1"
  local deadline=$((SECONDS + HEALTH_TIMEOUT_SECONDS))
  while (( SECONDS <= deadline )); do
    if ! is_running "$pid"; then
      return 1
    fi
    if node_health_check; then
      return 0
    fi
    sleep 0.25
  done
  return 1
}

port_owner_pid() {
  if command -v lsof >/dev/null 2>&1; then
    lsof -nP -iTCP:"$PORT" -sTCP:LISTEN -t 2>/dev/null | head -n 1
    return
  fi
  if command -v ss >/dev/null 2>&1; then
    ss -ltnp "sport = :$PORT" 2>/dev/null | sed -n 's/.*pid=\([0-9]\+\).*/\1/p' | head -n 1
    return
  fi
  return 1
}

assert_port_available() {
  local owner
  owner="$(port_owner_pid || true)"
  [[ -z "$owner" ]] && return 0
  local pid
  pid="$(current_pid || true)"
  [[ -n "$pid" && "$owner" == "$pid" ]] && return 0
  die "port $PORT is already in use by pid $owner"
}

cmd_status() {
  remove_stale_pid
  remove_stale_monitor_pid
  local pid
  pid="$(current_pid || true)"
  if [[ -n "$pid" ]]; then
    info "NTerminal running pid=$pid url=$(public_url) log=$LOG_PATH"
    return 0
  fi
  local monitor_pid
  monitor_pid="$(current_monitor_pid || true)"
  if [[ -n "$monitor_pid" ]]; then
    info "NTerminal supervisor running pid=$monitor_pid; server is restarting url=$(public_url) log=$LOG_PATH"
    return 4
  fi
  info "NTerminal stopped pid_file=$PID_PATH"
  return 3
}

cmd_start() {
  prepare_start
  remove_stale_pid
  remove_stale_monitor_pid

  local pid
  pid="$(current_pid || true)"
  if [[ -n "$pid" ]]; then
    info "NTerminal already running pid=$pid url=$(public_url)"
    return 0
  fi
  local monitor_pid
  monitor_pid="$(current_monitor_pid || true)"
  if [[ -n "$monitor_pid" ]]; then
    info "NTerminal supervisor already running pid=$monitor_pid url=$(public_url)"
    return 0
  fi

  if launchd_controls_app; then
    start_launchd
    return 0
  fi

  assert_port_available
  if ! start_daemon; then
    rm -f "$MONITOR_PID_PATH"
    echo "NTerminal failed to start supervisor. Last log lines:" >&2
    tail -n 40 "$LOG_PATH" >&2 || true
    return 1
  fi
  chmod 600 "$PID_PATH" 2>/dev/null || true
  pid="$(current_pid || true)"
  if [[ -z "$pid" ]]; then
    rm -f "$MONITOR_PID_PATH"
    echo "NTerminal failed to start supervisor. Last log lines:" >&2
    tail -n 40 "$LOG_PATH" >&2 || true
    return 1
  fi

  if wait_for_health "$pid"; then
    monitor_pid="$(current_monitor_pid || true)"
    if [[ -n "$monitor_pid" ]]; then
      info "NTerminal started pid=$pid supervisor=$monitor_pid url=$(public_url) log=$LOG_PATH"
    else
      info "NTerminal started pid=$pid url=$(public_url) log=$LOG_PATH"
    fi
    return 0
  fi

  local exit_hint="failed to become healthy"
  if ! is_running "$pid"; then
    exit_hint="process exited during startup"
  fi
  monitor_pid="$(current_monitor_pid || true)"
  if [[ -n "$monitor_pid" ]]; then
    kill -TERM "$monitor_pid" 2>/dev/null || true
  fi
  rm -f "$PID_PATH" "$MONITOR_PID_PATH"
  echo "NTerminal $exit_hint. Health check: $(health_url). Last log lines:" >&2
  tail -n 40 "$LOG_PATH" >&2 || true
  return 1
}

cmd_stop() {
  remove_stale_pid
  remove_stale_monitor_pid
  local pid
  pid="$(current_pid || true)"
  local monitor_pid
  monitor_pid="$(current_monitor_pid || true)"
  if [[ -z "$pid" && -z "$monitor_pid" ]]; then
    info "NTerminal already stopped"
    return 0
  fi

  if [[ -n "$monitor_pid" ]]; then
    kill -TERM "$monitor_pid" 2>/dev/null || true
    local monitor_deadline=$((SECONDS + STOP_TIMEOUT_SECONDS))
    while (( SECONDS <= monitor_deadline )); do
      if ! is_running "$monitor_pid"; then
        rm -f "$MONITOR_PID_PATH"
        break
      fi
      sleep 0.25
    done
    if is_running "$monitor_pid"; then
      kill -KILL "$monitor_pid" 2>/dev/null || true
      rm -f "$MONITOR_PID_PATH"
    fi

    if [[ -n "$pid" && "$(is_running "$pid" && echo yes || echo no)" == yes ]]; then
      stop_supervised_child "$pid"
    fi

    rm -f "$PID_PATH" "$MONITOR_PID_PATH"
    if [[ -n "$pid" ]]; then
      info "NTerminal stopped pid=$pid supervisor=$monitor_pid"
    else
      info "NTerminal stopped supervisor=$monitor_pid"
    fi
    return 0
  fi

  kill -TERM "$pid" 2>/dev/null || true
  local deadline=$((SECONDS + STOP_TIMEOUT_SECONDS))
  while (( SECONDS <= deadline )); do
    if ! is_running "$pid"; then
      rm -f "$PID_PATH"
      info "NTerminal stopped pid=$pid"
      return 0
    fi
    sleep 0.25
  done

  kill -KILL "$pid" 2>/dev/null || true
  rm -f "$PID_PATH"
  info "NTerminal force-stopped pid=$pid"
}

cmd_restart() {
  if launchd_controls_app; then
    start_launchd
  else
    prepare_start
    # Replace the supervisor instead of only restarting its child. The
    # supervisor keeps its original environment, so reusing it after onboarding
    # or npm replacement can restart the server with stale .env values or old
    # package code.
    cmd_stop >/dev/null || true
    cmd_start
  fi
  info "NTerminal restarted"
}

cmd_restart_after_update() {
  prepare_start
  if launchd_controls_app; then
    start_launchd
    info "NTerminal restarted after update"
    return 0
  fi
  cmd_stop >/dev/null || true
  cmd_start
}

cmd_build() {
  ensure_app_dir
  ensure_runtime
  info "Package install uses bundled dist; no build needed."
}

cmd_deploy() {
  ensure_app_dir
  ensure_env_file
  ensure_runtime
  cmd_restart
}

cmd_logs() {
  local follow=0
  local lines=100
  if [[ "${1:-}" == "-f" || "${1:-}" == "follow" ]]; then
    follow=1
    shift || true
  fi
  if [[ "${1:-}" =~ ^[0-9]+$ ]]; then
    lines="$1"
  fi
  [[ -f "$LOG_PATH" ]] || die "log file not found: $LOG_PATH"
  if (( follow == 1 )); then
    tail -n "$lines" -f "$LOG_PATH"
  else
    tail -n "$lines" "$LOG_PATH"
  fi
}

clean_state_paths() {
  local state_path="$1"
  local pid_path="$2"
  local log_path="$3"
  local monitor_pid_path="${4:-}"
  local state_dir
  state_dir="$(dirname "$state_path")"
  rm -f "$state_path" "$pid_path" "$log_path" "$state_dir/update.lock"
  if [[ -n "$monitor_pid_path" ]]; then
    rm -f "$monitor_pid_path"
  fi
  rm -rf "$state_dir/notification-assets"
}

cmd_clean() {
  local force=0
  local remove_env=0
  local kill_tmux=1
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --force) force=1 ;;
      --env) remove_env=1 ;;
      --keep-tmux) kill_tmux=0 ;;
      *) die "unknown clean option: $1" ;;
    esac
    shift
  done

  if (( force != 1 )); then
    die "clean is destructive; rerun with: $(control_command clean) --force [--env] [--keep-tmux]"
  fi

  ensure_app_dir
  cmd_stop >/dev/null
  if (( kill_tmux == 1 )) && command -v tmux >/dev/null 2>&1; then
    tmux -L nterminal kill-server >/dev/null 2>&1 || true
  fi

  local state_dir
  state_dir="$(dirname "$STATE_PATH")"
  clean_state_paths "$STATE_PATH" "$PID_PATH" "$LOG_PATH" "$MONITOR_PID_PATH"
  if (( remove_env == 1 )); then
    rm -f "$ENV_FILE"
  fi

  info "NTerminal cleaned state_dir=$state_dir env_removed=$remove_env tmux_killed=$kill_tmux"
}

cmd_uninstall() {
  local force=0
  local kill_tmux=1
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --force) force=1 ;;
      --keep-tmux) kill_tmux=0 ;;
      *) die "unknown uninstall option: $1" ;;
    esac
    shift
  done

  if (( force != 1 )); then
    die "uninstall is destructive; rerun with: $(control_command uninstall) --force [--keep-tmux]"
  fi

  ensure_app_dir
  unload_launchd_if_controlled
  cmd_stop >/dev/null
  if (( kill_tmux == 1 )) && command -v tmux >/dev/null 2>&1; then
    tmux -L nterminal kill-server >/dev/null 2>&1 || true
  fi
  clean_state_paths "$STATE_PATH" "$PID_PATH" "$LOG_PATH" "$MONITOR_PID_PATH"
  rm -f "$ENV_FILE"
  remove_owned_launchd_plist

  info "NTerminal runtime uninstalled state_dir=$(dirname "$STATE_PATH") env_removed=1 tmux_killed=$kill_tmux"
  info "Package files are still installed. To remove them, run: npm uninstall -g nterminal"
}

control_command() {
  local command="$1"
  printf 'nterminal %s' "$command"
}

cmd_pid() {
  local pid
  pid="$(current_pid || true)"
  [[ -n "$pid" ]] || return 3
  echo "$pid"
}

cmd_doctor() {
  local failed=0
  ensure_app_dir

  if command -v "$NODE_BIN" >/dev/null 2>&1; then
    local node_major
    node_major="$("$NODE_BIN" -p 'Number(process.versions.node.split(".")[0])')"
    if (( node_major < 22 )); then
      echo "fail node: version must be >=22"
      failed=1
    else
      echo "ok node: $("$NODE_BIN" --version)"
    fi
  else
    echo "fail node: command not found: $NODE_BIN"
    failed=1
  fi

  if command -v "$NPM_BIN" >/dev/null 2>&1; then
    echo "ok npm: $("$NPM_BIN" --version)"
  else
    echo "fail npm: command not found: $NPM_BIN"
    failed=1
  fi

  if [[ -f "$ENV_FILE" ]]; then
    echo "ok env: $ENV_FILE"
    validate_required_env || failed=1
  else
    echo "fail env: .env not found at $ENV_FILE"
    failed=1
  fi
  doctor_runtime_state || failed=1

  if [[ -f "$RUNTIME_ENTRY" ]]; then
    echo "ok runtime: dist/server/index.js"
  else
    echo "fail runtime: dist/server/index.js not found"
    failed=1
  fi

  ensure_directories
  if [[ -w "$(dirname "$PID_PATH")" && -w "$(dirname "$MONITOR_PID_PATH")" && -w "$(dirname "$LOG_PATH")" ]]; then
    echo "ok writable: pid/monitor/log directories"
  else
    echo "fail writable: pid/monitor/log directories"
    failed=1
  fi

  local pid owner
  pid="$(current_pid || true)"
  owner="$(port_owner_pid || true)"
  if [[ -z "$owner" || -n "$pid" && "$owner" == "$pid" ]]; then
    echo "ok port: $PORT"
  else
    echo "fail port: $PORT owned by pid $owner"
    failed=1
  fi

  return "$failed"
}

cmd_help() {
  cat <<EOF
Usage: nterminal <command>

Commands:
  status         Show process status. Exit 0 when running, 3 when stopped, 4 when the supervisor is restarting it.
  start          Start the built server in the background.
  stop           Stop the managed server and supervisor with SIGTERM, then SIGKILL after timeout.
  restart        Stop and start the server.
  restart-after-update
                 Stop the supervisor and start again after an npm package update.
  build          No-op for package installs; verifies the bundled runtime exists.
  deploy         Restart the installed package using the bundled runtime.
  clean --force  Stop server, kill NTerminal tmux sessions, and remove local state/log/pid files.
                 Add --env to also remove .env. Add --keep-tmux to preserve live panes.
  uninstall --force
                 Stop server, unload NTerminal launchd agent, remove local runtime files and .env.
                 Add --keep-tmux to preserve live panes.
  logs [N]       Show the last N log lines. Default: 100.
  logs -f [N]    Follow logs.
  doctor         Check Node/npm/env/runtime/pid/log/port readiness.
  pid            Print the running PID. Exit 3 when stopped.
  url            Print the configured local URL.
  help           Show this help.

Environment:
  NTERMINAL_APP_DIR                  Override app directory.
  NTERMINAL_ENV_FILE                 Override .env path.
  NTERMINAL_STATE_PATH               Default ~/.nterminal/state.json.
  NTERMINAL_PID_PATH                 Default ~/.nterminal/nterminal.pid.
  NTERMINAL_MONITOR_PID_PATH         Default ~/.nterminal/nterminal.monitor.pid.
  NTERMINAL_LOG_PATH                 Default ~/.nterminal/nterminal.log.
  NTERMINAL_HEALTH_TIMEOUT_SECONDS   Default 15.
  NTERMINAL_STOP_TIMEOUT_SECONDS     Default 20.
  NTERMINAL_LAUNCHD_LABEL            Default com.nterminal.local. Used when a loaded LaunchAgent controls this app.
EOF
}

case "$COMMAND" in
  status) cmd_status "$@" ;;
  start) cmd_start "$@" ;;
  supervise) cmd_supervise "$@" ;;
  stop) cmd_stop "$@" ;;
  restart|reload) cmd_restart "$@" ;;
  restart-after-update) cmd_restart_after_update "$@" ;;
  build) cmd_build "$@" ;;
  deploy) cmd_deploy "$@" ;;
  clean|purge) cmd_clean "$@" ;;
  uninstall|remove) cmd_uninstall "$@" ;;
  logs|log) cmd_logs "$@" ;;
  doctor) cmd_doctor "$@" ;;
  pid) cmd_pid "$@" ;;
  url) public_url ;;
  help|-h|--help) cmd_help ;;
  *) cmd_help >&2; die "unknown command: $COMMAND" ;;
esac
