#!/bin/bash
# 轻量后端部署（light-update 的执行体）。
#   - composer.lock 门：远端 raw/PHP/composer.lock == 本机？ 相同→code.zip(~1MB,保留vendor)；不同→latest.zip全量
#   - **先下载校验，确认源存在后才做任何破坏性操作**（避免"删了迁移又没铺上"）
#   - 产物版本门：版本号只认包里的 package.json，不新于本机就拒绝部署（见下）
#   - 部署前 tar 备份 PHP 代码(排除 vendor)；migrate 后健康检查；失败自动回滚
#   - 绝不 snap refresh 前端、绝不 apt
# 退出码：0=成功(或无需更新)，1=失败/已回滚。末行输出 DEPLOY_OK <ver> | ROLLED_BACK | ABORT_* | SKIP_*
# 同一行也写进 /var/tmp/aaden-lu-status，供 index.js 读出来发到 cloud deviceLog
# —— 光靠退出码分不清"TSE 忙跳过"和"产物陈旧"，deviceLog 里就只剩一句含糊的失败。
set -uo pipefail
S3="https://aaden-backend.s3.eu-central-1.amazonaws.com"
# 参数只有 staging 通道（modules/release-channel.js，测试机）会传；不带参数 = 原来的 stable 路径，URL 和判断都不变。
#   $1 = staging → 从 S3 的 staging/ 前缀拉（composer.lock 变了不退回全量包，直接跳过，由调用方这次改走 stable）
#   $1 = branch:<通道名> → 分支通道（POS-166）：从 branches/<通道名>/ 拉；通道名不合规直接中止（这个值会拼进 URL）
#   $2 = semver  → 版本带「-」后缀时按 semver 比（1.9.83-staging.15 < 1.9.83）；纯 x.y.z 之间照旧
#   $2 = pin     → 分支通道用：包的版本和本机不一样就装（1.9.85-br.x 在 semver 里比 1.9.85 小，按「更新才装」会一直被 stable 盖掉）
CHANNEL="${1:-}"
CMP="${2:-}"
PREFIX=""
BRANCH=""
[ "$CHANNEL" = staging ] && PREFIX="/staging"
case "$CHANNEL" in
  branch:*)
    BRANCH="${CHANNEL#branch:}"
    # 用 bash 自己的正则整串匹配（grep 按行匹配，带换行的值能绕过）
    if [ "${#BRANCH}" -le 45 ] && [[ "$BRANCH" =~ ^(feat|fix)-[a-z0-9]+(-[a-z0-9]+)*$ ]]; then
      PREFIX="/branches/$BRANCH"
    else
      BRANCH="!"
    fi
    ;;
esac
STAGE=/var/tmp/aaden-lu-stage      # 绝对路径：download 与 sudo cp 必须看到同一个目录（~ 在 sudo 下会变 /root）
BACKUP=/var/lib/aaden-php-backup
PW='Innerken322.'
sudoS(){ echo "$PW" | sudo -S bash -c "$*"; }

STATUS_FILE=/var/tmp/aaden-lu-status
rm -f "$STATUS_FILE" 2>/dev/null || true
# 每条退出路径都经 finish：状态既打到 stdout(人看日志)，也落盘(index.js 读了发 deviceLog)。
finish(){ echo "$1"; echo "$1" > "$STATUS_FILE" 2>/dev/null || true; exit "$2"; }

# 同一时间只跑一个：04:59 cron、开机、do-log 补跑、云端命令、远程触发可能撞在一起，两个同时 cp/migrate 会互相踩。
# 锁文件谁先建都要让另一个用户也能打开（root 和 aaden 都会跑），所以 666；flock 只要读权限。
LOCK=/var/tmp/aaden-lu.lock
[ -e "$LOCK" ] || { touch "$LOCK" && chmod 666 "$LOCK"; } 2>/dev/null
if exec 9<"$LOCK" 2>/dev/null; then
  flock -n 9 || finish "SKIP_BUSY (另一个 light-update 正在跑)" 0
fi
[ "$BRANCH" = "!" ] && finish "ABORT_BAD_CHANNEL (分支通道名不合规)" 1

# 点分数字版本比较：a > b 返回 0。与 index.js 的 versionGt 同一套语义（'1.9.61' > '1.7.908'）。
# 只有 staging 通道传了 semver、并且两个版本里至少一个带「-」后缀（1.9.83-staging.15）时才改用 verGtSemver；
# 纯 x.y.z 之间、以及不带参数的 stable 路径，一律走下面原来的比较，结果和改动前逐个一致。
verGt(){ [ "$CMP" = semver ] && case "$1$2" in *-*) verGtSemver "$1" "$2"; return;; esac; python3 -c "
import sys
pa=[int(x) if x.isdigit() else 0 for x in sys.argv[1].split('.')]
pb=[int(x) if x.isdigit() else 0 for x in sys.argv[2].split('.')]
n=max(len(pa),len(pb)); pa+=[0]*(n-len(pa)); pb+=[0]*(n-len(pb))
sys.exit(0 if pa>pb else 1)" "$1" "$2" 2>/dev/null; }

# semver：x.y.z[-预发布]，有预发布的 < 没有的，预发布逐段比（数字段按数值、数字段 < 字母段）；不合法一律「不大于」（不更新）
verGtSemver(){ python3 -c "
import re,sys
def p(v):
    m=re.fullmatch(r'(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?',v.strip())
    if not m: return None
    return [int(x) for x in m.groups()[:3]], (m.group(4).split('.') if m.group(4) else [])
def key(pre):
    if not pre: return (1,)
    return (0,)+tuple((0,int(x),'') if x.isdigit() else (1,0,x) for x in pre)+((-1,0,''),)
a,b=p(sys.argv[1]),p(sys.argv[2])
if a is None or b is None: sys.exit(1)
sys.exit(0 if (a[0],key(a[1]))>(b[0],key(b[1])) else 1)" "$1" "$2" 2>/dev/null; }

# 读 package.json 的 version；读不到输出空串。
pkgVer(){ python3 -c "
import json,sys
try: print(json.load(open(sys.argv[1]))['version'])
except Exception: print('')" "$1" 2>/dev/null; }

# 早失败：老 aaden 设备的 sudo 密码可能不是共享密码。先探一次 ——
# 否则会一路穿到 ABORT_DOWNLOAD，把"密码不符"误报成"下载问题"，排障跑偏。
# NOPASSWD 设备此探测同样通过（sudo 不问，管道密码被忽略）。
if ! echo "$PW" | sudo -S true 2>/dev/null; then finish "ABORT_SUDO_AUTH (aaden sudo 密码不符?)" 1; fi

# ---- composer.lock 门 ----
REMOTE_LOCK=$(curl -s "$S3$PREFIX/raw/PHP/composer.lock" --max-time 30)
LOCAL_LOCK=$(cat /var/www/html/PHP/composer.lock 2>/dev/null)
RH=$(printf '%s' "$REMOTE_LOCK" | shasum -a 256 | cut -d' ' -f1)
LH=$(printf '%s' "$LOCAL_LOCK"  | shasum -a 256 | cut -d' ' -f1)
if [ -n "$REMOTE_LOCK" ] && [ "$RH" = "$LH" ]; then
  ARTIFACT="code.zip"; echo "composer.lock 一致 → 轻量 code.zip"
elif [ "$CHANNEL" = staging ]; then
  # staging/ 下没有全量包：vendor 变了就这次不走 staging（调用方改走 stable），不拿 stable 的全量包冒充
  finish "SKIP_STAGING_LOCK (staging 的 composer.lock 和本机不一致或取不到，这次不走 staging)" 1
elif [ -n "$BRANCH" ]; then
  # branches/ 下也没有全量包：分支改了 composer 依赖（或取不到 lock）就这次不走分支，由调用方改走 stable
  finish "SKIP_BRANCH_LOCK (分支 $BRANCH 的 composer.lock 和本机不一致或取不到，这次不走分支)" 1
else
  ARTIFACT="latest.zip"; echo "composer.lock 变化(或取不到) → 全量 latest.zip"
fi

# ---- 下载 + 解压到绝对暂存 ----
sudoS "rm -rf $STAGE && mkdir -p $STAGE && chown $(id -un):$(id -gn) $STAGE"
# 用 curl 而非 axel：axel 在设备上是 snap 包，受限写不进 /var/tmp。
# code.zip 仅 ~1MB，curl 瞬间完成；latest.zip 全量走 curl 稍慢但那是罕见 fallback。
ZIP="$STAGE/$ARTIFACT"
# 断点续传：网慢的设备 20 分钟下不完 500MB 的 latest.zip（10-07/08 的 9002 每天 1200 秒超时、白下一遍）。
# 半截文件放在 STAGE 外面跨次保留，下次 curl -C - 接着下；远端 ETag 变了（发了新版）就作废重下。
# root（cron）和 aaden（补跑 / 云端命令）都会跑，目录 777、文件归当前用户。
DL=/var/tmp/aaden-lu-dl
PART="$DL/$ARTIFACT.part"
sudoS "mkdir -p $DL && chmod 777 $DL && { chown -f $(id -un):$(id -gn) $DL/* 2>/dev/null; true; }"
HEAD=$(curl -sfI "$S3$PREFIX/$ARTIFACT" --max-time 30 | tr -d '\r')
ETAG=$(printf '%s\n' "$HEAD" | awk 'tolower($1)=="etag:"{print $2}' | tr -d '"')
SIZE=$(printf '%s\n' "$HEAD" | awk 'tolower($1)=="content-length:"{print $2}')
if [ -f "$PART" ] && [ "$(cat "$DL/$ARTIFACT.etag" 2>/dev/null)" != "$ETAG" ]; then
  echo "远端 $ARTIFACT 换了（ETag 变化），丢掉上次下了一半的"; rm -f "$PART"
fi
echo "$ETAG" > "$DL/$ARTIFACT.etag"
[ -f "$PART" ] && echo "接着上次续传：已有 $(( $(stat -c %s "$PART") / 1048576 )) MB"
curl -fsSL -C - "$S3$PREFIX/$ARTIFACT" -o "$PART" --max-time 1200
GOT=$(stat -c %s "$PART" 2>/dev/null || echo 0)
if [ -n "$SIZE" ] && [ "$GOT" -lt "$SIZE" ]; then
  # 没下完：半截留着下次接着下，这次什么都不动
  finish "SKIP_DOWNLOAD_PARTIAL ($ARTIFACT 已下 $((GOT / 1048576))/$((SIZE / 1048576)) MB，下次续传)" 0
fi
if [ ! -s "$PART" ]; then echo "下载失败或为空: $ARTIFACT"; finish "ABORT_DOWNLOAD ($ARTIFACT)" 1; fi
mv -f "$PART" "$ZIP" && rm -f "$DL/$ARTIFACT.etag"
unzip -q -o "$ZIP" -d "$STAGE" && rm -f "$ZIP"

# ---- 关键：破坏任何东西之前，先确认源 PHP 代码确实在 ----
if [ ! -f "$STAGE/PHP/composer.lock" ] || [ ! -d "$STAGE/PHP" ]; then
  echo "源包缺 PHP/ 或 composer.lock，中止（未改动任何文件）"
  finish "ABORT_BAD_ARTIFACT ($ARTIFACT)" 1
fi

# ---- 产物版本门：版本号只认包里的 package.json ----
# 早先是部署完单独 curl raw/package.json 当版本号，那是两个独立对象，可以不一致：
# 2026-09 就出现过 raw 已经 1.9.69、code.zip 还停在 8/19 的 1.9.67（发布工作流挂在
# GitHub Release 上，而那两次发版只推了 tag 没建 Release）。设备于是铺了旧代码、
# 盖上新版本号，上报 1.9.69 —— 版本门(本机==远端)从此永远跳过，再也不会自愈。
# 现在：包里没版本号、或不比本机新，就在动任何文件之前拒绝，并把状态上报出去。
ART_VER=$(pkgVer "$STAGE/package.json")
LOCAL_VER=$(pkgVer /var/www/html/package.json)
if [ -z "$ART_VER" ]; then
  echo "源包里没有 package.json，取不到版本号，中止（未改动任何文件）"
  finish "ABORT_NO_VERSION ($ARTIFACT)" 1
fi
if [ "$CMP" = pin ]; then
  # 分支通道：只要求和本机不一样（可以比本机「旧」：从 stable / staging 切到分支时就是这样）
  if [ "$ART_VER" = "${LOCAL_VER:-}" ]; then
    echo "源包版本 $ART_VER 和本机一样，中止（未改动任何文件）"
    finish "ABORT_STALE_ARTIFACT (包 $ART_VER == 本机)" 1
  fi
elif ! verGt "$ART_VER" "${LOCAL_VER:-0.0.0}"; then
  echo "源包版本 $ART_VER 不比本机 ${LOCAL_VER:-未知} 新 —— 产物陈旧或发布流水线没跑，中止（未改动任何文件）"
  finish "ABORT_STALE_ARTIFACT (包 $ART_VER <= 本机 ${LOCAL_VER:-未知})" 1
fi
echo "产物版本门通过：${LOCAL_VER:-未知} -> $ART_VER"

# ---- TSE 门：绝不在有进行中的交易/TSE 签名时部署 ----
# 结账把 START/FINISH 塞进 TseExecuteQueue，后台 worker 签名后置 finished=1（出错也置 1）。
# 信号 = "未完成 且 已到期 且 新鲜(<5min)"：
#   · 开着的桌吃两小时 → 不阻塞（TSE 任务早签完置 1 了）——正是要避免的"活动单门过严"
#   · 正在签/待签的交易 → 有 finished=0 行 → 跳过等下轮 ——正是不能打断的瞬间
#   · 5min 上界 → worker 挂了留下的卡死行不会永久拦住更新
# 紧贴 cp 之前查，消掉"下载几秒期间开了新交易"的竞态。DB 凭据从 config.ini 读（全网 aaden/共享密码）。
CONF=/var/www/html/Resource/config/config.ini
DBCREDS=$(sudoS "php -r '\$c=@parse_ini_file(\"$CONF\",true);\$d=\$c[\"DB\"]??[];echo (\$d[\"host\"]??\"localhost\").\"|\".(\$d[\"user\"]??\"aaden\").\"|\".(\$d[\"password\"]??\"\");'" 2>/dev/null)
DB_HOST="${DBCREDS%%|*}"; _r="${DBCREDS#*|}"; DB_USER="${_r%%|*}"; DB_PASS="${_r#*|}"
sqlq(){ MYSQL_PWD="$DB_PASS" mysql -u"$DB_USER" -h"${DB_HOST:-localhost}" -N -e "$1"; }
DUE=""; TSE_ERR=""; TSE_NOTE=""
if [ -n "$DB_USER" ] && [ -n "$DB_PASS" ]; then
  DUE=$(sqlq "SELECT COUNT(*) FROM aaden.TseExecuteQueue WHERE finished=0 AND nextExecuteTime<=NOW() AND createdAt>=NOW()-INTERVAL 5 MINUTE;" 2>"$STAGE/tse.err")
  TSE_ERR=$(head -c 120 "$STAGE/tse.err" 2>/dev/null | tr '\n' ' ')
  if [ -z "$DUE" ]; then
    # 查询失败要分清两种：TseExecuteQueue 是 1.9.1（2025-03 的 migration）才有的，更老的后端没有这张表，
    # 签名在结账请求里同步做、没有队列可查 —— 这种按空闲处理，否则永远「保守跳过」、永远升不上来
    # （2026-10 的 1407 停在 1.7.913，每天 SKIP_TSE_QUERY）。连不上库之类的其他失败照旧跳过。
    HAS_QUEUE=$(sqlq "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='aaden' AND table_name='TseExecuteQueue';" 2>/dev/null)
    if [ "$HAS_QUEUE" = "0" ]; then
      echo "本机后端没有 TseExecuteQueue 表（1.9.1 之前的版本），没有 TSE 队列可查，按空闲处理"
      DUE=0; TSE_NOTE=" TSE_NO_QUEUE"
    fi
  fi
else
  TSE_ERR="config.ini 里读不到 [DB] 用户名/密码"
fi
if [ -z "$DUE" ]; then
  # 查不到 = 无法确认 TSE 状态 → 对财政数据保守跳过（不部署也不冒险打断签名）。
  # 非破坏性：此刻还没动任何文件。版本停滞会被 fleet-rollout-status 监控发现。
  echo "TSE 队列查询失败，保守跳过本轮（未改动任何文件）: $TSE_ERR"; finish "SKIP_TSE_QUERY${TSE_ERR:+ ($TSE_ERR)}" 0
fi
if [ "$DUE" -gt 0 ]; then
  echo "有进行中的交易/TSE 签名 (due=$DUE)，跳过本轮，等下个周期"; finish "SKIP_TSE_BUSY (due=$DUE)" 0
fi
echo "TSE 队列空闲 (due=0)，可安全部署"

# ---- 备份 PHP 代码(排除 vendor) ----
sudoS "rm -rf $BACKUP && mkdir -p $BACKUP && tar czf $BACKUP/php-code.tgz --exclude='PHP/vendor' -C /var/www/html PHP package.json 2>/dev/null"

# ---- 部署（源已校验，可安全 rm+cp）----
sudoS "rm -rf /var/www/html/PHP/DB/migrations/*"
if [ "$ARTIFACT" = "code.zip" ]; then
  sudoS "cp -rf $STAGE/PHP/. /var/www/html/PHP/"
  # 版本号取包里的那份 —— 与刚铺下去的代码同源同批，不可能对不上。
  # （旧版这里是 curl raw/package.json，正是"旧代码贴新版本号"的来源。）
  sudoS "cp -f $STAGE/package.json /var/www/html/package.json"
else
  sudoS "cp -rf $STAGE/. /var/www/html/"
fi
sudoS "chmod -R 777 /var/www/html/Resource; chmod +x /var/www/html/crontab.sh /var/www/html/PHP/vendor/bin/phinx 2>/dev/null; chown -hR www-data:www-data /var/www/html"

# ---- migrate ----
sudoS "cd /var/www/html/PHP && vendor/bin/phinx migrate" 2>&1 | tail -3

# ---- 健康检查 ----
HEALTHY=1
for ep in "Orders.php?op=getCurrentActiveOrders&lang=DE" "MyVersion.php?op=version"; do
  c=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost/PHP/$ep" --max-time 15)
  echo "  健康检查 $ep -> $c"; [ "$c" = "200" ] || HEALTHY=0
done

# ---- 回滚 or 成功 ----
if [ "$HEALTHY" != "1" ]; then
  echo "健康检查失败 → 回滚"
  sudoS "tar xzf $BACKUP/php-code.tgz -C /var/www/html && chown -hR www-data:www-data /var/www/html"
  sudoS "rm -rf $STAGE"; finish "ROLLED_BACK (回到 ${LOCAL_VER:-未知})" 1
fi
sudoS "rm -rf $STAGE"
finish "DEPLOY_OK $(pkgVer /var/www/html/package.json)${TSE_NOTE}" 0
