#!/usr/bin/env bash
# UTF-8 无 BOM、LF 换行。与 docker-compose-all.yml、docker-compose.yml、upgrade.sh 一同发布。
set -Eeuo pipefail
set +x
umask 077

release_url="${HAS_WAF_RELEASE_URL:-https://has-waf.haser.top}"
install_dir="${HAS_WAF_INSTALL_DIR:-/opt/has-waf}"
registry="${HAS_WAF_REGISTRY:-registry.haser.top}"
middleware_registry="${HAS_WAF_MIDDLEWARE_REGISTRY:-docker.1ms.run}"
image_tag="${HAS_WAF_TAG:-${WAF_VERSION:-latest}}"
machine_arch="${HAS_WAF_ARCH:-$(uname -m)}"
case "$machine_arch" in
  x86_64|amd64) image_repository=has-waf;;
  aarch64|arm64) image_repository=has-waf-arm;;
  *) printf '不支持的架构：%s（仅支持 amd64 / arm64）\n' "$machine_arch" >&2; exit 1;;
esac
requested_registry=${HAS_WAF_REGISTRY:+yes}
requested_tag=${HAS_WAF_TAG:+yes}
if [[ -n "${WAF_VERSION:-}" ]]; then requested_tag=yes; fi
requested_middleware=${HAS_WAF_MIDDLEWARE_REGISTRY:+yes}
requested_database=${HAS_WAF_DATABASE:+yes}
database_backend=${HAS_WAF_DATABASE:-sqlite}
deployment=full
requested_deployment=false
ui_port=${HAS_WAF_UI_PORT:-8000}
http_port=${HAS_WAF_HTTP_PORT:-8080}
https_port=${HAS_WAF_HTTPS_PORT:-8443}
site_port_start=${HAS_WAF_SITE_PORT_START:-5010}
site_port_end=${HAS_WAF_SITE_PORT_END:-5015}
requested_ports=false
ui_port_set=false
http_port_set=false
https_port_set=false
site_ports_set=false
[[ -n "${HAS_WAF_UI_PORT:-}" ]] && { ui_port_set=true; requested_ports=true; }
[[ -n "${HAS_WAF_HTTP_PORT:-}" ]] && { http_port_set=true; requested_ports=true; }
[[ -n "${HAS_WAF_HTTPS_PORT:-}" ]] && { https_port_set=true; requested_ports=true; }
if [[ -n "${HAS_WAF_SITE_PORT_START:-}${HAS_WAF_SITE_PORT_END:-}" ]]; then
  site_ports_set=true; requested_ports=true
fi
redis_host=${REDIS_HOST:-host.docker.internal}
redis_port=${REDIS_PORT:-6379}
redis_db=${REDIS_DB:-0}
redis_password=${REDIS_PASSWORD:-}
clickhouse_host=${CLICKHOUSE_HOST:-host.docker.internal}
clickhouse_port=${CLICKHOUSE_PORT:-8123}
clickhouse_user=${CLICKHOUSE_USER:-default}
clickhouse_password=${CLICKHOUSE_PASSWORD:-}
clickhouse_database=${CLICKHOUSE_DATABASE:-has_waf}
log_storage_backend=${LOG_STORAGE_BACKEND:-clickhouse}
standalone_database_url=${DATABASE_URL:-}
if [[ "$standalone_database_url" == mysql* ]]; then database_backend=mysql; fi
compose_name=docker-compose-all.yml
action=install
explicit_action=false
prepare_only=false
non_interactive=false
input_fd=0

fail() { printf 'HAS-WAF: %s\n' "$*" >&2; exit 1; }
usage() {
  cat <<'HELP'
HAS-WAF 安装与更新
用法：bash scripts/install.sh [选项]
  --dir 路径                    安装目录，默认 /opt/has-waf
  --registry 域名               registry.haser.top 或 registry.hasbin.top
  --middleware-registry 域名    中间件源，支持自定义域名（仅全量部署）
  --tag / --version 标签        WAF 镜像版本，默认 latest；也兼容 WAF_VERSION
  --database sqlite|mysql       主数据库，默认 SQLite；全量部署时 MySQL 自动拉起
  --standalone                  单容器部署：只运行 WAF，连接已有 Redis / ClickHouse / MySQL
  --ui-port 端口                控制台宿主机访问端口，默认 8000
  --http-port 端口              WAF HTTP 宿主机访问端口，默认 8080
  --https-port 端口             WAF HTTPS 宿主机访问端口，默认 8443
  --site-ports 起始-结束        站点端口范围，默认 5010-5015
  --non-interactive             跳过菜单，使用参数、环境变量或已有配置
  --prepare-only                仅生成配置，不拉取和启动容器
  --update                      更新已有部署，默认拉取 WAF latest，保留数据库和凭据
  --status / --stop             查看状态或停止服务，保留数据
  --help                        显示帮助

中文编号菜单，回车采用默认项；无终端时自动采用默认配置。
Docker 不存在时自动安装（需要 root 和 curl），无需手工编辑 .env。
单容器部署会询问外部 Redis / ClickHouse / MySQL 连接信息，也可用同名环境变量。
私有源使用 docker login，密码隐藏输入，不写入项目配置。
无交互登录可预先 docker login，或设置 HAS_WAF_REGISTRY_USERNAME、
HAS_WAF_REGISTRY_PASSWORD；自定义中间件源使用 MIDDLEWARE_REGISTRY_USERNAME、
MIDDLEWARE_REGISTRY_PASSWORD。REDIS_IMAGE、CLICKHOUSE_IMAGE、MYSQL_IMAGE 可覆盖完整镜像地址。
容器内部监听端口固定，只有宿主机映射端口可修改；站点端口需与控制台中的监听端口一致。
HELP
}
while (($#)); do
  case "$1" in
    --dir|--registry|--middleware-registry|--tag|--version|--database|--ui-port|--http-port|--https-port|--site-ports)
      (($# >= 2)) || fail "选项 $1 缺少参数"
      case "$1" in
        --dir) install_dir=$2;; --registry) registry=$2; requested_registry=yes;;
        --middleware-registry) middleware_registry=$2; requested_middleware=yes;;
        --tag|--version) image_tag=$2; requested_tag=yes;;
        --database) database_backend=$2; requested_database=yes;;
        --ui-port) ui_port=$2; ui_port_set=true; requested_ports=true;;
        --http-port) http_port=$2; http_port_set=true; requested_ports=true;;
        --https-port) https_port=$2; https_port_set=true; requested_ports=true;;
        --site-ports)
          site_port_start=${2%%-*}; site_port_end=${2#*-}; site_ports_set=true; requested_ports=true;;
      esac
      shift 2;;
    --standalone) deployment=standalone; requested_deployment=true; shift;;
    --prepare-only) prepare_only=true; shift;;
    --non-interactive) non_interactive=true; shift;;
    --update) action=update; explicit_action=true; shift;;
    --status) action=status; explicit_action=true; shift;;
    --stop) action=stop; explicit_action=true; shift;;
    --help|-h) usage; exit 0;;
    *) fail "未知选项：$1";;
  esac
done
# bash -c "$(curl ...)" can read from the controlling terminal as well.
if [[ ( "$action" == install || "$action" == update ) && "$non_interactive" == false ]]; then
  if [[ -t 0 ]]; then input_fd=0
  elif { exec 8</dev/tty; } 2>/dev/null; then input_fd=8
  else non_interactive=true
  fi
fi
ask() {
  printf '%s' "$1"
  IFS= read -r -u "$input_fd" answer || fail "无法读取输入；自动化部署请添加 --non-interactive"
}
ask_secret() {
  printf '%s' "$1"
  IFS= read -rs -u "$input_fd" answer || fail "无法读取输入；自动化部署请添加 --non-interactive"
  printf '\n'
}
# 已有部署只覆盖显式传入的端口，其余沿用现有映射，避免误改线上入口。
if [[ -f "$install_dir/.env" ]]; then
  existing_ui_port=$(sed -n 's/^HAS_WAF_UI_PORT=//p' "$install_dir/.env")
  existing_http_port=$(sed -n 's/^HAS_WAF_HTTP_PORT=//p' "$install_dir/.env")
  existing_https_port=$(sed -n 's/^HAS_WAF_HTTPS_PORT=//p' "$install_dir/.env")
  existing_site_start=$(sed -n 's/^HAS_WAF_SITE_PORT_START=//p' "$install_dir/.env")
  existing_site_end=$(sed -n 's/^HAS_WAF_SITE_PORT_END=//p' "$install_dir/.env")
  [[ "$ui_port_set" == true || -z "$existing_ui_port" ]] || ui_port=$existing_ui_port
  [[ "$http_port_set" == true || -z "$existing_http_port" ]] || http_port=$existing_http_port
  [[ "$https_port_set" == true || -z "$existing_https_port" ]] || https_port=$existing_https_port
  if [[ "$site_ports_set" == false && -n "$existing_site_start" && -n "$existing_site_end" ]]; then
    site_port_start=$existing_site_start
    site_port_end=$existing_site_end
  fi
fi
existing_deployment=''
if [[ -f "$install_dir/.env" ]]; then
  existing_deployment=$(sed -n 's/^HAS_WAF_DEPLOYMENT=//p' "$install_dir/.env")
  existing_deployment=${existing_deployment:-full}
  if [[ "$requested_deployment" == false ]]; then deployment=$existing_deployment; fi
  if [[ "$requested_deployment" == true && "$deployment" != "$existing_deployment" ]]; then
    fail "已有部署使用 $existing_deployment 方式；切换部署方式需要数据迁移，请使用新的 --dir"
  fi
fi
if [[ "$non_interactive" == false && "$explicit_action" == false ]]; then
  default_action=1
  if [[ -f "$install_dir/.env" ]]; then default_action=2; fi
  printf '\n  HAS-WAF\n  ================================\n  1) 安装 / 重新部署\n  2) 更新已有部署\n  3) 查看运行状态\n  4) 停止服务（保留数据）\n'
  while true; do
    ask "请选择操作 [$default_action]："
    case "${answer:-$default_action}" in
      1) action=install; break;; 2) action=update; break;;
      3) action=status; break;; 4) action=stop; break;;
      *) printf '请输入 1、2、3 或 4。\n';;
    esac
  done
fi
if [[ "$action" == update ]]; then
  [[ -f "$install_dir/.env" && ( -f "$install_dir/docker-compose-all.yml" || -f "$install_dir/docker-compose.yml" ) ]] || fail "目录 $install_dir 中没有找到已有部署，请先安装"
  if [[ -z "$requested_tag" && "$non_interactive" == false ]]; then
    ask '请输入更新版本 [latest]：'; image_tag=${answer:-latest}
  fi
  requested_tag=yes
  printf '更新将保留现有数据库、镜像源、密码和 data 目录；仅拉取 WAF 镜像。\n'
fi
if [[ "$action" == install && "$non_interactive" == false ]]; then
  if [[ -f "$install_dir/.env" ]]; then
    existing_database=$(sed -n 's/^HAS_WAF_DATABASE=//p' "$install_dir/.env")
    database_backend=${existing_database:-sqlite}
  fi
  if [[ "$requested_deployment" == false && -z "$existing_deployment" ]]; then
    printf '\n请选择部署方式：\n  1) 全量部署（WAF + Redis + ClickHouse，可选 MySQL，默认）\n  2) 单容器部署（仅 WAF 容器，连接已有 Redis / ClickHouse / MySQL）\n'
    while true; do
      ask '请输入序号 [1]：'
      case "${answer:-1}" in
        1) deployment=full; break;; 2) deployment=standalone; break;;
        *) printf '请输入 1 或 2。\n';;
      esac
    done
  fi
  if [[ -z "$requested_registry" ]]; then
    printf '\n请选择 WAF 镜像源：\n  1) registry.haser.top（默认）\n  2) registry.hasbin.top（全球加速）\n'
    while true; do
      ask '请输入序号 [1]：'
      case "${answer:-1}" in
        1) registry=registry.haser.top; break;;
        2) registry=registry.hasbin.top; break;;
        *) printf '请输入 1 或 2。\n';;
      esac
    done
    requested_registry=yes
  fi
  if [[ "$deployment" == full ]]; then
    if [[ -z "$requested_middleware" ]]; then
      printf '\n请选择 Redis / ClickHouse 镜像源：\n  1) docker.1ms.run（默认，公开源）\n  2) 自定义镜像源\n  3) 使用所选 WAF 私有源\n'
      while true; do
        ask '请输入序号 [1]：'
        case "${answer:-1}" in
          1) middleware_registry=docker.1ms.run; break;;
          2) ask '请输入镜像源域名（例如 registry.example.com）：'; middleware_registry=$answer
             [[ -n "$middleware_registry" ]] || { printf '域名不能为空。\n'; continue; }; break;;
          3) middleware_registry=$registry; break;;
          *) printf '请输入 1、2 或 3。\n';;
        esac
      done
      requested_middleware=yes
    fi
    if [[ -z "$requested_database" && -z "$existing_deployment" ]]; then
      database_default=1
      if [[ "$database_backend" == mysql ]]; then database_default=2; fi
      printf '\n请选择主数据库：\n  1) SQLite（轻量，无额外数据库容器）\n  2) MySQL（自动拉起数据库服务）\n'
      while true; do
        ask "请输入序号 [$database_default]："
        case "${answer:-$database_default}" in
          1) database_backend=sqlite; break;; 2) database_backend=mysql; break;;
          *) printf '请输入 1 或 2。\n';;
        esac
      done
      requested_database=yes
    fi
  elif [[ -z "$existing_deployment" ]]; then
    if [[ -z "$requested_database" ]]; then
      database_default=1
      if [[ "$database_backend" == mysql ]]; then database_default=2; fi
      printf '\n请选择主数据库：\n  1) SQLite（容器内，默认）\n  2) 外部 MySQL（填写连接串）\n'
      while true; do
        ask "请输入序号 [$database_default]："
        case "${answer:-$database_default}" in
          1) database_backend=sqlite; break;; 2) database_backend=mysql; break;;
          *) printf '请输入 1 或 2。\n';;
        esac
      done
      requested_database=yes
    fi
    if [[ "$database_backend" == mysql && -z "$standalone_database_url" ]]; then
      ask_secret 'MySQL 连接串（mysql+pymysql://用户:密码@主机:3306/库名，输入不回显）：'
      standalone_database_url=$answer
    fi
    if [[ -z "${LOG_STORAGE_BACKEND:-}" ]]; then
      log_default=1
      if [[ "$log_storage_backend" == sqlite ]]; then log_default=2; fi
      printf '\n请选择访问日志存储：\n  1) ClickHouse（连接已有服务，默认）\n  2) SQLite（容器内文件，适合小流量）\n'
      while true; do
        ask "请输入序号 [$log_default]："
        case "${answer:-$log_default}" in
          1) log_storage_backend=clickhouse; break;; 2) log_storage_backend=sqlite; break;;
          *) printf '请输入 1 或 2。\n';;
        esac
      done
    fi
    printf '\n请填写外部 Redis 连接信息（回车使用默认值）：\n'
    ask "Redis 地址 [$redis_host]："; redis_host=${answer:-$redis_host}
    ask "Redis 端口 [$redis_port]："; redis_port=${answer:-$redis_port}
    ask "Redis 数据库编号 [$redis_db]："; redis_db=${answer:-$redis_db}
    ask_secret 'Redis 密码（留空沿用环境变量或无密码，输入不回显）：'
    [[ -z "$answer" ]] || redis_password=$answer
    if [[ "$log_storage_backend" == clickhouse ]]; then
      printf '\n请填写外部 ClickHouse 连接信息（回车使用默认值）：\n'
      ask "ClickHouse 地址 [$clickhouse_host]："; clickhouse_host=${answer:-$clickhouse_host}
      ask "ClickHouse 端口 [$clickhouse_port]："; clickhouse_port=${answer:-$clickhouse_port}
      ask "ClickHouse 用户 [$clickhouse_user]："; clickhouse_user=${answer:-$clickhouse_user}
      ask "ClickHouse 数据库 [$clickhouse_database]："; clickhouse_database=${answer:-$clickhouse_database}
      ask_secret 'ClickHouse 密码（留空沿用环境变量或无密码，输入不回显）：'
      [[ -z "$answer" ]] || clickhouse_password=$answer
    fi
  fi
  if [[ "$requested_ports" == false && -z "$existing_deployment" ]]; then
    printf '\n请填写宿主机映射端口（回车使用默认值，容器内部端口不变）：\n'
    ask "控制台访问端口 [$ui_port]："; ui_port=${answer:-$ui_port}
    ask "HTTP 接入端口 [$http_port]："; http_port=${answer:-$http_port}
    ask "HTTPS 接入端口 [$https_port]："; https_port=${answer:-$https_port}
    ask "站点端口范围（起始-结束，例如 5010-5015） [$site_port_start-$site_port_end]："
    if [[ -n "$answer" ]]; then
      [[ "$answer" =~ ^[0-9]+-[0-9]+$ ]] || fail "站点端口范围格式应为 起始-结束"
      site_port_start=${answer%%-*}; site_port_end=${answer#*-}
    fi
  fi
fi
registry="${registry#https://}"; registry="${registry%/}"
middleware_registry="${middleware_registry#https://}"; middleware_registry="${middleware_registry%/}"
case "$registry" in registry.haser.top|registry.hasbin.top) ;; *) fail "WAF 源只能使用 registry.haser.top 或 registry.hasbin.top";; esac
[[ "$middleware_registry" =~ ^[a-zA-Z0-9][a-zA-Z0-9.-]*(:[0-9]{1,5})?$ ]] || fail "中间件源必须为域名或域名:端口，不含路径"
[[ "$image_tag" =~ ^[a-zA-Z0-9_][a-zA-Z0-9_.-]{0,127}$ ]] || fail "镜像标签格式无效"
[[ "$install_dir" == /* && "$install_dir" != / && "$install_dir" != *$'\n'* ]] || fail "安装目录必须是绝对路径，且不能是根目录 /"
[[ "$release_url" == https://* ]] || fail "安装资源地址必须使用 HTTPS"
case "$database_backend" in sqlite|mysql) ;; *) fail "主数据库只能选择 sqlite 或 mysql";; esac
case "$log_storage_backend" in clickhouse|sqlite) ;; *) fail "日志存储只能选择 clickhouse 或 sqlite";; esac
if [[ "$deployment" == standalone && -n "$requested_middleware" ]]; then
  fail "单容器部署连接外部中间件，不使用 --middleware-registry"
fi
if [[ "$deployment" == standalone && "$database_backend" == mysql ]]; then
  [[ "$standalone_database_url" == mysql+pymysql://* && "$standalone_database_url" != *[[:space:]]* ]] || fail "单容器 MySQL 连接串需以 mysql+pymysql:// 开头且不能包含空白字符"
fi
port_valid() { [[ "$1" =~ ^[0-9]{1,5}$ ]] && ((10#$1 >= 1 && 10#$1 <= 65535)); }
for port in "$ui_port" "$http_port" "$https_port" "$redis_port" "$clickhouse_port"; do
  port_valid "$port" || fail "端口无效：$port"
done
port_valid "$site_port_start" && port_valid "$site_port_end" || fail "站点端口范围无效：$site_port_start-$site_port_end"
((10#$site_port_start < 10#$site_port_end)) || fail "站点端口范围起始值必须小于结束值"
[[ "$ui_port" != "$http_port" && "$ui_port" != "$https_port" && "$http_port" != "$https_port" ]] || fail "控制台、HTTP、HTTPS 端口不能重复"
for port in "$ui_port" "$http_port" "$https_port"; do
  ((10#$port < 10#$site_port_start || 10#$port > 10#$site_port_end)) || fail "站点端口范围不能包含控制台、HTTP 或 HTTPS 端口"
done
[[ "$redis_host" =~ ^[a-zA-Z0-9._:-]+$ ]] || fail "Redis 地址格式无效"
[[ "$clickhouse_host" =~ ^[a-zA-Z0-9._:-]+$ ]] || fail "ClickHouse 地址格式无效"
[[ "$clickhouse_user" =~ ^[a-zA-Z0-9._-]+$ ]] || fail "ClickHouse 用户名格式无效"
[[ "$clickhouse_database" =~ ^[a-zA-Z0-9_]+$ ]] || fail "ClickHouse 数据库名格式无效"
[[ "$redis_db" =~ ^[0-9]{1,3}$ ]] || fail "Redis 数据库编号无效"
for image in "${REDIS_IMAGE:-default}" "${CLICKHOUSE_IMAGE:-default}" "${MYSQL_IMAGE:-default}"; do
  [[ "$image" =~ ^[a-zA-Z0-9][a-zA-Z0-9_./:@-]+$ ]] || fail "中间件镜像地址格式无效"
done
if [[ "$deployment" == standalone ]]; then compose_name=docker-compose.yml; fi

if ! command -v docker >/dev/null; then
  [[ "$action" == install && "$prepare_only" == false ]] || fail "需要 Docker Engine 和 Compose v2"
  ((EUID == 0)) || fail "请使用 root 运行，以便自动安装 Docker"
  command -v curl >/dev/null || fail "请先安装 curl"
  docker_setup=$(mktemp)
  trap 'rm -f -- "$docker_setup"' EXIT
  printf '正在通过 Docker 官方安装脚本安装 Docker Engine。\n'
  curl --proto '=https' --tlsv1.2 -fsSL --retry 3 https://get.docker.com -o "$docker_setup"
  sh "$docker_setup"
  rm -f -- "$docker_setup"
  trap - EXIT
  if command -v systemctl >/dev/null; then systemctl enable --now docker; fi
fi
docker compose version >/dev/null 2>&1 || fail "需要 Docker Compose v2，请安装 docker-compose-plugin"
compose() { docker compose --project-name has-waf --project-directory "$install_dir" --env-file "$install_dir/.env" -f "$install_dir/$compose_name" "$@"; }
if [[ "$action" == status || "$action" == stop ]]; then
  [[ -f "$install_dir/$compose_name" && -f "$install_dir/.env" ]] || fail "目录 $install_dir 中没有找到已有部署"
  if [[ "$action" == status ]]; then compose ps; else compose stop; fi
  exit 0
fi

mkdir -p "$install_dir"
command -v flock >/dev/null || fail "需要 flock 命令，请安装 util-linux"
exec 9>"$install_dir/.install.lock"
flock -n 9 || fail "已有另一个安装进程正在运行"
temp_dir=$(mktemp -d "$install_dir/.install.XXXXXX")
trap 'rm -rf -- "$temp_dir"' EXIT

# A local checkout works without network access to the release website.
script_dir=''
if [[ -n "${BASH_SOURCE[0]:-}" && -f "${BASH_SOURCE[0]}" ]]; then
  script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
fi
if [[ -n "$script_dir" && -f "$script_dir/$compose_name" ]]; then
  cp -- "$script_dir/$compose_name" "$temp_dir/$compose_name"
elif [[ -n "$script_dir" && -f "$script_dir/../$compose_name" ]]; then
  cp -- "$script_dir/../$compose_name" "$temp_dir/$compose_name"
else
  command -v curl >/dev/null || fail "需要 curl 命令"
  curl --proto '=https' --tlsv1.2 -fSL --retry 3 --connect-timeout 15 --max-time 120 \
    "${release_url%/}/$compose_name" -o "$temp_dir/$compose_name"
fi

set_env_value() {
  local key=$1 value=$2 file=$3
  if grep -q "^${key}=" "$file"; then
    sed -i "s|^${key}=.*|${key}=${value}|" "$file"
  else
    printf '%s=%s\n' "$key" "$value" >>"$file"
  fi
}
if [[ -f "$install_dir/.env" ]]; then
  existing_database=$(sed -n 's/^HAS_WAF_DATABASE=//p' "$install_dir/.env")
  existing_database=${existing_database:-sqlite}
  if [[ -n "$requested_database" && "$database_backend" != "$existing_database" ]]; then
    fail "已有部署使用 $existing_database；切换数据库需要数据迁移，请使用新的 --dir 创建部署"
  fi
  database_backend=$existing_database
  # Never regenerate credentials on rerun or source changes.
  for key in JWT_SECRET_KEY SETUP_ACCESS_TOKEN; do
    grep -Eq "^${key}=.+" "$install_dir/.env" || fail "已有 .env 缺少 $key；为保护现有凭据，已停止安装"
  done
  if [[ "$deployment" == full ]]; then
    for key in REDIS_PASSWORD CLICKHOUSE_PASSWORD; do
      grep -Eq "^${key}=.+" "$install_dir/.env" || fail "已有 .env 缺少 $key；为保护现有凭据，已停止安装"
    done
  fi
  cp -- "$install_dir/.env" "$install_dir/.env.previous"
  chmod 600 "$install_dir/.env.previous"
  # Only explicit image choices change on rerun; secrets remain byte-for-byte intact.
  cp -- "$install_dir/.env" "$temp_dir/env"
  current_image=$(sed -n 's/^HAS_WAF_IMAGE=//p' "$temp_dir/env")
  if [[ -n "$requested_registry" || -n "$requested_tag" ]]; then
    if [[ -z "$requested_registry" ]]; then registry=${current_image%%/*}; fi
    if [[ -z "$requested_tag" ]]; then image_tag=${current_image##*:}; fi
    sed -i "s|^HAS_WAF_IMAGE=.*|HAS_WAF_IMAGE=$registry/$image_repository:$image_tag|" "$temp_dir/env"
  fi
  selected_image=$(sed -n 's/^HAS_WAF_IMAGE=//p' "$temp_dir/env")
  sed -i '/^HAS_WAF_REGISTRY=/d' "$temp_dir/env"
  printf 'HAS_WAF_REGISTRY=%s\n' "${selected_image%%/*}" >>"$temp_dir/env"
  if [[ "$deployment" == full && -n "$requested_middleware" ]]; then
    sed -i "s|^REDIS_IMAGE=.*|REDIS_IMAGE=$middleware_registry/library/redis:7-alpine|; s|^CLICKHOUSE_IMAGE=.*|CLICKHOUSE_IMAGE=$middleware_registry/clickhouse/clickhouse-server:24.8|" "$temp_dir/env"
    if [[ "$database_backend" == mysql ]]; then
      sed -i "s|^MYSQL_IMAGE=.*|MYSQL_IMAGE=$middleware_registry/library/mysql:8.4|" "$temp_dir/env"
    fi
  fi
  if [[ "$deployment" == full && -n "${REDIS_IMAGE:-}" ]]; then sed -i "s|^REDIS_IMAGE=.*|REDIS_IMAGE=$REDIS_IMAGE|" "$temp_dir/env"; fi
  if [[ "$deployment" == full && -n "${CLICKHOUSE_IMAGE:-}" ]]; then sed -i "s|^CLICKHOUSE_IMAGE=.*|CLICKHOUSE_IMAGE=$CLICKHOUSE_IMAGE|" "$temp_dir/env"; fi
  if [[ "$deployment" == full && -n "${MYSQL_IMAGE:-}" && "$database_backend" == mysql ]]; then sed -i "s|^MYSQL_IMAGE=.*|MYSQL_IMAGE=$MYSQL_IMAGE|" "$temp_dir/env"; fi
  if [[ "$requested_ports" == true ]]; then
    set_env_value HAS_WAF_UI_PORT "$ui_port" "$temp_dir/env"
    set_env_value HAS_WAF_HTTP_PORT "$http_port" "$temp_dir/env"
    set_env_value HAS_WAF_HTTPS_PORT "$https_port" "$temp_dir/env"
    set_env_value HAS_WAF_SITE_PORT_START "$site_port_start" "$temp_dir/env"
    set_env_value HAS_WAF_SITE_PORT_END "$site_port_end" "$temp_dir/env"
  fi
  set_env_value HAS_WAF_DEPLOYMENT "$deployment" "$temp_dir/env"
  mv -- "$temp_dir/env" "$install_dir/.env"
  printf '已保留现有服务密码和密钥。\n'
else
  command -v od >/dev/null || fail "需要 od 命令生成随机密码"
  random_secret() { od -An -N32 -tx1 /dev/urandom | tr -d ' \n'; }
  if [[ "$deployment" == standalone ]]; then
    if [[ "$database_backend" != mysql ]]; then
      standalone_database_url=sqlite:////data/db/waf.db
    fi
    cat >"$temp_dir/env" <<ENV
HAS_WAF_IMAGE=$registry/$image_repository:$image_tag
HAS_WAF_REGISTRY=$registry
HAS_WAF_DEPLOYMENT=standalone
HAS_WAF_DATABASE=$database_backend
DATABASE_URL=$standalone_database_url
LOG_STORAGE_BACKEND=$log_storage_backend
SQLITE_LOG_DATABASE_PATH=/data/db/waf-logs.db
JWT_SECRET_KEY=$(random_secret)
SETUP_ACCESS_TOKEN=$(random_secret)
HAS_WAF_UI_PORT=$ui_port
HAS_WAF_HTTP_PORT=$http_port
HAS_WAF_HTTPS_PORT=$https_port
HAS_WAF_SITE_PORT_START=$site_port_start
HAS_WAF_SITE_PORT_END=$site_port_end
REDIS_HOST=$redis_host
REDIS_PORT=$redis_port
REDIS_DB=$redis_db
REDIS_PASSWORD=$redis_password
TZ=Asia/Shanghai
ENV
    if [[ "$log_storage_backend" == clickhouse ]]; then
      cat >>"$temp_dir/env" <<ENV
CLICKHOUSE_HOST=$clickhouse_host
CLICKHOUSE_PORT=$clickhouse_port
CLICKHOUSE_USER=$clickhouse_user
CLICKHOUSE_PASSWORD=$clickhouse_password
CLICKHOUSE_DATABASE=$clickhouse_database
ENV
    fi
  else
    redis_image="${REDIS_IMAGE:-$middleware_registry/library/redis:7-alpine}"
    clickhouse_image="${CLICKHOUSE_IMAGE:-$middleware_registry/clickhouse/clickhouse-server:24.8}"
    for image in "$redis_image" "$clickhouse_image"; do
      [[ "$image" =~ ^[a-zA-Z0-9][a-zA-Z0-9_./:@-]+$ ]] || fail "中间件镜像地址格式无效"
    done
    cat >"$temp_dir/env" <<ENV
HAS_WAF_IMAGE=$registry/$image_repository:$image_tag
HAS_WAF_REGISTRY=$registry
HAS_WAF_DEPLOYMENT=full
HAS_WAF_DATABASE=$database_backend
REDIS_IMAGE=$redis_image
CLICKHOUSE_IMAGE=$clickhouse_image
JWT_SECRET_KEY=$(random_secret)
SETUP_ACCESS_TOKEN=$(random_secret)
REDIS_PASSWORD=$(random_secret)
CLICKHOUSE_PASSWORD=$(random_secret)
HAS_WAF_UI_PORT=$ui_port
HAS_WAF_HTTP_PORT=$http_port
HAS_WAF_HTTPS_PORT=$https_port
HAS_WAF_SITE_PORT_START=$site_port_start
HAS_WAF_SITE_PORT_END=$site_port_end
ENV
    if [[ "$database_backend" == mysql ]]; then
      mysql_password=$(random_secret)
      cat >>"$temp_dir/env" <<ENV
COMPOSE_PROFILES=mysql
MYSQL_IMAGE=${MYSQL_IMAGE:-$middleware_registry/library/mysql:8.4}
MYSQL_PASSWORD=$mysql_password
MYSQL_ROOT_PASSWORD=$(random_secret)
DATABASE_URL=mysql+pymysql://has_waf:$mysql_password@mysql:3306/has_waf
ENV
      unset mysql_password
    else
      printf 'DATABASE_URL=sqlite:////data/db/waf.db\n' >>"$temp_dir/env"
    fi
  fi
  mv -- "$temp_dir/env" "$install_dir/.env"
fi
chmod 600 "$install_dir/.env"
# Do not print expanded Compose configuration: it contains secrets.
docker compose --project-name has-waf --project-directory "$install_dir" --env-file "$install_dir/.env" \
  -f "$temp_dir/$compose_name" config --quiet
if [[ -f "$install_dir/$compose_name" ]]; then
  cp -- "$install_dir/$compose_name" "$install_dir/$compose_name.previous"
fi
mv -- "$temp_dir/$compose_name" "$install_dir/$compose_name"
if [[ "$deployment" == standalone ]]; then
  for name in data/db data/backups logs certs static-sites conf-data; do
    mkdir -p "$install_dir/$name"
  done
else
  for name in db conf logs certs static-sites backups redis clickhouse clickhouse-logs mysql; do
    mkdir -p "$install_dir/data/$name"
  done
  chmod 755 "$install_dir/data" "$install_dir/data/clickhouse" "$install_dir/data/clickhouse-logs"
fi
if [[ "$deployment" == standalone ]]; then
  printf '配置已准备到 %s，密码和密钥已自动生成；将连接外部 Redis 与日志存储。\n' "$install_dir"
else
  printf '配置已准备到 %s，密码和密钥已自动生成。\n' "$install_dir"
fi
if "$prepare_only"; then exit 0; fi
docker info >/dev/null 2>&1 || fail "Docker 引擎不可用，请启动后重试；已有凭据将保留"
# Passwords go through stdin to Docker's credential store, never project .env.
login_registry() {
  local host=$1 user=$2 password=$3 choice=''
  if [[ -z "$user" && -z "$password" && "$non_interactive" == false ]]; then
    printf '\n%s 需要登录：\n  1) 输入账号密码登录（默认）\n  2) 复用 Docker 已有登录凭据\n' "$host"
    while true; do
      ask '请输入序号 [1]：'; choice=${answer:-1}
      case "$choice" in 1|2) break;; *) printf '请输入 1 或 2。\n';; esac
    done
    if [[ "$choice" == 2 ]]; then return; fi
    ask '用户名：'; user=$answer
    printf '密码（隐藏输入）：'
    IFS= read -rs -u "$input_fd" password || fail '无法读取密码'
    printf '\n'
  fi
  if [[ -n "$user" || -n "$password" ]]; then
    [[ -n "$user" && -n "$password" ]] || fail "镜像源 $host 的用户名和密码必须同时提供"
    printf '%s' "$password" | docker login "$host" --username "$user" --password-stdin || fail "镜像源 $host 登录失败"
    unset password
  else
    printf '将复用 Docker 对 %s 的已有登录凭据。\n' "$host"
  fi
}
registry_user=${HAS_WAF_REGISTRY_USERNAME:-}
registry_password=${HAS_WAF_REGISTRY_PASSWORD:-}
middleware_user=${MIDDLEWARE_REGISTRY_USERNAME:-}
middleware_password=${MIDDLEWARE_REGISTRY_PASSWORD:-}
unset HAS_WAF_REGISTRY_USERNAME HAS_WAF_REGISTRY_PASSWORD MIDDLEWARE_REGISTRY_USERNAME MIDDLEWARE_REGISTRY_PASSWORD
installed_image=$(sed -n 's/^HAS_WAF_IMAGE=//p' "$install_dir/.env")
registry=${installed_image%%/*}
login_registry "$registry" "$registry_user" "$registry_password"
unset registry_user registry_password
authenticated_hosts=" $registry docker.1ms.run "
for image_key in REDIS_IMAGE CLICKHOUSE_IMAGE MYSQL_IMAGE; do
  middleware_image=$(sed -n "s/^$image_key=//p" "$install_dir/.env")
  [[ -n "$middleware_image" ]] || continue
  middleware_registry=${middleware_image%%/*}
  if [[ "$authenticated_hosts" != *" $middleware_registry "* ]]; then
    login_registry "$middleware_registry" "$middleware_user" "$middleware_password"
    authenticated_hosts+="$middleware_registry "
  fi
done
unset middleware_user middleware_password
if [[ "$action" == update ]]; then
  compose pull has-waf || fail "镜像拉取失败，现有容器未重建；请检查登录权限和网络后重试"
else
  compose pull || fail "镜像拉取失败，请检查源地址、登录权限和网络后重试；已有凭据将保留"
fi
startup_timeout=${HAS_WAF_START_TIMEOUT:-360}
if [[ "$database_backend" == mysql && -z "${HAS_WAF_START_TIMEOUT:-}" ]]; then startup_timeout=900; fi
[[ "$startup_timeout" =~ ^[0-9]{2,4}$ ]] && ((10#$startup_timeout >= 60 && 10#$startup_timeout <= 3600)) || fail "启动等待时长必须为 60 到 3600 秒"
printf '正在等待全部服务就绪，最长 %s 秒。首次创建 MySQL 数据目录和表结构可能较慢。\n' "$startup_timeout"
compose up -d --wait --wait-timeout "$startup_timeout" || { compose ps; fail "服务启动失败，请查看日志：docker compose --project-directory $install_dir -f $install_dir/$compose_name logs --tail 100"; }
compose ps
ui_port=$(sed -n 's/^HAS_WAF_UI_PORT=//p' "$install_dir/.env")
if [[ "$action" == update ]]; then
  printf '\nHAS-WAF 已更新，访问：http://<服务器IP>:%s/\n' "${ui_port:-8000}"
else
  printf '\nHAS-WAF 已就绪，访问：http://<服务器IP>:%s/setup\n' "${ui_port:-8000}"
  printf '初始化令牌保存在 %s/.env 的 SETUP_ACCESS_TOKEN 中，请在向导中填写。\n' "$install_dir"
  printf '请在浏览器完成管理员设置，中间件配置将自动带入。\n'
fi
