#!/usr/bin/env bash
# 从固定远程源获取管理端文件与数据记录，
# 替换已有 SafeLine 容器中的对应文件并更新数据库。
# 本脚本不会创建备份。
set -Eeuo pipefail

CONTAINER="${CONTAINER:-safeline-mgt}"
PG_CONTAINER="${PG_CONTAINER:-safeline-pg}"
PG_USER="${PG_USER:-safeline-ce}"
PG_DB="${PG_DB:-safeline-ce}"
MGT_PORT="${MGT_PORT:-9443}"
MGT_URL="https://dl.naixi.net/waf/files/mgt"
LICENSE_URL="https://dl.naixi.net/waf/files/db/license.json"
MANAGER_INFO_URL="https://dl.naixi.net/waf/files/db/manager_info.json"
FRONTEND_INDEX_URL="https://dl.naixi.net/waf/files/static/index.html"
FRONTEND_MAIN="index-BvbeqHg2.js"
FRONTEND_MAIN_URL="https://dl.naixi.net/waf/files/static/$FRONTEND_MAIN"
EXPECTED_VERSION="v9.4.1"

MGT_TMP=""
FRONTEND_TMP=""
cleanup() {
  [ -z "$MGT_TMP" ] || rm -f "$MGT_TMP"
  [ -z "$FRONTEND_TMP" ] || rm -rf "$FRONTEND_TMP"
}
trap cleanup EXIT

c_ok()   { printf '\033[1;32m[完成] %s\033[0m\n' "$*"; }
c_warn() { printf '\033[1;33m[提示] %s\033[0m\n' "$*"; }
die()    { printf '\033[1;31m[错误] %s\033[0m\n' "$*" >&2; exit 1; }

read_input() {
  if [ ! -t 0 ] && (exec </dev/tty) 2>/dev/null; then
    read -r "$@" < /dev/tty
  else
    read -r "$@"
  fi
}

usage() {
  cat <<'EOF'
用法：
  bash crack.sh apply     执行替换
  bash crack.sh status    查看当前状态
  bash crack.sh -h        显示本说明

不带参数且在终端中运行时，会进入交互菜单。

可选环境变量（用于覆盖默认值）：
  CONTAINER      管理容器名称，默认 safeline-mgt
  PG_CONTAINER   数据库容器名称，默认 safeline-pg
  PG_USER        数据库用户，默认 safeline-ce
  PG_DB          数据库名称，默认 safeline-ce
  MGT_PORT       管理端口，默认 9443
  MGT_SHA256     管理端文件校验值（可选）
EOF
}

require_root() { [ "$(id -u)" = 0 ] || die "请使用 root 用户运行此脚本"; }
require_docker() { command -v docker >/dev/null 2>&1 || die "未检测到 docker 命令，请先安装并启动 Docker"; }

container_exists() { docker inspect "$1" >/dev/null 2>&1; }
container_running() { [ "$(docker inspect -f '{{.State.Running}}' "$1" 2>/dev/null || true)" = true ]; }

health_ok() {
  command -v curl >/dev/null 2>&1 || return 0
  curl -ksS --max-time 4 "https://127.0.0.1:$MGT_PORT/api/open/health" 2>/dev/null \
    | grep -q '"ok"'
}

wait_health() {
  local i
  for i in $(seq 1 60); do
    if health_ok; then return 0; fi
    sleep 2
  done
  return 1
}

fetch_mgt() {
  local dst="$1"
  rm -f "$dst"
  curl -fsSL --retry 2 --connect-timeout 15 --max-time 120 -o "$dst" "$MGT_URL" \
    || return 1
  [ "$(od -An -t x1 -N4 "$dst" | tr -d ' \n')" = 7f454c46 ] || return 1
  local size hash expected
  size="$(wc -c < "$dst")"
  [ "$size" -gt 1000000 ] || return 1
  hash="$(sha256sum "$dst" | awk '{print $1}')"
  expected="${MGT_SHA256:-}"
  if [ -n "$expected" ] && [ "$hash" != "$expected" ]; then
    return 1
  fi
}

parse_args() {
  if [ "$#" -eq 0 ]; then
    if [ -t 0 ] || (exec </dev/tty) 2>/dev/null; then
      interactive_menu
      exit $?
    fi
    usage
    exit 2
  fi
  ACTION="${1:-}"
  [ -n "$ACTION" ] || { usage; exit 2; }
  case "${2:-}" in
    "") ;;
    -h|--help) usage; exit 0 ;;
    *) die "未知参数：$2，使用 -h 查看说明" ;;
  esac
}

check_containers() {
  container_exists "$CONTAINER" || die "未找到管理容器：$CONTAINER"
  container_exists "$PG_CONTAINER" || die "未找到数据库容器：$PG_CONTAINER"
  container_running "$CONTAINER" || die "管理容器未在运行：$CONTAINER"
  container_running "$PG_CONTAINER" || die "数据库容器未在运行：$PG_CONTAINER"
  local version
  version="$(docker exec "$CONTAINER" cat /app/version 2>/dev/null | tr -d '\r\n')"
  [ "$version" = "$EXPECTED_VERSION" ] \
    || die "版本不匹配：当前仅支持 $EXPECTED_VERSION，实际为 ${version:-未知}"
}

replace_binary() {
  local binary="$1"
  local remote="/tmp/mgt.patched.$$"
  if ! docker cp "$binary" "$CONTAINER:$remote"; then return 1; fi
  if ! docker exec "$CONTAINER" sh -c "chmod 0755 '$remote' && mv -f '$remote' /app/mgt"; then return 1; fi
  c_ok "管理端文件已替换"
}

run_sql_file() {
  local file="$1" remote="/tmp/safeline-db-command.$$.$RANDOM.sql" rc=0
  if ! docker cp "$file" "$PG_CONTAINER:$remote"; then return 1; fi
  docker exec "$PG_CONTAINER" psql -X -v ON_ERROR_STOP=1 --single-transaction \
    -U "$PG_USER" -d "$PG_DB" -f "$remote" >/dev/null || rc=$?
  docker exec "$PG_CONTAINER" rm -f "$remote" >/dev/null 2>&1 || true
  return "$rc"
}

fetch_remote_file() {
  local url="$1" dst="$2"
  rm -f "$dst"
  if curl -fsSL --retry 2 --connect-timeout 15 --max-time 60 -o "$dst" "$url"; then
    # Reject HTML/error pages even when a proxy reports HTTP 200.
    [ "$(sed -n 's/^[[:space:]]*\({\).*/\1/p' "$dst" | head -1)" = "{" ] && return 0
  fi
  return 1
}

run_db_files() {
  command -v base64 >/dev/null 2>&1 || die "未检测到 base64 命令"
  command -v curl >/dev/null 2>&1 || die "未检测到 curl 命令"
  local tmpdir lic_file mgr_file lic_b64 mgr_b64 sql
  tmpdir="$(mktemp -d)"
  lic_file="$tmpdir/license.json"
  mgr_file="$tmpdir/manager_info.json"
  if ! fetch_remote_file "$LICENSE_URL" "$lic_file"; then
    rm -rf "$tmpdir"
    die "无法获取许可数据"
  fi
  if ! fetch_remote_file "$MANAGER_INFO_URL" "$mgr_file"; then
    rm -rf "$tmpdir"
    die "无法获取管理信息"
  fi
  lic_b64="$(tr -d '\r\n' < "$lic_file" | base64 -w0)"
  mgr_b64="$(tr -d '\r\n' < "$mgr_file" | base64 -w0)"
  [ -n "$lic_b64" ] || { rm -rf "$tmpdir"; die "远程许可数据为空"; }
  [ -n "$mgr_b64" ] || { rm -rf "$tmpdir"; die "远程管理信息为空"; }
  sql="$(mktemp)"
  {
    printf "INSERT INTO mgt_options(key,value) VALUES('license', convert_from(decode('%s','base64'),'UTF8'))\n" "$lic_b64"
    printf "  ON CONFLICT (key) DO UPDATE SET value=EXCLUDED.value, updated_at=now();\n"
    printf "INSERT INTO mgt_options(key,value) VALUES('manager_info', convert_from(decode('%s','base64'),'UTF8'))\n" "$mgr_b64"
    printf "  ON CONFLICT (key) DO UPDATE SET value=EXCLUDED.value, updated_at=now();\n"
  } > "$sql"
  local rc=0
  if run_sql_file "$sql"; then rc=0; else rc=$?; fi
  rm -f "$sql"
  rm -rf "$tmpdir"
  return "$rc"
}

frontend_patched() {
  docker exec "$CONTAINER" sh -c \
    "grep -Fq 'const K=!1;' '/app/static/assets/$FRONTEND_MAIN'" 2>/dev/null
}

fetch_frontend() {
  local index_file="$1" main_file="$2"
  curl -fsSL --retry 2 --connect-timeout 15 --max-time 60 \
    -o "$index_file" "$FRONTEND_INDEX_URL" || return 1
  curl -fsSL --retry 2 --connect-timeout 15 --max-time 120 \
    -o "$main_file" "$FRONTEND_MAIN_URL" || return 1
  [ "$(wc -c < "$index_file")" -gt 500 ] || return 1
  [ "$(wc -c < "$main_file")" -gt 1000000 ] || return 1
  grep -Fq "/assets/$FRONTEND_MAIN" "$index_file" || return 1
  grep -Fq 'const K=!1;' "$main_file" || return 1
  grep -Fq '雷池' "$main_file" || return 1
  if grep -Fq '小羊' "$main_file"; then return 1; fi
}

replace_frontend() {
  local index_file="$1" main_file="$2"
  local tag
  docker cp "$index_file" "$CONTAINER:/app/static/index.html" || return 1
  docker cp "$main_file" "$CONTAINER:/app/static/assets/$FRONTEND_MAIN" || return 1
  docker exec "$CONTAINER" chmod 0644 /app/static/index.html \
    "/app/static/assets/$FRONTEND_MAIN" || return 1

  # Match install.sh's full asset versioning to prevent old and new chunks
  # from being mixed by an immutable browser cache.
  tag="$(date +%Y%m%d%H%M%S)"
  docker exec "$CONTAINER" sh -c \
    "sed -i -E 's#\\?v=[0-9]{8}[0-9a-z]*##g' /app/static/assets/*.js /app/static/index.html" || return 1
  docker exec "$CONTAINER" sh -c \
    "sed -i -E 's|\"\\./([A-Za-z0-9_.-]+\\.js)\"|\"./\\1?v=$tag\"|g' /app/static/assets/*.js" || return 1
  docker exec "$CONTAINER" sh -c \
    "sed -i -E 's|\"assets/([A-Za-z0-9_.-]+\\.js)\"|\"assets/\\1?v=$tag\"|g' /app/static/assets/*.js" || return 1
  docker exec "$CONTAINER" sh -c \
    "sed -i -E 's#(src|href)=\"(/assets/[A-Za-z0-9_.-]+\\.js)\"#\\1=\"\\2?v=$tag\"#g' /app/static/index.html" || return 1
  frontend_patched || return 1
  c_ok "前端文件已替换"
}

apply_changes() {
  require_root; require_docker; check_containers
  MGT_TMP="$(mktemp)"
  if ! fetch_mgt "$MGT_TMP"; then
    die "无法获取管理端文件"
  fi
  FRONTEND_TMP="$(mktemp -d)"
  fetch_frontend "$FRONTEND_TMP/index.html" "$FRONTEND_TMP/$FRONTEND_MAIN" \
    || die "无法获取或校验前端文件"
  replace_binary "$MGT_TMP" || die "管理端文件替换失败"
  run_db_files || die "数据库更新失败；管理端文件已替换，请检查后重试"
  c_ok "数据库已更新"
  replace_frontend "$FRONTEND_TMP/index.html" "$FRONTEND_TMP/$FRONTEND_MAIN" \
    || die "前端文件替换失败"
  docker restart "$CONTAINER" >/dev/null
  wait_health || c_warn "健康检查超时，可执行：docker logs --tail 200 $CONTAINER"
  c_ok "全部操作已完成"
}

status_value() {
  case "$1" in
    ok)   printf '\033[1;32m%s\033[0m' "$2" ;;
    warn) printf '\033[1;33m%s\033[0m' "$2" ;;
    err)  printf '\033[1;31m%s\033[0m' "$2" ;;
    *)    printf '%s' "$2" ;;
  esac
}

container_state_text() {
  if ! container_exists "$1"; then
    status_value err "未找到"
  elif container_running "$1"; then
    status_value ok "运行中"
  else
    status_value warn "已停止"
  fi
}

show_status() {
  require_docker
  printf '\n---------- 当前状态 ----------\n'
  printf '管理容器    : %s（%s）\n' "$CONTAINER" "$(container_state_text "$CONTAINER")"
  printf '数据库容器  : %s（%s）\n' "$PG_CONTAINER" "$(container_state_text "$PG_CONTAINER")"
  printf '前端替换    : %s\n' "$(frontend_patched && status_value ok 已应用 || status_value warn 未应用)"
  printf '健康检查    : %s\n' "$(health_ok && status_value ok 正常 || status_value warn 不可用)"
}

interactive_menu() {
  local choice
  while true; do
    printf '\n================ 雷池管理端替换工具 ================\n'
    printf '  1) 执行替换\n'
    printf '  2) 查看当前状态\n'
    printf '  0) 退出\n'
    printf '请输入选项 [0-2]：'
    if ! read_input choice; then printf '\n'; return 0; fi
    case "$choice" in
      1) apply_changes ;;
      2) show_status ;;
      0) return 0 ;;
      *) c_warn "无效选项，请输入 0、1 或 2" ;;
    esac
  done
}

parse_args "$@"
case "$ACTION" in
  apply)    apply_changes ;;
  status)   show_status ;;
  *)        usage; exit 2 ;;
esac
