#!/usr/bin/env bash
#
# WolfPanel Agent uninstaller (v1 development).
#
# Usage:
#   sudo ./uninstall.sh                 # interactive scope prompt
#   sudo ./uninstall.sh --scope 1|2|3   # non-interactive
#
# Scopes:
#   1) Remove agent only          (service + /opt/wolfpanel agent tree)
#   2) + local runtime data       (also /var/lib/wolfpanel)
#   3) + agent config/secrets     (also /etc/wolfpanel agent.conf, agent token, logs)
#   4) + user data and service identities: the Roundcube webmail tree
#      (/opt/wolfpanel/webmail), the Stalwart admin credential and the Git
#      deploy keys. Needs --confirm-purge (or typing "purge" interactively).
#
# Scopes 1-3 never delete user data or a service identity (kararlar K-P21,
# preview QA P82/P83): what stays is listed at the end. The agent's own
# certbot deploy hook is removed in every scope (it would call the removed
# binary); an empty backup directory is removed, a non-empty one is kept.
#
# Safe by design: every step tolerates already-missing files.
#
set -uo pipefail

# --- Settings ---------------------------------------------------------------
# Mirrors install.sh's overrides exactly (same env var names, same production
# defaults), so an install that was sandboxed via these vars is uninstalled
# from the SAME paths it was installed to -- never the production defaults.
# This is security-critical: these were previously hardcoded independently of
# install.sh, so a sandboxed/test install still had the real production
# directories wiped on uninstall.
OPT_DIR="${WOLFPANEL_OPT_DIR:-/opt/wolfpanel}"
ETC_DIR="${WOLFPANEL_ETC_DIR:-/etc/wolfpanel}"
VAR_DIR="${WOLFPANEL_VAR_DIR:-/var/lib/wolfpanel}"
LOG_DIR="${WOLFPANEL_LOG_DIR:-/var/log/wolfpanel}"
UNIT="${WOLFPANEL_SYSTEMD_UNIT:-/etc/systemd/system/wolfpanel-agent.service}"
# Files the agent made outside its own directories. Defaults mirror the agent
# (src/actions/mail.py DEPLOY_HOOK_PATH, src/actions/backup.py BACKUP_BASE_DIR,
# tests/test_uninstall_keeps_user_data.py checks they match).
MAIL_DEPLOY_HOOK="${WOLFPANEL_MAIL_DEPLOY_HOOK:-/etc/letsencrypt/renewal-hooks/deploy/wolfpanel-mail.sh}"
BACKUP_BASE_DIR="${WOLFPANEL_BACKUP_BASE_DIR:-/var/backups/wolfpanel}"
# Kept in scopes 1-3 (K-P21): user data and service identities.
WEBMAIL_DIR="${OPT_DIR}/webmail"
MAIL_ADMIN_SECRET="secrets/stalwart-admin.json"
DEPLOY_KEYS_DIR="deploy-keys"

# True as soon as ANY of the four base-directory env overrides is set, i.e.
# this run targets a non-default (sandboxed/test) install. Used to keep the
# real host's systemd service untouched during a sandboxed uninstall, the
# same way the directory removals below stay confined to the override paths.
SANDBOXED=false
if [[ -n "${WOLFPANEL_OPT_DIR:-}${WOLFPANEL_ETC_DIR:-}${WOLFPANEL_VAR_DIR:-}${WOLFPANEL_LOG_DIR:-}" ]]; then
  SANDBOXED=true
fi

# Absolute paths that must never be an rm -rf target, no matter what
# OPT_DIR/ETC_DIR/VAR_DIR/LOG_DIR resolve to. Guards an empty/unset override,
# a typo, or a resolved path landing on a shared system directory.
FORBIDDEN_REMOVE_TARGETS=(
  "/" "/root" "/home" "${HOME:-/root}" "/opt" "/etc" "/var" "/var/lib" "/var/log"
  "/usr" "/bin" "/sbin" "/lib" "/lib64" "/boot" "/dev" "/proc" "/sys" "/srv"
  "/mnt" "/media" "/tmp"
)

SCOPE=""
CONFIRM_PURGE=false
KEPT=()

log() { echo "[wolfpanel] $*"; }
warn() { echo "[wolfpanel][warning] $*" >&2; }
die() { echo "[wolfpanel][fatal] $*" >&2; exit 1; }

warn_identity_left() {
  warn "$1"
  warn "The agent token and server identity remain on disk under ${ETC_DIR}/secrets."
  warn "This server can still be identified/re-used by the panel. Use '--scope 3' to remove everything."
}

parse_args() {
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --scope) SCOPE="${2:-}"; shift 2 ;;
      --scope=*) SCOPE="${1#*=}"; shift ;;
      --confirm-purge) CONFIRM_PURGE=true; shift ;;
      *) echo "unknown argument: $1" >&2; exit 1 ;;
    esac
  done
}

require_root() {
  [[ "$(id -u)" -eq 0 ]] || { echo "must run as root (use sudo)" >&2; exit 1; }
}

prompt_scope() {
  [[ -n "${SCOPE}" ]] && return
  cat <<EOF
Select uninstall scope:
  1) Remove agent only
  2) Remove agent + local runtime data
  3) Remove agent config, secrets and logs too (keeps webmail, the mail admin credential and deploy keys)
  4) Remove everything including user data: webmail, mail admin credential, deploy keys
EOF
  read -r -p "Scope [1-4]: " SCOPE
  if [[ "${SCOPE}" == "4" && "${CONFIRM_PURGE}" != "true" ]]; then
    local typed
    read -r -p "Type 'purge' to confirm deleting user data: " typed
    [[ "${typed}" == "purge" ]] && CONFIRM_PURGE=true
  fi
}

stop_service() {
  if [[ "${SANDBOXED}" == "true" ]]; then
    log "sandbox override active; skipping real systemd service management"
    return
  fi
  command -v systemctl >/dev/null 2>&1 || { log "systemd not present; skipping"; return; }
  log "stopping and disabling service"
  systemctl stop wolfpanel-agent.service 2>/dev/null || true
  systemctl disable wolfpanel-agent.service 2>/dev/null || true
  if [[ -f "${UNIT}" ]]; then
    rm -f -- "${UNIT}"
    systemctl daemon-reload 2>/dev/null || true
  fi
}

# Refuses to remove anything that is empty, relative, or resolves (after
# following symlinks / ".." segments via realpath) to one of the protected
# roots above, or to any top-level directory (depth < 2) not covered by that
# explicit list. Aborts the whole uninstall on a violation rather than
# skipping it, so a caller relying on this to clean a real path finds out
# immediately if its target was rejected.
safe_remove_path() {
  local raw="$1" label="$2" resolved forbidden depth=0 seg

  [[ -n "${raw}" ]] || die "refusing to remove ${label}: path is empty"
  [[ "${raw}" == /* ]] || die "refusing to remove ${label}: path '${raw}' is not absolute"

  if command -v realpath >/dev/null 2>&1; then
    resolved="$(realpath -m -- "${raw}" 2>/dev/null || true)"
    [[ -n "${resolved}" ]] || die "refusing to remove ${label}: could not resolve path '${raw}'"
  else
    resolved="${raw}"
  fi

  for forbidden in "${FORBIDDEN_REMOVE_TARGETS[@]}"; do
    if [[ "${resolved}" == "${forbidden}" ]]; then
      die "refusing to remove ${label}: resolved to protected path '${resolved}' (from '${raw}')"
    fi
  done

  IFS='/' read -ra _segs <<< "${resolved}"
  for seg in "${_segs[@]}"; do [[ -n "${seg}" ]] && depth=$((depth + 1)); done
  if (( depth < 2 )); then
    die "refusing to remove ${label}: resolved path '${resolved}' is a top-level directory"
  fi

  if [[ -e "${resolved}" ]]; then
    log "removing ${label}: ${resolved}"
    rm -rf -- "${resolved}"
  fi
}

# Remove the panel host-verification wiring the agent added to nginx (only
# lines/files marked "# managed-by: wolfpanel-verify"; nginx -t + reload,
# rolled back on failure). Runs while the agent code is still installed.
cleanup_verify() {
  if [[ "${SANDBOXED}" == "true" ]]; then
    log "sandbox override active; skipping verify cleanup"
    return
  fi
  if [[ -x "${OPT_DIR}/current/wolfpanel-agent" ]]; then
    log "removing panel host-verification lines from nginx vhosts"
    "${OPT_DIR}/current/wolfpanel-agent" verify-cleanup || log "verify cleanup reported a problem (see output above)"
  fi
}

# Remove every entry of a directory except the names given (kept entries are
# recorded for the summary). The directory itself goes when nothing is kept.
remove_dir_except() {
  local dir="$1" label="$2"; shift 2
  local keep=("$@") entry name kept_any=false
  [[ -d "${dir}" ]] || return 0
  for entry in "${dir}"/* "${dir}"/.[!.]*; do
    [[ -e "${entry}" || -L "${entry}" ]] || continue
    name="$(basename -- "${entry}")"
    if printf '%s\n' "${keep[@]}" | grep -qx -- "${name}"; then
      kept_any=true
      continue
    fi
    if [[ -L "${entry}" ]]; then
      # the link itself, never what it points at (safe_remove_path resolves links)
      log "removing ${label}: ${name} (symlink)"
      rm -f -- "${entry}"
      continue
    fi
    safe_remove_path "${entry}" "${label}: ${name}"
  done
  if [[ "${kept_any}" == "false" ]]; then
    safe_remove_path "${dir}" "${label}"
  fi
}

# The agent tree under OPT_DIR; the Roundcube webmail next to it is user data
# (K-P21) and stays, together with its nginx vhost and php-fpm pool.
remove_agent_tree() {
  if [[ -d "${WEBMAIL_DIR}" ]]; then
    remove_dir_except "${OPT_DIR}" "agent" "webmail"
    KEPT+=("webmail (Roundcube, its database and configuration): ${WEBMAIL_DIR}")
  else
    safe_remove_path "${OPT_DIR}" "agent"
  fi
}

# Agent config, token and server identity go; the Stalwart admin credential
# and the Git deploy keys are service identities / user data and stay (K-P21).
remove_agent_config() {
  local keep=()
  if [[ -f "${ETC_DIR}/${MAIL_ADMIN_SECRET}" ]]; then
    remove_dir_except "${ETC_DIR}/secrets" "secrets" "$(basename -- "${MAIL_ADMIN_SECRET}")"
    KEPT+=("Stalwart admin credential (panel mail management): ${ETC_DIR}/${MAIL_ADMIN_SECRET}")
    keep+=("secrets")
  fi
  if [[ -d "${ETC_DIR}/${DEPLOY_KEYS_DIR}" ]]; then
    KEPT+=("Git deploy keys: ${ETC_DIR}/${DEPLOY_KEYS_DIR}")
    keep+=("${DEPLOY_KEYS_DIR}")
  fi
  if (( ${#keep[@]} )); then
    remove_dir_except "${ETC_DIR}" "config/secrets" "${keep[@]}"
  else
    safe_remove_path "${ETC_DIR}" "config/secrets"
  fi
}

# The certbot deploy hook the mail provisioning wrote calls the agent CLI
# (mail-reload-tls); without the agent it can only fail. Only the agent's own
# file (its marker line) is removed, never a hook someone else wrote.
remove_deploy_hook() {
  if [[ "${SANDBOXED}" == "true" && -z "${WOLFPANEL_MAIL_DEPLOY_HOOK:-}" ]]; then
    return
  fi
  [[ -f "${MAIL_DEPLOY_HOOK}" ]] || return 0
  if grep -q "Managed by WolfPanel agent" "${MAIL_DEPLOY_HOOK}"; then
    log "removing the agent's certbot renewal hook: ${MAIL_DEPLOY_HOOK}"
    rm -f -- "${MAIL_DEPLOY_HOOK}"
    warn "Stalwart's certificate copies will no longer refresh after certbot renewals until the agent is reinstalled."
  fi
}

# The site backup directory holds user data; only an empty one is removed.
remove_empty_backup_dir() {
  if [[ "${SANDBOXED}" == "true" && -z "${WOLFPANEL_BACKUP_BASE_DIR:-}" ]]; then
    return
  fi
  if [[ -d "${BACKUP_BASE_DIR}" ]]; then
    if rmdir -- "${BACKUP_BASE_DIR}" 2>/dev/null; then
      log "removed empty backup directory ${BACKUP_BASE_DIR}"
    else
      KEPT+=("site backups: ${BACKUP_BASE_DIR}")
    fi
  fi
}

print_kept() {
  if (( ${#KEPT[@]} )); then
    log "kept on this server (user data and service identities, K-P21):"
    local item
    for item in "${KEPT[@]}"; do log "  - ${item}"; done
    log "config backup copies the agent made (*.wolfpanel.bak.*) are kept next to the files they back up."
  fi
}

main() {
  parse_args "$@"
  require_root
  prompt_scope

  case "${SCOPE}" in
    1|2|3) ;;
    4)
      if [[ "${CONFIRM_PURGE}" != "true" ]]; then
        die "scope 4 deletes user data (webmail, mail admin credential, deploy keys); re-run with --confirm-purge"
      fi
      ;;
    *) echo "invalid scope: ${SCOPE}" >&2; exit 1 ;;
  esac

  stop_service
  cleanup_verify
  remove_deploy_hook

  case "${SCOPE}" in
    1)
      remove_agent_tree
      warn_identity_left "agent binaries removed; runtime state (${VAR_DIR}) and config/secrets (${ETC_DIR}) kept"
      ;;
    2)
      remove_agent_tree
      safe_remove_path "${VAR_DIR}" "runtime state"
      warn_identity_left "runtime state removed, but config/secrets in ${ETC_DIR} (incl. agent token + server id) are kept"
      ;;
    3)
      remove_agent_tree
      safe_remove_path "${VAR_DIR}" "runtime state"
      remove_agent_config
      safe_remove_path "${LOG_DIR}" "logs"
      log "scope 3: removed agent config, secrets (agent token), runtime state and logs"
      ;;
    4)
      safe_remove_path "${OPT_DIR}" "agent and webmail"
      safe_remove_path "${VAR_DIR}" "runtime state"
      safe_remove_path "${ETC_DIR}" "config, secrets, mail admin credential and deploy keys"
      safe_remove_path "${LOG_DIR}" "logs"
      log "scope 4: removed everything including user data"
      ;;
  esac
  remove_empty_backup_dir
  print_kept

  log "uninstall complete (scope ${SCOPE})"
}

# Run main only when executed directly; sourcing exposes functions for testing
# (mirrors install.sh's same guard).
if [[ "${BASH_SOURCE[0]:-$0}" == "${0}" ]]; then
  main "$@"
fi
