#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-2.0-only
#
# ebs-build.sh — 用 EulerMaker EBS (ccb) 自助构建内核
#
# 依赖: bash >= 4, curl, grep, sed, ruby (仅 --install-ccb 需要 ruby/gem), python3 (配置 package_repos 时需,缺失时降级为只追加/提示)
# 所有参数可默认,用环境变量覆盖;密码只从 PASSWORD 环境变量读取,绝不写入文件。
#
# 用法:
#   ebs-build.sh --install-ccb        安装 ccb 客户端到 ~/.local(方案 A: git clone)
#   ebs-build.sh --config             交互生成 ~/.config/cli/defaults/config.yaml
#   ebs-build.sh --setup              准备工程: 创建继承子工程 + 配置好 kernel 包源 + snapshot(重复运行安全)
#   ebs-build.sh build                触发构建并轮询至 aarch64/x86_64 完成(需确认,或 FORCE=1 跳过)
#   ebs-build.sh log <job_id> [--check]  打印构建日志 URL;--check 拉取并 grep 错误/签名状态
#   ebs-build.sh download [-a <arch>] 下载构建产出的 rpm 到当前目录
#   ebs-build.sh repo-url [arch...]  显示最后一次成功构建的产物仓库 URL(不触发构建)
#   ebs-build.sh --help
#
# 配置:版本/分支等从本地 ebs-build.conf 读取,首次运行自动从模板
#       ebs-build.conf.example 复制;环境变量可覆盖配置值
#   PROJECT     默认 ${USER}-<PROJECT_SUFFIX,取自 ebs-build.conf>
#   SPEC_URL / SPEC_BRANCH / INHERIT_FROM  默认取 ebs-build.conf,可用环境变量覆盖
#   EBS_CONFIG  配置文件路径(默认 <脚本上级目录>/ebs-build.conf)
#   PASSWORD    社区账号密码(--config / build 首次运行时需要)
#   MAX_POLLS   轮询次数上限(默认 90,配合 POLL_INTERVAL=60 即 90 分钟)
#   FORCE=1     跳过 build 前的确认提示(供 CI 使用)
#
# 详见 docs/ebs-build.md

set -euo pipefail

# ---------------- 参数与配置 ----------------
# 版本/分支等从本地配置文件 ebs-build.conf 读取,首次运行自动从模板
# ebs-build.conf.example 复制。取值优先级:环境变量 > 配置文件。
# 用 EBS_CONFIG 可指定别的配置文件路径。
CONF="${EBS_CONFIG:-$(cd "$(dirname "$0")/.." && pwd)/ebs-build.conf}"

# 读取配置值:环境变量优先,其次配置文件(不内置版本默认值)
conf_get() {
  local key="$1" fallback="$2"
  local val="$fallback"
  if [ -f "$CONF" ]; then
    local fv
    fv="$(sed -nE "s/^[[:space:]]*${key}[[:space:]]*=[[:space:]]*(.*)[[:space:]]*$/\\1/p" "$CONF" | head -1)"
    [ -n "$fv" ] && val="$fv"
  fi
  [ -n "${!key:-}" ] && val="${!key}"
  printf '%s' "$val"
}

# 读取并校验配置(setup / build / download 开头调用)
load_config() {
  if [ ! -f "$CONF" ]; then
    if [ -f "$CONF.example" ]; then
      cp "$CONF.example" "$CONF"
      info "未找到 $CONF,已从 $CONF.example 复制。"
      info "请编辑 $CONF 里的版本/分支(当前为模板示例值),临时覆盖可用环境变量。"
    else
      die "未找到配置文件 $CONF,也没有模板 $CONF.example。请复制模板或设置 EBS_CONFIG。"
    fi
  fi
  if [ -z "${PROJECT:-}" ]; then
    local sfx
    sfx="$(conf_get PROJECT_SUFFIX "")"
    PROJECT="${USER}${sfx:+-${sfx}}"
  fi
  SPEC_URL="$(conf_get SPEC_URL "")"
  SPEC_BRANCH="$(conf_get SPEC_BRANCH "")"
  INHERIT_FROM="$(conf_get INHERIT_FROM "")"
  PACKAGE="$(conf_get PACKAGE kernel)"
  if [ -z "$SPEC_URL" ] || [ -z "$SPEC_BRANCH" ] || [ -z "$INHERIT_FROM" ]; then
    die "配置文件 $CONF 缺少 SPEC_URL / SPEC_BRANCH / INHERIT_FROM,请补全后重试。"
  fi
}

# 校验 SPEC_URL 的远端仓库名等于 PACKAGE(kernel)。EBS 只对名为 kernel 的
# spec 仓库自动生成 Source0 kernel.tar.gz;名字不符不会触发生成,%prep 报
# No such file,构建快速失败。
check_spec_repo_name() {
  local repo
  repo="$(basename "${SPEC_URL%.git}")"
  [ "$repo" = "$PACKAGE" ] || die "SPEC_URL 仓库名 '$repo' 必须为 '$PACKAGE'(EBS 只对名为 kernel 的 spec 仓库自动生成 Source0 kernel.tar.gz)。fork 请用 gitcode.com/kernel-source/kernel 这类纯 kernel 名,不要加前缀。"
}

ARCHS="${ARCHS:-aarch64 x86_64}"
MAX_POLLS="${MAX_POLLS:-90}"
POLL_INTERVAL="${POLL_INTERVAL:-60}"

CCB_VERSION="${CCB_VERSION:-2.0.1}"
INSTALL_DIR="${CCB_INSTALL_DIR:-$HOME/.local/libexec/ccb-$CCB_VERSION}"
LOCAL_BIN="$HOME/.local/bin"
CONFIG_DIR="$HOME/.config/cli/defaults"
CONFIG_FILE="$CONFIG_DIR/config.yaml"
JWT_FILE="$HOME/.config/cli/jwt"
GATEWAY="https://eulermaker.openeuler.openatom.cn"

# ---------------- 工具函数 ----------------
die() { echo "[ebs] $*" >&2; exit 1; }
info() { echo "[ebs] $*"; }

need_cmd() { command -v "$1" >/dev/null 2>&1 || die "缺少命令: $1"; }

# 定位 ccb 可执行文件(支持 --install-ccb 生成的软链与系统安装)
find_ccb() {
  if command -v ccb >/dev/null 2>&1; then echo ccb; return 0; fi
  if [ -x "$LOCAL_BIN/ccb" ]; then echo "$LOCAL_BIN/ccb"; return 0; fi
  if [ -x /usr/sbin/ccb ]; then echo /usr/sbin/ccb; return 0; fi
  return 1
}
CCB="$(find_ccb 2>/dev/null || true)"

require_ccb() {
  [ -n "$CCB" ] || die "未找到 ccb,先运行: $0 --install-ccb"
}

require_jwt() {
  if [ ! -f "$JWT_FILE" ]; then
    info "未找到已保存的登录令牌 (~/.config/cli/jwt),需要重新登录"
    info "请确认已运行 --config 且已 export PASSWORD 环境变量,然后重试"
    return 1
  fi
}

# ccb 因缺少 PASSWORD / JWT 失效报错时,输出解决方法提示
jwt_password_hint() {
  grep -qE "ENV\['PASSWORD'\]|undefined method .strip. for nil|PASSWORD" <<<"$1" \
    && die "登录令牌(JWT)已失效且未设置密码环境变量。

  请先 export PASSWORD 后重试:
    export PASSWORD='你的社区账号密码'
    $0 build
  脚本不会把密码写入任何文件;若尚未配置账号,先执行 $0 --config。"
  return 1
}

# ---------------- --install-ccb ----------------
cmd_install_ccb() {
  info "检查依赖..."
  need_cmd git
  need_cmd wget
  for c in ruby; do need_cmd "$c"; done
  local miss=0
  for gem in activesupport rest-client; do
    if ! ruby -e "require '$gem'" >/dev/null 2>&1; then
      echo "  缺少 ruby gem: $gem"
      miss=1
    fi
  done
  if [ "$miss" -eq 1 ]; then
    echo "  请先安装: dnf install -y ruby rubygem-activesupport rubygem-rest-client wget"
    echo "  (docker 仅本地构建需要,此处不检查)"
    exit 1
  fi

  info "安装 ccb v${CCB_VERSION} 到 $INSTALL_DIR"
  if [ ! -d "$INSTALL_DIR/sbin/cli" ]; then
    mkdir -p "$(dirname "$INSTALL_DIR")"
    info "  git clone https://gitcode.com/src-openeuler/ccb ..."
    git clone --depth 1 "https://gitcode.com/src-openeuler/ccb" "$INSTALL_DIR" 2>&1 \
      || die "git clone 失败,请检查网络或改用官方 tarball"
  else
    info "  已存在,跳过下载"
  fi

  mkdir -p "$LOCAL_BIN"
  ln -sfn "$INSTALL_DIR/sbin/cli/ccb" "$LOCAL_BIN/ccb"
  info "已软链: $LOCAL_BIN/ccb -> $INSTALL_DIR/sbin/cli/ccb"

  if ! find_ccb >/dev/null 2>&1; then
    echo "  警告: ~/.local/bin 不在 PATH,可执行以下命令加入:"
    echo "    echo 'export PATH=\"\$HOME/.local/bin:\$PATH\"' >> ~/.bashrc && source ~/.bashrc"
  fi
  info "验证: $($LOCAL_BIN/ccb -h 2>&1 | head -1)"
  info "下一步: $0 --config"
}

# ---------------- --config ----------------
cmd_config() {
  mkdir -p "$CONFIG_DIR"
  if [ -f "$CONFIG_FILE" ]; then
    info "已有 $CONFIG_FILE,将覆盖。Ctrl-C 可取消。"
  fi
  read -rp "社区账号 (ACCOUNT): " ACCOUNT
  : "${ACCOUNT:?账号不能为空}"

  cat > "$CONFIG_FILE" <<EOF
GATEWAY_IP: ${GATEWAY_IP:-eulermaker.openeuler.openatom.cn}
GATEWAY_PORT: ${GATEWAY_PORT:-443}
SRV_URL: ${SRV_URL:-https://eulermaker.openeuler.openatom.cn}
ACCOUNT: ${ACCOUNT}
# PASSWORD 不写入本文件,构建时通过环境变量 PASSWORD 提供:
#   export PASSWORD='你的社区密码'
OAUTH_TOKEN_URL: ${OAUTH_TOKEN_URL:-https://omapi.osinfra.cn/oneid/oidc/token}
OAUTH_REDIRECT_URL: ${OAUTH_REDIRECT_URL:-https://eulermaker.openeuler.openatom.cn/oauth}
PUBLIC_KEY_URL: ${PUBLIC_KEY_URL:-https://omapi.osinfra.cn/oneid/public/key?community=openeuler}
EOF
  chmod 600 "$CONFIG_FILE"
  info "已生成 $CONFIG_FILE"
  info "提示: 使用前请先 export PASSWORD 环境变量(不会写入任何文件)"
  info "验证: $0 --setup 会检查登录令牌,必要时自动重新获取"
}

# ---------------- --setup ----------------
cmd_setup() {
  require_ccb
  load_config
  check_spec_repo_name
  info "准备工程: $PROJECT"

  # 1. 工程是否存在(不存在则创建继承子工程)
  if "$CCB" select projects os_project="$PROJECT" --size 1 2>/dev/null | grep -q '"_id"'; then
    info "  工程已存在,跳过创建"
  else
    info "  创建继承子工程: inherited_from=$INHERIT_FROM"
    "$CCB" create projects "$PROJECT" inherited_from="$INHERIT_FROM"
  fi

  # 2. 配置 package_repos:与 ebs-build.conf 的 SPEC_URL/SPEC_BRANCH 一致则跳过,否则更新
  # spec_url 需以 .git 结尾(云端 schema 强制),缺则补,避免 update 被拒
  case "$SPEC_URL" in
    *.git) : ;;
    *) SPEC_URL="${SPEC_URL}.git" ;;
  esac
  local cur_json
  cur_json="$("$CCB" select projects os_project="$PROJECT" --field package_repos --size 1 2>/dev/null)"

  # 已配置一致(spec_name/spec_url/spec_branch 三要素同时命中)则跳过
  if printf '%s' "$cur_json" | grep -Fq "\"spec_name\": \"${PACKAGE}\"" \
     && printf '%s' "$cur_json" | grep -Fq "\"spec_url\": \"${SPEC_URL}\"" \
     && printf '%s' "$cur_json" | grep -Fq "\"spec_branch\": \"${SPEC_BRANCH}\""; then
    info "  package_repos 已与配置一致,跳过"
  elif command -v python3 >/dev/null 2>&1; then
    info "  配置 package_repos: $SPEC_URL ($SPEC_BRANCH)"
    local tmp
    tmp="$(mktemp)"
    SPEC_URL="$SPEC_URL" SPEC_BRANCH="$SPEC_BRANCH" PACKAGE="$PACKAGE" python3 - "$cur_json" > "$tmp" <<'PYEOF'
import json, os, sys
doc = json.loads(sys.argv[1])
cur = doc[0]['_source'].get('package_repos', []) if doc else []
target = {"spec_name": os.environ["PACKAGE"], "spec_url": os.environ["SPEC_URL"], "spec_branch": os.environ["SPEC_BRANCH"]}
new = [e for e in cur if e.get("spec_name") != target["spec_name"]] + [target]
print(json.dumps({"package_repos": new}))
PYEOF
    "$CCB" update projects "$PROJECT" -j "$tmp"
    rm -f "$tmp"
  elif printf '%s' "$cur_json" | grep -Fq "\"spec_name\": \"${PACKAGE}\""; then
    info "  WARN: 缺少 python3 且 package_repos 已有 ${PACKAGE},无法精确更新 spec_url"
    info "        请手动执行: $CCB update projects $PROJECT -j '{\"package_repos\": [...]}'"
  else
    info "  追加 package_repos: $SPEC_URL ($SPEC_BRANCH)"
    local tmp
    tmp="$(mktemp)"
    cat > "$tmp" <<EOF
{"package_repos+":[{"spec_name":"${PACKAGE}","spec_url":"${SPEC_URL}","spec_branch":"${SPEC_BRANCH}"}]}
EOF
    "$CCB" update projects "$PROJECT" -j "$tmp"
    rm -f "$tmp"
  fi

  # 3. snapshot 是否存在(没有则创建,作为依赖基线/可复现版本号)
  if "$CCB" select snapshots os_project="$PROJECT" --size 1 2>/dev/null | grep -q '"_id"'; then
    info "  snapshot 已存在,跳过"
  else
    info "  创建 snapshot(固定依赖基线)"
    "$CCB" create snapshots "$PROJECT"
  fi

  info "完成。下一步: $0 build"
}

# ---------------- build ----------------
cmd_build() {
  require_ccb
  load_config
  check_spec_repo_name
  require_jwt || { "$CCB" -h >/dev/null 2>&1 || true; exit 1; }

  info "将触发真实云端构建: os_project=$PROJECT packages=$PACKAGE"
  if [ "${FORCE:-0}" != "1" ]; then
    read -rp "确认构建? [y/N] " ans
    case "$ans" in
      y|Y) : ;;
      *) die "已取消" ;;
    esac
  fi

  local out
  out="$("$CCB" build-single os_project="$PROJECT" packages="$PACKAGE" 2>&1)" || {
    jwt_password_hint "$out"
    die "build-single 失败,输出如下:
$out"
  }
  echo "$out"

  local ba
  ba="$(parse_build_arch <<<"$out")"
  if [ -z "$ba" ]; then
    info "未能从响应中解析 build_id/架构,请在上方输出中查找 build_id 后用 'log' 查看进度"
    exit 0
  fi
  info "触发成功,build_id -> arch:"
  sed 's/^/    /' <<<"$ba"

  poll_builds "$ba"
}

# 从 build-single 输出解析 "build_id arch" 每行一对
parse_build_arch() {
  awk '
    /"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"/ {
      match($0, /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/)
      bid = substr($0, RSTART, RLENGTH)
    }
    /"architecture"[[:space:]]*:[[:space:]]*"/ {
      if (bid != "") {
        arch = $0
        sub(/.*"architecture"[[:space:]]*:[[:space:]]*"/, "", arch)
        sub(/".*/, "", arch)
        print bid, arch
        bid = ""
      }
    }
  '
}

# 状态码 → 中文描述(204=准备 200=构建中 201=成功 202=已发布 203=失败)
status_label() {
  case "$1" in
    200) echo "构建中" ;;
    201) echo "成功" ;;
    202) echo "已发布" ;;
    203) echo "失败" ;;
    204) echo "准备中" ;;
    *)   echo "状态?" ;;
  esac
}

# 秒数格式化为 "Xm Ys"
fmt_elapsed() {
  echo "$(( $1 / 60 ))m $(( $1 % 60 ))s"
}

# 查 builds 索引的 status 数字(204=准备 200=构建中 201=成功 202=已发布 203=卡住/失败)
get_build_status() {
  "$CCB" select builds build_id="$1" --field status --size 1 2>/dev/null \
    | grep -oE '"status":[[:space:]]*[0-9]+' | grep -oE '[0-9]+' | head -1
}

poll_builds() {
  local ba="$1"
  local i=0 start elapsed labels
  start="$(date +%s)"
  while [ "$i" -lt "$MAX_POLLS" ]; do
    i=$((i + 1))
    elapsed=$(( $(date +%s) - start ))
    labels=""
    local all_done=1
    local bid arch status
    while read -r bid arch; do
      status="$(get_build_status "$bid")"
      [ -n "$status" ] || status="?"
      labels="${labels:+$labels, }${arch}: $(status_label "$status")"
      case "$status" in
        201|202) : ;;                                   # 成功 / 已发布
        203) info "  [${arch}] 构建失败(status=203)"; exit 1 ;;
        *) all_done=0 ;;                                # 204/200/空 → 继续等
      esac
    done <<<"$ba"

    if [ "$all_done" -eq 1 ]; then
      info "  全部架构构建成功: ${labels}"
      print_log_hint "$ba"
      while read -r bid arch; do print_repo_url "$arch"; done <<<"$ba"
      return 0
    fi

    info "  [第 ${i}/${MAX_POLLS} 次 · 已用 $(fmt_elapsed "$elapsed")]${labels:+ ${labels}}(${POLL_INTERVAL}s 后重试)"
    [ "$i" -lt "$MAX_POLLS" ] && sleep "$POLL_INTERVAL"
  done
  info "轮询超时(已等待 $(( MAX_POLLS * POLL_INTERVAL / 60 )) 分钟)。最近状态: ${labels}"
  info "仍可能在后台上构建,可用 'log <job_id> --check' 或 Web 构建历史查看"
  exit 1
}

print_log_hint() {
  local ba="$1" bid arch
  info "各架构 job 日志(可用 $0 log <job_id> 查看):"
  while read -r bid arch; do
    "$CCB" select jobs build_id="$bid" --field id,arch --size 20 2>/dev/null \
      | grep -oE '"id": "[^"]+"|"arch": "[^"]+"' | paste - - | sed 's/^/    /'
  done <<<"$ba"
}

# 查某架构最新产物 rpm 仓库路径(复用 ccb select)
get_rpm_path() {
  local arch="$1"
  "$CCB" select rpms "$PROJECT" repo_name="${PACKAGE%%:*}" architecture="$arch" \
    --field 'rpm_path' --sort 'submit_time:desc' --size 1 2>/dev/null \
    | python3 -c 'import sys,json
try:
    d=json.load(sys.stdin)
    print(d[0]["_source"]["rpm_path"] if d else "")
except Exception:
    print("")'
}

# 查项目产物服务器标识 emsx
get_emsx() {
  "$CCB" select projects "$PROJECT" --field 'emsx' --size 1 2>/dev/null \
    | python3 -c 'import sys,json
try:
    d=json.load(sys.stdin)
    print(d[0]["_source"].get("emsx","ems1") if d else "ems1")
except Exception:
    print("ems1")'
}

# 从 ccb 配置读取扁平键(~/.config/cli/defaults/config.yaml,KEY: value 格式)
ccb_cfg() {
  sed -nE "s/^${1//./\\.}: *//p" "$HOME/.config/cli/defaults/config.yaml" | tr -d ' \r' | head -1
}

# 产物仓库 URL 前缀
repo_prefix() {
  local proto host port
  proto="$(ccb_cfg SRV_HTTP_REPOSITORIES_PROTOCOL)"
  host="$(ccb_cfg SRV_HTTP_REPOSITORIES_HOST)"
  port="$(ccb_cfg SRV_HTTP_REPOSITORIES_PORT)"
  printf '%s%s:%s' "${proto:-https://}" "${host}" "${port}"
}

# 打印某架构产物仓库 URL
print_repo_url() {
  local arch="$1" path emsx
  path="$(get_rpm_path "$arch")" || return 1
  if [ -z "$path" ]; then
    info "  [${arch}] 未查到产物记录(rpms 索引无匹配)"
    return 1
  fi
  emsx="$(get_emsx)"
  info "  [${arch}] 产物仓库: $(repo_prefix)/api/${emsx}${path}"
}

# ---------------- log ----------------
cmd_log() {
  require_ccb
  local job_id="${1:-}"
  [ -n "$job_id" ] || die "用法: $0 log <job_id> [--check]"
  local check="${2:-}"

  local rr
  rr="$("$CCB" select jobs id="$job_id" --field result_root --size 1 2>/dev/null \
        | sed -nE 's/.*"result_root"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' | head -1)"
  [ -n "$rr" ] || die "未找到 job: $job_id"

  local url
  url="${GATEWAY}/api/result${rr#/result}/dmesg"
  echo "构建日志: $url"

  if [ "$check" = "--check" ]; then
    need_cmd curl
    info "拉取日志并检查...(可能较大,请耐心)"
    local log
    log="$(curl -fsSL "$url" 2>/dev/null)" || die "拉取日志失败: $url"

    info "--- 真实失败模式 ---"
    if grep -E 'make: \*\*\*|undefined reference|No rule to make target|Out of memory|Killed|PANIC|internal compiler' <<<"$log"; then
      exit 1
    else
      echo "  (未发现上述失败模式)"
    fi

    info "--- 模块签名状态 ---"
    grep -oE 'openEuler_has_sign_perm=[01]' <<<"$log" | sort -u || echo "  (日志中未找到签名探测输出)"
  fi
}

# ---------------- download ----------------
cmd_download() {
  require_ccb
  load_config
  local arch=""
  while [ "$#" -gt 0 ]; do
    case "$1" in
      -a|--arch) arch="$2"; shift 2 ;;
      *) die "未知参数: $1(用法: $0 download [-a aarch64|x86_64])" ;;
    esac
  done

  local args=(os_project="$PROJECT" packages="$PACKAGE")
  if [ -n "$arch" ]; then args+=(architecture="$arch"); fi
  info "下载 rpm: ${args[*]}"
  "$CCB" download "${args[@]}"
}

# 显示最后一次成功构建的产物仓库 URL(不触发构建)
cmd_repo_url() {
  require_ccb
  load_config
  local archs=("$@")
  [ "${#archs[@]}" -eq 0 ] && archs=($ARCHS)
  for arch in "${archs[@]}"; do print_repo_url "$arch" || true; done
}

# ---------------- help ----------------
cmd_help() {
  sed -n '2,26p' "$0" | sed 's/^# \{0,1\}//'
}

# ---------------- 入口 ----------------
case "${1:-}" in
  --install-ccb) cmd_install_ccb ;;
  --config)      cmd_config ;;
  --setup)       cmd_setup ;;
  build)         shift; cmd_build ;;
  log)           shift; cmd_log "$@" ;;
  download)      shift; cmd_download "$@" ;;
  repo-url)      shift; cmd_repo_url "$@" ;;
  --help|-h|"")  cmd_help ;;
  *)             die "未知命令: ${1:-}(--help 查看用法)" ;;
esac