#!/bin/bash
# Wrapper for ~/.kylinbot-pyenv/bin/python3
#
# Repo backup / packaging source of the user-pyenv python3 shim.
# install-into-debian.sh installs this file as template/bin/python3 (replacing
# the CPython python3 → python3.12 symlink). After kylinbot-pyenv-clone, the
# same layout appears under ~/.kylinbot-pyenv/bin/python3.
#
# Manual restore (dev only; packaging is the supported path):
#   install -Dm755 python-runtime/scripts/kylinbot-python3-shim \
#     ~/.kylinbot-pyenv/bin/python3
#   install -Dm644 python-runtime/scripts/kylinbot-python3-system-routes.list \
#     ~/.kylinbot-pyenv/bin/kylinbot-python3-system-routes.list
#
# Route interpreter by the script being executed:
#   - path matches routes list (prefix:) and not exclude: → system Python
#   - otherwise / no script → venv Python (this dir's python3.12)
#
# Bare names like `kylin-software-center-cli` are resolved via PATH
# (same idea as `#!/usr/bin/env python3` + command lookup), then the
# absolute script path is passed to the chosen interpreter.
#
# Logs every invocation to python3.calls.log in this directory.
#
# Optional:
#   KYLINBOT_PYTHON_WRAPPER_STDERR=1
#   KYLINBOT_PYTHON_WRAPPER_ROUTES_FILE=/path/to/routes.list
#   KYLINBOT_PYTHON_WRAPPER_SYSTEM_PREFIXES="/usr/bin /usr/sbin"
#     — if set, replaces prefix: rules from the list (exclude: still loaded)
#
# On system route: unset VIRTUAL_ENV/PYTHONHOME and remove pyenv bin dirs from
# PATH so nested `python3` / `env python3` in system scripts do not re-enter shim.
set -u

DIR="$(cd "$(dirname "$0")" && pwd)"
VENV_PYTHON="${DIR}/python3.12"
SYSTEM_PYTHON="/usr/bin/python3.12"
[[ -x "$SYSTEM_PYTHON" ]] || SYSTEM_PYTHON="/usr/bin/python3"

LOG_FILE="${DIR}/python3.calls.log"
TS="$(date '+%Y-%m-%d %H:%M:%S.%3N' 2>/dev/null || date '+%Y-%m-%d %H:%M:%S')"
PID="$$"
PPID_VAL="${PPID:-?}"
CWD="$(pwd -P 2>/dev/null || pwd)"

ROUTES_FILE="${KYLINBOT_PYTHON_WRAPPER_ROUTES_FILE:-${DIR}/kylinbot-python3-system-routes.list}"

# Loaded from routes list (and optional env override for prefixes).
SYSTEM_PREFIXES=()
SYSTEM_EXCLUDES=()

load_routes() {
  SYSTEM_PREFIXES=()
  SYSTEM_EXCLUDES=()
  local line key val canon
  if [[ -f "$ROUTES_FILE" ]]; then
    while IFS= read -r line || [[ -n "$line" ]]; do
      line="${line%%#*}"
      line="$(printf '%s' "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
      [[ -z "$line" ]] && continue
      key="${line%%:*}"
      val="${line#*:}"
      [[ "$key" == "$line" ]] && continue
      canon="$val"
      if command -v realpath >/dev/null 2>&1; then
        canon="$(realpath -m "$val" 2>/dev/null || printf '%s' "$val")"
      fi
      case "$key" in
        prefix)
          SYSTEM_PREFIXES+=("$val")
          [[ "$canon" != "$val" ]] && SYSTEM_PREFIXES+=("$canon")
          ;;
        exclude)
          SYSTEM_EXCLUDES+=("$val")
          [[ "$canon" != "$val" ]] && SYSTEM_EXCLUDES+=("$canon")
          ;;
      esac
    done < "$ROUTES_FILE"
  fi

  # Env override: space-separated prefixes replace list prefixes.
  if [[ -n "${KYLINBOT_PYTHON_WRAPPER_SYSTEM_PREFIXES:-}" ]]; then
    SYSTEM_PREFIXES=()
    # shellcheck disable=SC2206
    SYSTEM_PREFIXES=(${KYLINBOT_PYTHON_WRAPPER_SYSTEM_PREFIXES})
  fi

  # Safe defaults if list missing and no env override.
  if ((${#SYSTEM_PREFIXES[@]} == 0)); then
    SYSTEM_PREFIXES=("/usr/bin" "/usr/sbin")
  fi
  if ((${#SYSTEM_EXCLUDES[@]} == 0)); then
    SYSTEM_EXCLUDES=("/opt/kylinbot" "/opt/kylin-bot")
  fi
}

path_under() {
  local path="$1"
  local root="$2"
  [[ -z "$root" ]] && return 1
  [[ "$path" == "$root" || "$path" == "$root"/* ]]
}

is_system_path() {
  local path="$1"
  local p
  for p in "${SYSTEM_EXCLUDES[@]}"; do
    if path_under "$path" "$p"; then
      return 1
    fi
  done
  for p in "${SYSTEM_PREFIXES[@]}"; do
    if path_under "$path" "$p"; then
      return 0
    fi
  done
  return 1
}

# Drop a single directory entry from a colon-separated PATH (normalized compare).
path_without_dir() {
  local path="$1"
  local drop="$2"
  [[ -z "$drop" ]] && { printf '%s' "$path"; return 0; }
  local drop_canon="$drop"
  if command -v realpath >/dev/null 2>&1; then
    drop_canon="$(realpath -m "$drop" 2>/dev/null || printf '%s' "$drop")"
  fi
  local IFS=':'
  local p p_canon
  local -a out=()
  for p in $path; do
    [[ -z "$p" ]] && continue
    p_canon="$p"
    if command -v realpath >/dev/null 2>&1; then
      p_canon="$(realpath -m "$p" 2>/dev/null || printf '%s' "$p")"
    fi
    if [[ "$p" == "$drop" || "$p_canon" == "$drop_canon" ]]; then
      continue
    fi
    out+=("$p")
  done
  if ((${#out[@]} == 0)); then
    printf '/usr/local/bin:/usr/bin:/bin'
  else
    local IFS=':'
    printf '%s' "${out[*]}"
  fi
}

# Remove shim dir and known pyenv locations from PATH before system Python runs.
purify_path_for_system() {
  local path="${PATH-}"
  local ve="${VIRTUAL_ENV:-}"
  local kp="${KYLINBOT_PYENV:-}"
  path="$(path_without_dir "$path" "$DIR")"
  [[ -n "$ve" ]] && path="$(path_without_dir "$path" "${ve}/bin")"
  [[ -n "$kp" ]] && path="$(path_without_dir "$path" "${kp}/bin")"
  printf '%s' "$path"
}

load_routes

# ---------------------------------------------------------------------------
# Find script argv index (0-based in "$@"). Prints index or empty.
# ---------------------------------------------------------------------------
find_script_index() {
  local -a args=("$@")
  local i=0
  local n=${#args[@]}
  local a
  while (( i < n )); do
    a="${args[i]}"
    case "$a" in
      -c|-m|-W|-X|-Q|--check-hash-based-pycs)
        i=$((i + 2))
        continue
        ;;
      -*)
        if [[ "$a" == -c* && "$a" != "-c" ]]; then i=$((i + 1)); continue; fi
        if [[ "$a" == -m* && "$a" != "-m" ]]; then i=$((i + 1)); continue; fi
        if [[ "$a" == -W* && "$a" != "-W" ]]; then i=$((i + 1)); continue; fi
        if [[ "$a" == -X* && "$a" != "-X" ]]; then i=$((i + 1)); continue; fi
        i=$((i + 1))
        continue
        ;;
      --)
        i=$((i + 1))
        if (( i < n )); then
          printf '%s\n' "$i"
        fi
        return 0
        ;;
      *)
        printf '%s\n' "$i"
        return 0
        ;;
    esac
  done
  return 0
}

# Resolve script token → absolute path.
# Order:
#   1) absolute path
#   2) existing file relative to cwd
#   3) bare name (no slash) → PATH lookup (command -v)
#   4) otherwise cwd join (even if missing)
resolve_script_path() {
  local raw="$1"
  local abs found

  if [[ "$raw" == /* ]]; then
    abs="$raw"
  elif [[ -f "${CWD}/${raw}" ]]; then
    abs="${CWD}/${raw}"
  elif [[ "$raw" != */* ]]; then
    # Bare command name: look up on PATH (skip this wrapper dir's own name collisions for python*)
    found="$(command -v -- "$raw" 2>/dev/null || true)"
    if [[ -n "$found" && -f "$found" ]]; then
      abs="$found"
    else
      abs="${CWD}/${raw}"
    fi
  else
    abs="${CWD}/${raw}"
  fi

  if command -v realpath >/dev/null 2>&1; then
    realpath -m "$abs" 2>/dev/null || printf '%s\n' "$abs"
  else
    readlink -f "$abs" 2>/dev/null || printf '%s\n' "$abs"
  fi
}

# Copy "$@" into RUN_ARGS, optionally rewriting the script slot to absolute path.
SCRIPT_IDX="$(find_script_index "$@")"
SCRIPT_ARG=""
SCRIPT_RESOLVED=""
ROUTE="venv"
CHOSEN_PYTHON="$VENV_PYTHON"
ROUTE_REASON="default (no system script)"

RUN_ARGS=("$@")
if [[ -n "$SCRIPT_IDX" ]]; then
  SCRIPT_ARG="${RUN_ARGS[$SCRIPT_IDX]}"
  SCRIPT_RESOLVED="$(resolve_script_path "$SCRIPT_ARG")"
  # Always pass absolute script path to the real interpreter
  RUN_ARGS[$SCRIPT_IDX]="$SCRIPT_RESOLVED"

  if is_system_path "$SCRIPT_RESOLVED"; then
    ROUTE="system"
    CHOSEN_PYTHON="$SYSTEM_PYTHON"
    ROUTE_REASON="script under system prefix: ${SCRIPT_RESOLVED}"
  else
    ROUTE="venv"
    CHOSEN_PYTHON="$VENV_PYTHON"
    ROUTE_REASON="script outside system prefixes: ${SCRIPT_RESOLVED}"
  fi
fi

PARENT_CMD=""
PARENT_EXE=""
if [[ -r "/proc/${PPID_VAL}/cmdline" ]]; then
  PARENT_CMD="$(tr '\0' ' ' < "/proc/${PPID_VAL}/cmdline" 2>/dev/null | sed 's/[[:space:]]*$//')"
fi
if [[ -L "/proc/${PPID_VAL}/exe" ]]; then
  PARENT_EXE="$(readlink -f "/proc/${PPID_VAL}/exe" 2>/dev/null || true)"
fi

ANCESTRY=""
cur="$PPID_VAL"
for _ in 1 2 3 4 5 6; do
  [[ -z "$cur" || "$cur" == "0" || "$cur" == "1" ]] && break
  cmd=""
  if [[ -r "/proc/${cur}/cmdline" ]]; then
    cmd="$(tr '\0' ' ' < "/proc/${cur}/cmdline" 2>/dev/null | sed 's/[[:space:]]*$//')"
  fi
  [[ -z "$cmd" && -r "/proc/${cur}/comm" ]] && cmd="$(cat "/proc/${cur}/comm" 2>/dev/null)"
  ANCESTRY+="${cur}:${cmd} <- "
  if [[ -r "/proc/${cur}/stat" ]]; then
    cur="$(awk '{print $4}' "/proc/${cur}/stat" 2>/dev/null || echo "")"
  else
    break
  fi
done

quote_arg() { printf "%q" "$1"; }
ARGV_STR="$CHOSEN_PYTHON"
for a in "${RUN_ARGS[@]+"${RUN_ARGS[@]}"}"; do
  ARGV_STR+=" "
  ARGV_STR+="$(quote_arg "$a")"
done

{
  flock -x 9 2>/dev/null || true
  cat >&9 <<EOF
========== ${TS} ==========
pid:          ${PID}
ppid:         ${PPID_VAL}
parent_exe:   ${PARENT_EXE}
parent_cmd:   ${PARENT_CMD}
ancestry:     ${ANCESTRY}
cwd:          ${CWD}
script_arg:   ${SCRIPT_ARG:-<none>}
script_path:  ${SCRIPT_RESOLVED:-<none>}
route:        ${ROUTE}
route_reason: ${ROUTE_REASON}
chosen:       ${CHOSEN_PYTHON}
argv:         ${ARGV_STR}
argc:         $(( ${#RUN_ARGS[@]} + 1 ))
env.PATH:     ${PATH-}
env.VIRTUAL_ENV: ${VIRTUAL_ENV-}
env.PYTHONPATH:  ${PYTHONPATH-}
env.PWD:      ${PWD-}
status:       RUNNING

EOF
} 9>>"$LOG_FILE"

if [[ "${KYLINBOT_PYTHON_WRAPPER_STDERR:-0}" == "1" ]]; then
  echo "[python3-wrapper] route=${ROUTE} chosen=${CHOSEN_PYTHON} script=${SCRIPT_RESOLVED:-<none>}" >&2
fi

PATH_BEFORE_SYSTEM_PURIFY="${PATH-}"
if [[ "$ROUTE" == "system" ]]; then
  PATH="$(purify_path_for_system)"
  export PATH
  unset VIRTUAL_ENV
  unset PYTHONHOME
  if [[ "${KYLINBOT_PYTHON_WRAPPER_STDERR:-0}" == "1" ]]; then
    echo "[python3-wrapper] system path purified: ${PATH_BEFORE_SYSTEM_PURIFY} -> ${PATH}" >&2
  fi
fi

set +e
"$CHOSEN_PYTHON" "${RUN_ARGS[@]+"${RUN_ARGS[@]}"}"
EC=$?
set -e

END_TS="$(date '+%Y-%m-%d %H:%M:%S.%3N' 2>/dev/null || date '+%Y-%m-%d %H:%M:%S')"
{
  flock -x 9 2>/dev/null || true
  cat >&9 <<EOF
---------- end pid=${PID} @ ${END_TS} exit=${EC} route=${ROUTE} ----------

EOF
} 9>>"$LOG_FILE"

exit "$EC"
