#!/bin/bash
# AethEx GPIO 自动 OTA 更新客户端
#
# 功能：
#   1. 从更新服务器获取 manifest.json（固件清单）
#   2. 与本地 /etc/aethex_gpio_version 中的版本号比对
#   3. 如需升级则下载更新包，校验 SHA-256，并自动安装
#
# 服务器地址查找优先级：
#   1. --server <URL> 命令行参数
#   2. UPDATE_SERVER 环境变量
#   3. /etc/aethex_gpio_update.conf 配置文件
#
# 用法：
#   auto_update.sh [选项]
#
# 参数：
#   --server <URL>       更新服务器根地址（本次生效，不保存）
#   --set-server <URL>   设置并保存服务器地址到配置文件（后续无需再指定）
#   --check-only         只检查版本，不下载安装
#   --force              忽略版本比对，强制重新安装
#   --show-config        显示当前配置信息
#   --help               显示帮助
#
# 示例：
#   # 首次部署：设置服务器地址（写入配置文件，只需执行一次）
#   auto_update.sh --set-server http://192.168.1.100:8080
#
#   # 之后直接运行即可，自动读取配置
#   auto_update.sh
#
#   # 只检查，不安装
#   auto_update.sh --check-only
#
#   # 临时使用其他服务器（不修改配置文件）
#   auto_update.sh --server http://192.168.1.200:9090
#
#   # 定时任务（cron / systemd timer）直接调用，无需任何参数
#   auto_update.sh

set -e

# ---------------------------------------------------------------------------
# 配置
# ---------------------------------------------------------------------------
VERSION_FILE="${VERSION_FILE:-/etc/aethex_gpio_version}"
CONFIG_FILE="${CONFIG_FILE:-/etc/aethex_gpio_update.conf}"
WORK_DIR="/tmp/aethex_ota"
LOG_FILE="${LOG_FILE:-/var/log/aethex_update.log}"

# 公钥文件路径（用于验证 OTA 包签名）
VERIFY_KEY_FILE="/etc/aethex_gpio/ota_verify.pub"

# 超时设置（秒）
CONNECT_TIMEOUT=10
DOWNLOAD_TIMEOUT=120

# 下载重试
MAX_RETRIES=3
RETRY_DELAY=5

# ---------------------------------------------------------------------------
# 颜色与日志
# ---------------------------------------------------------------------------
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m'

log() {
    local level="$1"; shift
    local msg="$*"
    local ts
    ts=$(date '+%Y-%m-%d %H:%M:%S')
    case "${level}" in
        INFO)  echo -e "${GREEN}[INFO]${NC}  ${msg}" >&2 ;;
        WARN)  echo -e "${YELLOW}[WARN]${NC}  ${msg}" >&2 ;;
        ERROR) echo -e "${RED}[ERROR]${NC} ${msg}" >&2 ;;
        STEP)  echo -e "${BLUE}[>>]${NC}   ${msg}" >&2 ;;
    esac
    # 同时写日志文件（无颜色码）
    echo "${ts} [${level}] ${msg}" >> "${LOG_FILE}" 2>/dev/null || true
}

die() { log ERROR "$*"; exit 1; }

# ---------------------------------------------------------------------------
# 配置文件读写
# 格式（shell 变量，每行一个）：
#   UPDATE_SERVER=http://192.168.1.100:8080
# ---------------------------------------------------------------------------
read_config() {
    [[ -f "${CONFIG_FILE}" ]] || return 1
    local val
    val=$(grep -m1 '^UPDATE_SERVER=' "${CONFIG_FILE}" 2>/dev/null | cut -d'=' -f2-)
    val="${val//\"/}"; val="${val//\'/}"   # 去引号
    val="${val%/}"                          # 去末尾斜杠
    [[ -n "${val}" ]] && echo "${val}" && return 0
    return 1
}

write_config() {
    local url="$1"
    url="${url%/}"
    mkdir -p "$(dirname "${CONFIG_FILE}")"
    cat > "${CONFIG_FILE}" <<EOF
# AethEx GPIO OTA 更新配置
# 由 auto_update.sh --set-server 写入，可手动修改
UPDATE_SERVER=${url}
EOF
    chmod 644 "${CONFIG_FILE}"
}

# ---------------------------------------------------------------------------
# 参数解析
# ---------------------------------------------------------------------------
OPT_SERVER=""
OPT_SET_SERVER=""
CHECK_ONLY=0
FORCE=0
SHOW_CONFIG=0

while [[ $# -gt 0 ]]; do
    case "$1" in
        --server)      OPT_SERVER="$2";     shift 2 ;;
        --set-server)  OPT_SET_SERVER="$2";  shift 2 ;;
        --check-only)  CHECK_ONLY=1;         shift ;;
        --force)       FORCE=1;              shift ;;
        --show-config) SHOW_CONFIG=1;        shift ;;
        --help|-h)
            grep "^#" "$0" | head -45 | sed 's/^# \?//'
            exit 0
            ;;
        *) die "未知参数: $1，使用 --help 查看帮助" ;;
    esac
done

# --set-server：写入配置文件后提示
if [[ -n "${OPT_SET_SERVER}" ]]; then
    write_config "${OPT_SET_SERVER}"
    log INFO "服务器地址已保存: ${OPT_SET_SERVER}"
    log INFO "配置文件: ${CONFIG_FILE}"
    log INFO "后续直接运行 auto_update.sh 即可，无需再指定 --server"
    # 如果同时没有其他操作意图，直接退出
    [[ ${CHECK_ONLY} -eq 0 && ${FORCE} -eq 0 && -z "${OPT_SERVER}" ]] && exit 0
fi

# --show-config：显示配置信息后退出
if [[ ${SHOW_CONFIG} -eq 1 ]]; then
    echo "配置文件:   ${CONFIG_FILE}"
    if [[ -f "${CONFIG_FILE}" ]]; then
        local_url=$(read_config) && echo "服务器地址: ${local_url}" || echo "服务器地址: (未配置)"
    else
        echo "服务器地址: (配置文件不存在)"
    fi
    echo "版本文件:   ${VERSION_FILE}"
    if [[ -f "${VERSION_FILE}" ]]; then
        echo "当前版本:   $(cat "${VERSION_FILE}")"
    else
        echo "当前版本:   (未安装)"
    fi
    exit 0
fi

# 确定最终服务器地址：--server > UPDATE_SERVER env > 配置文件
resolve_server() {
    # 1. 命令行参数
    if [[ -n "${OPT_SERVER}" ]]; then
        echo "${OPT_SERVER%/}"
        return 0
    fi
    # 2. 环境变量
    if [[ -n "${UPDATE_SERVER:-}" ]]; then
        echo "${UPDATE_SERVER%/}"
        return 0
    fi
    # 3. 配置文件
    if read_config; then
        return 0
    fi
    return 1
}

SERVER_URL=$(resolve_server) || \
    die "未找到更新服务器地址。请先执行:\n  auto_update.sh --set-server http://<服务器IP>:<端口>"
MANIFEST_URL="${SERVER_URL}/manifest.json"

# ---------------------------------------------------------------------------
# 检查依赖工具
# ---------------------------------------------------------------------------
check_deps() {
    local missing=()
    for cmd in wget sha256sum tar; do
        command -v "${cmd}" &>/dev/null || missing+=("${cmd}")
    done
    if [[ ${#missing[@]} -gt 0 ]]; then
        die "缺少依赖工具: ${missing[*]}，请先安装"
    fi
}

# ---------------------------------------------------------------------------
# 版本比较：ver_gt A B -> 0表示A>B，1表示A<=B
# 版本号格式：MAJOR.MINOR.PATCH（如 1.2.3）
# ---------------------------------------------------------------------------
ver_gt() {
    local a="$1" b="$2"
    # 按 . 分割，逐段比较
    IFS='.' read -r -a a_parts <<< "${a}"
    IFS='.' read -r -a b_parts <<< "${b}"
    local i
    for i in 0 1 2; do
        local av="${a_parts[$i]:-0}"
        local bv="${b_parts[$i]:-0}"
        # 去掉非数字（如后缀 -rc1）
        av="${av//[^0-9]/}"; av="${av:-0}"
        bv="${bv//[^0-9]/}"; bv="${bv:-0}"
        if (( av > bv )); then return 0; fi
        if (( av < bv )); then return 1; fi
    done
    return 1  # 相等也返回 false（不需要更新）
}

# ---------------------------------------------------------------------------
# 读取本地版本号
# 版本文件格式（由 update.sh 写入）：
#   <version> git:<hash> updated:<date>
#   例：1.2.3 git:abc1234 updated:2026-03-15 10:30
# ---------------------------------------------------------------------------
get_local_version() {
    if [[ ! -f "${VERSION_FILE}" ]]; then
        echo "0.0.0"
        return
    fi
    local ver
    ver=$(awk '{print $1}' "${VERSION_FILE}" 2>/dev/null || echo "0.0.0")
    echo "${ver}"
}

# ---------------------------------------------------------------------------
# 获取并解析 manifest.json
# manifest 格式（服务器端 JSON 文件）：
# {
#   "version": "1.3.0",
#   "build_date": "2026-03-19 10:00",
#   "git_hash": "abc1234",
#   "package_url": "http://192.168.1.100:8080/packages/aethex_gpio_update_1.3.0.tar.gz",
#   "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
#   "min_version": "1.0.0",
#   "changelog": "Fix WiFi recovery race condition; Add version compare in auto_update"
# }
# ---------------------------------------------------------------------------
fetch_manifest() {
    local tmp_manifest="${WORK_DIR}/manifest.json"
    mkdir -p "${WORK_DIR}"

    local attempt
    for attempt in $(seq 1 "${MAX_RETRIES}"); do
        log STEP "获取清单 (${attempt}/${MAX_RETRIES}): ${MANIFEST_URL}"
        rm -f "${tmp_manifest}"

        if wget -q --timeout="${CONNECT_TIMEOUT}" -O "${tmp_manifest}" "${MANIFEST_URL}" 2>/dev/null; then
            if grep -q '"version"' "${tmp_manifest}"; then
                echo "${tmp_manifest}"
                return 0
            fi
            log WARN "manifest.json 格式错误（缺少 version 字段）"
        else
            log WARN "获取 manifest.json 失败 (${attempt}/${MAX_RETRIES})"
        fi

        if [[ ${attempt} -lt ${MAX_RETRIES} ]]; then
            log INFO "${RETRY_DELAY} 秒后重试..."
            sleep "${RETRY_DELAY}"
        fi
    done

    die "无法获取 manifest.json，已重试 ${MAX_RETRIES} 次。请检查服务器地址和网络连接"
}

# 从 JSON 文件中提取指定字段（不依赖 jq，使用 grep/sed）
json_get() {
    local file="$1"
    local key="$2"
    grep -oP "\"${key}\"\s*:\s*\"\K[^\"]*" "${file}" 2>/dev/null | head -1
}

# ---------------------------------------------------------------------------
# 验证 OTA 包签名
# 使用 RSA 公钥验证包的签名
# ---------------------------------------------------------------------------
verify_signature() {
    local pkg_file="$1"
    local signature_b64="$2"

    # 如果没有签名，检查是否强制验证
    if [[ -z "${signature_b64}" ]]; then
        if [[ -f "${VERIFY_KEY_FILE}" ]]; then
            log WARN "manifest 中无签名，但存在公钥文件，拒绝安装"
            return 1
        fi
        log WARN "manifest 中无签名，跳过签名验证（兼容旧版本）"
        return 0
    fi

    # 检查公钥文件
    if [[ ! -f "${VERIFY_KEY_FILE}" ]]; then
        log WARN "未找到公钥文件 ${VERIFY_KEY_FILE}，跳过签名验证"
        return 0
    fi

    log STEP "验证 OTA 包签名..."

    # 将 Base64 签名解码并验证
    local sig_file="${WORK_DIR}/pkg.sig"
    echo "${signature_b64}" | base64 -d > "${sig_file}" 2>/dev/null

    if openssl dgst -sha256 -verify "${VERIFY_KEY_FILE}" -signature "${sig_file}" "${pkg_file}" 2>/dev/null; then
        log INFO "签名验证通过"
        rm -f "${sig_file}"
        return 0
    else
        log ERROR "签名验证失败！可能被篡改"
        rm -f "${sig_file}"
        return 1
    fi
}

# ---------------------------------------------------------------------------
# 下载并校验更新包（含重试机制）
# ---------------------------------------------------------------------------
download_package() {
    local pkg_url="$1"
    local expected_sha256="$2"
    local signature="$3"
    local pkg_name
    pkg_name="${WORK_DIR}/$(basename "${pkg_url}")"

    # 根据是否有 TTY 决定 wget 参数（无 TTY 时不显示进度条，避免日志污染）
    local wget_opts=(--timeout="${DOWNLOAD_TIMEOUT}")
    if [[ -t 2 ]]; then
        wget_opts+=(-q --show-progress)
    else
        wget_opts+=(-q)
    fi

    local attempt
    for attempt in $(seq 1 "${MAX_RETRIES}"); do
        log STEP "下载更新包 (${attempt}/${MAX_RETRIES}): ${pkg_url}"
        rm -f "${pkg_name}"

        if wget "${wget_opts[@]}" -O "${pkg_name}" "${pkg_url}" 2>&2; then
            # 下载成功，校验 SHA-256
            log STEP "校验 SHA-256..."
            local actual_sha256
            actual_sha256=$(sha256sum "${pkg_name}" | awk '{print $1}')

            if [[ "${actual_sha256}" != "${expected_sha256}" ]]; then
                log ERROR "SHA-256 校验失败！\n  期望: ${expected_sha256}\n  实际: ${actual_sha256}"
                rm -f "${pkg_name}"
                continue
            fi

            log INFO "SHA-256 校验通过"

            # 验证签名
            if ! verify_signature "${pkg_name}" "${signature}"; then
                log ERROR "签名验证失败，拒绝安装"
                rm -f "${pkg_name}"
                return 1
            fi

            RESULT_PKG_FILE="${pkg_name}"
            return 0
        else
            log WARN "下载失败 (${attempt}/${MAX_RETRIES})"
        fi

        if [[ ${attempt} -lt ${MAX_RETRIES} ]]; then
            log INFO "${RETRY_DELAY} 秒后重试..."
            sleep "${RETRY_DELAY}"
        fi
    done

    die "下载失败，已重试 ${MAX_RETRIES} 次: ${pkg_url}"
}

# ---------------------------------------------------------------------------
# 执行安装
# ---------------------------------------------------------------------------
do_install() {
    local pkg_file="$1"
    local remote_version="$2"

    log STEP "解压更新包..."
    local extract_dir="${WORK_DIR}/pkg_extract"
    rm -rf "${extract_dir}"
    mkdir -p "${extract_dir}"
    tar xzf "${pkg_file}" -C "${extract_dir}"

    # 找到解压后的根目录（tar 包内可能有一层子目录）
    local pkg_root
    pkg_root=$(find "${extract_dir}" -maxdepth 2 -name "update.sh" | head -1 | xargs dirname 2>/dev/null)
    [[ -z "${pkg_root}" ]] && die "更新包结构错误：找不到 update.sh"

    log STEP "执行安装脚本..."
    bash "${pkg_root}/update.sh"

    # 安装成功后清理
    rm -rf "${extract_dir}" "${pkg_file}"
    log INFO "清理临时文件完成"
}

# ---------------------------------------------------------------------------
# 主流程
# ---------------------------------------------------------------------------
main() {
    log INFO "=== AethEx GPIO 自动更新检查 ==="
    check_deps

    # 1. 读取本地版本
    local local_ver
    local_ver=$(get_local_version)
    log INFO "本地版本: ${local_ver}"

    # 2. 获取服务器清单
    local manifest
    manifest=$(fetch_manifest)

    local remote_ver build_date git_hash pkg_url sha256 min_ver changelog signature
    remote_ver=$(json_get "${manifest}" "version")
    build_date=$(json_get "${manifest}" "build_date")
    git_hash=$(json_get  "${manifest}" "git_hash")
    pkg_url=$(json_get   "${manifest}" "package_url")
    sha256=$(json_get    "${manifest}" "sha256")
    min_ver=$(json_get   "${manifest}" "min_version")
    changelog=$(json_get "${manifest}" "changelog")
    signature=$(json_get "${manifest}" "signature")

    [[ -z "${remote_ver}" ]] && die "manifest.json 中 version 字段为空"
    [[ -z "${pkg_url}"    ]] && die "manifest.json 中 package_url 字段为空"
    [[ -z "${sha256}"     ]] && die "manifest.json 中 sha256 字段为空"

    log INFO "服务器版本: ${remote_ver}  构建: ${build_date}  git: ${git_hash}"
    [[ -n "${changelog}" ]] && log INFO "变更: ${changelog}"

    # 3. 强制升级判断（本地版本低于 min_version）
    local force_upgrade=0
    if [[ -n "${min_ver}" ]] && ver_gt "${min_ver}" "${local_ver}"; then
        log WARN "本地版本 ${local_ver} 低于最低要求 ${min_ver}，强制升级"
        force_upgrade=1
    fi

    # 4. 版本比对
    if [[ ${FORCE} -eq 0 && ${force_upgrade} -eq 0 ]]; then
        if ! ver_gt "${remote_ver}" "${local_ver}"; then
            log INFO "已是最新版本（${local_ver}），无需更新"
            rm -rf "${WORK_DIR}"
            exit 0
        fi
    fi

    log INFO "发现新版本: ${local_ver} -> ${remote_ver}"

    # 5. 仅检查模式
    if [[ ${CHECK_ONLY} -eq 1 ]]; then
        log INFO "[check-only] 不执行下载安装"
        echo ""
        echo "  当前版本: ${local_ver}"
        echo "  可用版本: ${remote_ver}  (${build_date})"
        echo "  变更说明: ${changelog}"
        echo ""
        rm -rf "${WORK_DIR}"
        exit 0
    fi

    # 6. 下载
    RESULT_PKG_FILE=""
    download_package "${pkg_url}" "${sha256}" "${signature}"
    local pkg_file="${RESULT_PKG_FILE}"

    # 7. 安装
    do_install "${pkg_file}" "${remote_ver}"

    log INFO "=== 更新完成: ${local_ver} -> ${remote_ver} ==="
    rm -rf "${WORK_DIR}"
}

main "$@"
