#!/usr/bin/env bash
# 业务机采集 Agent（Grafana Alloy）一键安装 / 重配置
# 用法:
#   curl -fsSL https://downloads.monitor.chem-cloud.cn/install_alloy.sh | sudo bash
#   sudo bash install_alloy.sh              # 完整安装
#   sudo bash install_alloy.sh install      # 同上
#   sudo bash install_alloy.sh config       # 仅重新配置并重启
set -euo pipefail

DOWNLOAD_BASE="${DOWNLOAD_BASE:-https://downloads.monitor.chem-cloud.cn}"
ALLOY_ZIP_URL="${ALLOY_ZIP_URL:-${DOWNLOAD_BASE}/alloy-linux-amd64.zip}"
ALLOY_BIN="${ALLOY_BIN:-/usr/bin/alloy}"
ALLOY_CONFIG="${ALLOY_CONFIG:-/etc/alloy/config.alloy}"
ALLOY_STATE_DIR="${ALLOY_STATE_DIR:-/var/lib/alloy}"
ALLOY_USER="${ALLOY_USER:-root}"
SYSTEMD_UNIT="/etc/systemd/system/alloy.service"
WORKDIR="${TMPDIR:-/tmp}/alloy-install-$$"

usage() {
  cat <<'EOF'
用法:
  install_alloy.sh [install]   下载、安装并配置 Alloy（默认）
  install_alloy.sh config      仅重新交互配置并重启（不重新下载）
  install_alloy.sh -h|--help   显示帮助

环境变量（可选）:
  DOWNLOAD_BASE   下载站根地址，默认 https://downloads.monitor.chem-cloud.cn
  ALLOY_ZIP_URL   Alloy zip 完整 URL
EOF
}

die() { echo "ERROR: $*" >&2; exit 1; }
info() { echo "==> $*"; }
warn() { echo "WARN: $*" >&2; }

require_root() {
  if [ "$(id -u)" -ne 0 ]; then
    die "请使用 root 运行（sudo bash install_alloy.sh ...）"
  fi
}

require_amd64() {
  local arch
  arch="$(uname -m)"
  case "${arch}" in
    x86_64|amd64) ;;
    *) die "当前仅支持 x86_64/amd64，检测到: ${arch}" ;;
  esac
}

detect_os_family() {
  # echo: ubuntu | centos
  local id like=""
  if [ -f /etc/os-release ]; then
    # shellcheck disable=SC1091
    . /etc/os-release
    id="$(echo "${ID:-}" | tr '[:upper:]' '[:lower:]')"
    like="$(echo "${ID_LIKE:-}" | tr '[:upper:]' '[:lower:]')"
  else
    die "无法识别系统（缺少 /etc/os-release）"
  fi
  case "${id}" in
    ubuntu|debian|linuxmint|pop)
      echo ubuntu
      return
      ;;
    centos|rhel|rocky|almalinux|ol|anolis|openEuler|euler|fedora|amzn)
      echo centos
      return
      ;;
  esac
  case " ${like} " in
    *" debian "*|*" ubuntu "*)
      echo ubuntu
      return
      ;;
    *" rhel "*|*" fedora "*|*" centos "*)
      echo centos
      return
      ;;
  esac
  die "暂不支持的系统: ID=${id} ID_LIKE=${like}（仅支持 Ubuntu/Debian 系与 CentOS/RHEL 系）"
}

prompt_monitor_server() {
  local input=""
  local tty="/dev/tty"
  if [ ! -r "${tty}" ]; then
    die "当前环境无法交互输入（缺少 /dev/tty）。请下载脚本后执行: sudo bash install_alloy.sh"
  fi
  echo
  echo "请输入监控服务器地址（IP 或域名，不要带 http://、https:// 和端口）"
  echo "示例: 192.168.1.100  或  monitor.example.com"
  while true; do
    read -r -p "监控服务器地址: " input <"${tty}"
    input="$(echo "${input}" | sed -e 's|^https\?://||' -e 's|/.*$||' -e 's|:.*$||' -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
    if [ -n "${input}" ]; then
      MONITOR_SERVER="${input}"
      return
    fi
    echo "地址不能为空，请重新输入。"
  done
}

check_connectivity() {
  local host="$1"
  local ok=1
  info "检测监控服务器连通性（必须成功，否则无法推送指标/日志）"
  echo "  - 指标口: http://${host}:9090/-/ready"
  echo "  - 日志口: http://${host}:3100/ready"
  echo "  若失败，请在监控服务器安全组/防火墙放行 9090、3100（建议仅对业务机 IP 开放）"

  if command -v curl >/dev/null 2>&1; then
    if curl -fsS --connect-timeout 5 --max-time 10 "http://${host}:9090/-/ready" >/dev/null; then
      echo "  [OK] 9090 可达"
    else
      echo "  [FAIL] 无法访问 http://${host}:9090/-/ready"
      ok=0
    fi
    if curl -fsS --connect-timeout 5 --max-time 10 "http://${host}:3100/ready" >/dev/null; then
      echo "  [OK] 3100 可达"
    else
      echo "  [FAIL] 无法访问 http://${host}:3100/ready"
      ok=0
    fi
  elif command -v wget >/dev/null 2>&1; then
    if wget -q -O /dev/null --timeout=10 "http://${host}:9090/-/ready"; then
      echo "  [OK] 9090 可达"
    else
      echo "  [FAIL] 无法访问 http://${host}:9090/-/ready"
      ok=0
    fi
    if wget -q -O /dev/null --timeout=10 "http://${host}:3100/ready"; then
      echo "  [OK] 3100 可达"
    else
      echo "  [FAIL] 无法访问 http://${host}:3100/ready"
      ok=0
    fi
  else
    die "需要 curl 或 wget 做连通性检测"
  fi

  if [ "${ok}" -ne 1 ]; then
    echo
    die "连通性检测失败。请确认监控服务器已安装并运行，且已放行 9090/3100 后重试。"
  fi
}

is_ignored_ip() {
  local ip="$1"
  case "${ip}" in
    ""|127.*|169.254.*) return 0 ;;
    # Docker / 常见容器网桥网段
    172.17.*|172.18.*|172.19.*|172.20.*|172.21.*|172.22.*|172.23.*|172.24.*|172.25.*|172.26.*|172.27.*|172.28.*|172.29.*)
      return 0
      ;;
  esac
  return 1
}

is_ignored_iface() {
  local iface="$1"
  case "${iface}" in
    lo|docker*|br-*|veth*|virbr*|cni*|flannel*|cali*|kube*|nerdctl*|podman*)
      return 0
      ;;
  esac
  return 1
}

ip_in_list() {
  local needle="$1"
  shift
  local x
  for x in "$@"; do
    [ "${x}" = "${needle}" ] && return 0
  done
  return 1
}

detect_public_ip() {
  # 云主机公网 IP 常为 EIP/NAT，本机网卡上看不到；尝试元数据或公网探测
  local pub=""
  if command -v curl >/dev/null 2>&1; then
    # 常见云元数据（失败忽略）
    pub="$(curl -fsS --connect-timeout 1 --max-time 2 http://169.254.169.254/latest/meta-data/public-ipv4 2>/dev/null || true)"
    if [ -z "${pub}" ]; then
      pub="$(curl -fsS --connect-timeout 1 --max-time 2 http://169.254.169.254/2021-03-23/meta-data/public-ipv4s 2>/dev/null | head -n1 || true)"
    fi
    if [ -z "${pub}" ]; then
      pub="$(curl -fsS --connect-timeout 2 --max-time 3 https://api.ipify.org 2>/dev/null || true)"
    fi
    if [ -z "${pub}" ]; then
      pub="$(curl -fsS --connect-timeout 2 --max-time 3 https://ifconfig.me/ip 2>/dev/null || true)"
    fi
  fi
  pub="$(echo "${pub}" | tr -d '[:space:]')"
  # 粗校验 IPv4
  if [[ "${pub}" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
    printf '%s\n' "${pub}"
  fi
}

collect_candidate_ips() {
  # 输出候选 IP 列表到 stdout，一行一个
  local preferred=""
  local host="$1"
  local -a ips=()
  local -a notes=()  # 并行说明，用全局 CANDIDATE_NOTES 不太方便；改为仅输出 IP，说明在 prompt 里处理

  if command -v ip >/dev/null 2>&1; then
    preferred="$(ip -4 route get "${host}" 2>/dev/null | awk '{
      for (i=1;i<=NF;i++) if ($i=="src") { print $(i+1); exit }
    }' || true)"
  fi
  if [ -n "${preferred}" ] && ! is_ignored_ip "${preferred}"; then
    ips+=("${preferred}")
  fi

  if command -v ip >/dev/null 2>&1; then
    while read -r iface ip; do
      [ -z "${ip}" ] && continue
      is_ignored_iface "${iface}" && continue
      is_ignored_ip "${ip}" && continue
      if ! ip_in_list "${ip}" "${ips[@]:-}"; then
        ips+=("${ip}")
      fi
    done < <(ip -4 -o addr show scope global 2>/dev/null | awk '{
      iface=$2; gsub(/:$/,"",iface);
      split($4, a, "/");
      print iface, a[1]
    }')
  fi

  local pub=""
  pub="$(detect_public_ip || true)"
  if [ -n "${pub}" ] && ! is_ignored_ip "${pub}"; then
    if ! ip_in_list "${pub}" "${ips[@]:-}"; then
      # 公网 IP 插到最前（云主机场景更常作为资产 host）
      ips=("${pub}" "${ips[@]:-}")
    else
      # 已在列表中则挪到最前
      local -a rest=()
      local x
      for x in "${ips[@]}"; do
        [ "${x}" = "${pub}" ] && continue
        rest+=("${x}")
      done
      ips=("${pub}" "${rest[@]:-}")
    fi
  fi

  if [ "${#ips[@]}" -eq 0 ] && command -v hostname >/dev/null 2>&1; then
    while read -r ip; do
      is_ignored_ip "${ip}" && continue
      ips+=("${ip}")
    done < <(hostname -I 2>/dev/null | tr ' ' '\n')
  fi

  if [ "${#ips[@]}" -eq 0 ]; then
    DETECTED_PUBLIC_IP="${pub:-}"
    DETECTED_ROUTE_IP="${preferred:-}"
    return 0
  fi

  # 导出公网探测结果供 prompt 标注
  DETECTED_PUBLIC_IP="${pub:-}"
  DETECTED_ROUTE_IP="${preferred:-}"
  printf '%s\n' "${ips[@]}"
}

is_valid_ipv4() {
  local ip="$1"
  [[ "${ip}" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] || return 1
  return 0
}

prompt_local_ip() {
  local host="$1"
  local -a ips=()
  local tty="/dev/tty"
  DETECTED_PUBLIC_IP=""
  DETECTED_ROUTE_IP=""
  if [ ! -r "${tty}" ]; then
    die "当前环境无法交互输入（缺少 /dev/tty）。请下载脚本后执行: sudo bash install_alloy.sh"
  fi
  mapfile -t ips < <(collect_candidate_ips "${host}")

  echo
  echo "请选择本机标识 IP（写入采集配置，并应与监控平台中主机资产的 host 字段保持一致）"
  echo "说明："
  echo "  - 云主机公网地址常为 NAT/弹性 IP，本机网卡上可能只有内网 IP；脚本会尽量自动探测公网 IP"
  echo "  - 已自动过滤 Docker / 容器网桥等地址"
  echo "  - 列表不符合预期时，请选择「手动输入」"
  local i mark
  if [ "${#ips[@]}" -gt 0 ]; then
    for i in "${!ips[@]}"; do
      mark=""
      if [ -n "${DETECTED_PUBLIC_IP}" ] && [ "${ips[$i]}" = "${DETECTED_PUBLIC_IP}" ]; then
        mark="  ← 自动探测的公网 IP（可选）"
      elif [ -n "${DETECTED_ROUTE_IP}" ] && [ "${ips[$i]}" = "${DETECTED_ROUTE_IP}" ]; then
        mark="  ← 访问监控服务器时使用的内网源 IP（可选）"
      fi
      echo "  $((i + 1))) ${ips[$i]}${mark}"
    done
  else
    echo "  （未自动探测到可用 IP，请手动输入）"
  fi
  local manual_idx=$((${#ips[@]} + 1))
  echo "  ${manual_idx}) 手动输入"

  local choice="" custom=""
  while true; do
    if [ "${#ips[@]}" -gt 0 ]; then
      read -r -p "请输入序号 [1-${manual_idx}]（直接回车选 1）: " choice <"${tty}"
    else
      read -r -p "请输入序号 [${manual_idx}]（手动输入）: " choice <"${tty}"
      [ -z "${choice}" ] && choice="${manual_idx}"
    fi
    if [ -z "${choice}" ] && [ "${#ips[@]}" -gt 0 ]; then
      LOCAL_IP="${ips[0]}"
      return
    fi
    if [ "${#ips[@]}" -gt 0 ] && [[ "${choice}" =~ ^[0-9]+$ ]] && [ "${choice}" -ge 1 ] && [ "${choice}" -le "${#ips[@]}" ]; then
      LOCAL_IP="${ips[$((choice - 1))]}"
      return
    fi
    if [ "${choice}" = "${manual_idx}" ]; then
      while true; do
        read -r -p "请输入本机标识 IP（IPv4，例如 10.0.0.12）: " custom <"${tty}"
        custom="$(echo "${custom}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
        if is_valid_ipv4 "${custom}"; then
          LOCAL_IP="${custom}"
          return
        fi
        echo "IP 格式不正确，请重新输入。"
      done
    fi
    echo "无效序号，请重新输入。"
  done
}

template_metrics() {
  cat <<'EOF'
prometheus.exporter.unix "local_system" {
}

prometheus.scrape "system_metrics" {
  targets         = prometheus.exporter.unix.local_system.targets
  forward_to      = [prometheus.relabel.canonical_instance.receiver]
  scrape_interval = "30s"
}

prometheus.relabel "canonical_instance" {
  forward_to = [prometheus.remote_write.monitor_server.receiver]

  // 可选：保留 Alloy 默认的主机名，便于 Grafana 对照
  rule {
    action        = "replace"
    source_labels = ["instance"]
    regex         = "(.*)"
    target_label  = "alloy_hostname"
    replacement   = "${1}"
  }

  // 与 monitor_assets.host / 本机 path_targets 中的 host 一致
  rule {
    action        = "replace"
    source_labels = ["instance"]
    regex         = ".*"
    target_label  = "instance"
    replacement   = "<local_ip>"
  }
}

prometheus.remote_write "monitor_server" {
  endpoint {
    url = "http://<monitor_server>:9090/api/v1/write"
  }
}
EOF
}

# 按系统检查日志文件是否存在
# stdout: 仅输出 path|job（存在的文件）
# stderr: 人类可读的检查结果
select_log_targets() {
  local family="$1"
  local -a candidates=()
  if [ "${family}" = "ubuntu" ]; then
    candidates=(
      "/var/log/syslog|messages"
      "/var/log/auth.log|auth"
      "/var/log/ufw.log|ufw"
    )
  else
    candidates=(
      "/var/log/messages|messages"
      "/var/log/secure|auth"
      "/var/log/firewalld|ufw"
    )
  fi

  info "检查本机日志文件（仅系统/授权/防火墙；不采集应用日志）" >&2
  local item path job
  local found=0
  for item in "${candidates[@]}"; do
    path="${item%%|*}"
    job="${item##*|}"
    if [ -f "${path}" ]; then
      echo "  [OK]   ${path}  -> job=${job}" >&2
      printf '%s\n' "${path}|${job}"
      found=1
    else
      echo "  [SKIP] ${path}  （不存在，跳过）" >&2
    fi
  done
  if [ "${found}" -eq 0 ]; then
    warn "未找到任何可采集的系统日志文件；将只上报主机指标，不上报日志"
  fi
}

append_log_section() {
  # $1 = output config file, $2 = targets file (lines: path|job)
  local out="$1"
  local targets_file="$2"
  local -a lines=()
  local line
  while IFS= read -r line || [ -n "${line}" ]; do
    [ -n "${line}" ] && lines+=("${line}")
  done <"${targets_file}"

  if [ "${#lines[@]}" -eq 0 ]; then
    cat >>"${out}" <<'EOF'

// 未检测到可用系统/授权/防火墙日志，已跳过日志采集（仅推送指标）
EOF
    return
  fi

  {
    echo
    echo '// 仅采集系统 / 授权 / 防火墙日志，不采集应用日志'
    echo 'local.file_match "system_logs" {'
    echo '  path_targets = ['
    local i path job
    for i in "${!lines[@]}"; do
      path="${lines[$i]%%|*}"
      job="${lines[$i]##*|}"
      # Alloy/River 语法要求列表项末尾必须有逗号（含最后一项）
      printf '    { __path__ = "%s", job = "%s", host = "<local_ip>" },\n' "${path}" "${job}"
    done
    echo '  ]'
    echo '}'
    echo
    cat <<'EOF'
loki.source.file "log_collector" {
  targets    = local.file_match.system_logs.targets
  forward_to = [loki.write.monitor_server.receiver]
}

loki.write "monitor_server" {
  endpoint {
    url          = "http://<monitor_server>:3100/loki/api/v1/push"
    batch_wait   = "5s"
  }
}
EOF
  } >>"${out}"
}

escape_sed_repl() {
  # escape \ and & and | for sed replacement
  printf '%s' "$1" | sed -e 's/[\\&|]/\\&/g'
}

write_config() {
  local family="$1"
  local monitor="$2"
  local local_ip="$3"
  local tmp targets_file
  tmp="$(mktemp)"
  targets_file="$(mktemp)"

  template_metrics >"${tmp}"
  select_log_targets "${family}" >"${targets_file}"
  append_log_section "${tmp}" "${targets_file}"

  local m e
  m="$(escape_sed_repl "${monitor}")"
  e="$(escape_sed_repl "${local_ip}")"
  sed -i "s|<monitor_server>|${m}|g; s|<local_ip>|${e}|g" "${tmp}"

  mkdir -p "$(dirname "${ALLOY_CONFIG}")"
  install -m 0644 "${tmp}" "${ALLOY_CONFIG}"
  rm -f "${tmp}" "${targets_file}"
  info "已写入配置: ${ALLOY_CONFIG}"
}

download_and_install_binary() {
  mkdir -p "${WORKDIR}"
  cd "${WORKDIR}"
  info "下载 Alloy: ${ALLOY_ZIP_URL}"
  if command -v curl >/dev/null 2>&1; then
    curl -fL --connect-timeout 15 --retry 3 -o alloy-linux-amd64.zip "${ALLOY_ZIP_URL}"
  else
    wget -O alloy-linux-amd64.zip "${ALLOY_ZIP_URL}"
  fi
  info "解压..."
  if command -v unzip >/dev/null 2>&1; then
    unzip -o alloy-linux-amd64.zip >/dev/null
  elif command -v busybox >/dev/null 2>&1 && busybox unzip alloy-linux-amd64.zip >/dev/null 2>&1; then
    :
  else
    die "需要 unzip 命令解压 alloy-linux-amd64.zip，请先安装 unzip"
  fi

  local bin=""
  bin="$(find "${WORKDIR}" -type f -name 'alloy' -perm -u+x 2>/dev/null | head -n1 || true)"
  if [ -z "${bin}" ]; then
    bin="$(find "${WORKDIR}" -type f \( -name 'alloy' -o -name 'alloy-linux-amd64' \) 2>/dev/null | head -n1 || true)"
  fi
  if [ -z "${bin}" ]; then
    die "zip 中未找到 alloy 可执行文件"
  fi
  chmod +x "${bin}"
  install -m 0755 "${bin}" "${ALLOY_BIN}"
  info "已安装二进制: ${ALLOY_BIN}"
  "${ALLOY_BIN}" --version 2>/dev/null || "${ALLOY_BIN}" -version 2>/dev/null || true
}

write_systemd_unit() {
  mkdir -p "${ALLOY_STATE_DIR}"
  # Alloy: run [flags] config-path；flags 必须在配置文件之前
  # 默认 HTTP 调试口 12345 易冲突导致进程退出，改为专用端口
  cat >"${SYSTEMD_UNIT}" <<EOF
[Unit]
Description=Grafana Alloy (Monitor Agent)
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=${ALLOY_USER}
ExecStart=${ALLOY_BIN} run --storage.path=${ALLOY_STATE_DIR} --server.http.listen-addr=127.0.0.1:43124 ${ALLOY_CONFIG}
Restart=on-failure
RestartSec=5
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target
EOF
  systemctl daemon-reload
  systemctl enable alloy.service >/dev/null
  info "已注册 systemd 服务: alloy.service"
}

restart_alloy() {
  systemctl restart alloy.service
  sleep 2
  if systemctl is-active --quiet alloy.service; then
    info "Alloy 服务已启动"
  else
    echo
    echo "----- alloy.service 状态 -----"
    systemctl status alloy.service --no-pager -l || true
    echo
    echo "----- journalctl -u alloy -n 80 -----"
    journalctl -u alloy -n 80 --no-pager || true
    die "Alloy 启动失败。请把上面 journalctl 输出发给实施人员。"
  fi
}

do_config_only() {
  require_root
  require_amd64
  if [ ! -x "${ALLOY_BIN}" ]; then
    die "未找到 ${ALLOY_BIN}，请先执行完整安装: bash install_alloy.sh install"
  fi
  if ! command -v systemctl >/dev/null 2>&1; then
    die "需要 systemd"
  fi

  OS_FAMILY="$(detect_os_family)"
  info "检测到系统类型: ${OS_FAMILY}"
  prompt_monitor_server
  check_connectivity "${MONITOR_SERVER}"
  prompt_local_ip "${MONITOR_SERVER}"
  write_config "${OS_FAMILY}" "${MONITOR_SERVER}" "${LOCAL_IP}"
  write_systemd_unit
  restart_alloy
  print_done
}

do_install() {
  require_root
  require_amd64
  if ! command -v systemctl >/dev/null 2>&1; then
    die "需要 systemd"
  fi
  if ! command -v curl >/dev/null 2>&1 && ! command -v wget >/dev/null 2>&1; then
    die "需要 curl 或 wget"
  fi

  OS_FAMILY="$(detect_os_family)"
  info "检测到系统类型: ${OS_FAMILY}"

  prompt_monitor_server
  check_connectivity "${MONITOR_SERVER}"
  prompt_local_ip "${MONITOR_SERVER}"

  download_and_install_binary
  write_config "${OS_FAMILY}" "${MONITOR_SERVER}" "${LOCAL_IP}"
  write_systemd_unit
  restart_alloy
  print_done
}

print_done() {
  cat <<EOF

========================================
 采集 Agent（Alloy）安装/配置完成
 监控服务器: ${MONITOR_SERVER}
 本机标识 IP: ${LOCAL_IP}
 配置文件:   ${ALLOY_CONFIG}
 数据目录:   ${ALLOY_STATE_DIR}
 服务状态:   systemctl status alloy
========================================
请在监控平台中添加主机资产，并将 host 设置为: ${LOCAL_IP}
常用命令:
  systemctl status alloy
  journalctl -u alloy -f
  # 重新配置:
  # curl -fsSL ${DOWNLOAD_BASE}/install_alloy.sh | sudo bash -s -- config
EOF
}

cleanup() {
  rm -rf "${WORKDIR}" 2>/dev/null || true
}
trap cleanup EXIT

CMD="${1:-install}"
case "${CMD}" in
  -h|--help|help)
    usage
    ;;
  install)
    do_install
    ;;
  config|configure|reconfig)
    do_config_only
    ;;
  *)
    usage
    die "未知命令: ${CMD}"
    ;;
esac
