#!/usr/bin/env bash
# Integrity Guard 一键安装器 V1.0
#
# 用法：
#   curl -fsSL https://<host>/integrity-guard/install.sh | bash
#   bash install.sh --package ./ig-1.0.0.tar.gz --manifest ./manifest.json --sig ./manifest.sig
#   bash install.sh --rollback
#
# 信任链（顺序不可颠倒）：
#   1) 用内嵌公钥验 manifest.json 的原始字节签名
#   2) 比对 tar.gz 的 SHA256 与 manifest 中的 package_sha256
#   3) 任一步失败立即退出，绝不落盘解包

set -euo pipefail

PREFIX="${IG_PREFIX:-/opt/integrity-guard}"
STATE_DIR="${IG_STATE:-/var/lib/ig}"
CONF_DIR="${IG_CONFDIR:-/etc/ig}"
SERVICE_NAME="integrity-guard"
DEFAULT_BASE_PORT=8790
ASSUME_YES=0
LOCAL_PKG=""
LOCAL_MANIFEST=""
LOCAL_SIG=""
DO_ROLLBACK=0
VERIFY_ONLY=0
PKG_URL="${IG_PKG_URL:-}"

while [ $# -gt 0 ]; do
  case "$1" in
    --yes|-y) ASSUME_YES=1; shift ;;
    --verify-only) VERIFY_ONLY=1; shift ;;
    --package) LOCAL_PKG="$2"; shift 2 ;;
    --manifest) LOCAL_MANIFEST="$2"; shift 2 ;;
    --sig) LOCAL_SIG="$2"; shift 2 ;;
    --rollback) DO_ROLLBACK=1; shift ;;
    --prefix) PREFIX="$2"; shift 2 ;;
    --state) STATE_DIR="$2"; shift 2 ;;
    --help|-h) sed -n '2,20p' "$0"; exit 0 ;;
    *) echo "未知参数: $1" >&2; exit 2 ;;
  esac
done

say() { printf '\033[1m[IG]\033[0m %s\n' "$*"; }
die() { printf '\033[1;31m[IG] 失败:\033[0m %s\n' "$*" >&2; exit 1; }

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

# ---------------------------------------------------------------------------
# 回滚：还原上一版目录并重启
# ---------------------------------------------------------------------------
if [ "$DO_ROLLBACK" = "1" ]; then
  [ -d "${PREFIX}.prev" ] || die "没有可回滚的备份: ${PREFIX}.prev"
  say "回滚 ${PREFIX} <- ${PREFIX}.prev"
  rm -rf "${PREFIX}"
  mv "${PREFIX}.prev" "${PREFIX}"
  if command -v systemctl >/dev/null 2>&1 && [ -f "/etc/systemd/system/${SERVICE_NAME}.service" ]; then
    systemctl daemon-reload
    systemctl restart "${SERVICE_NAME}" || true
  else
    pkill -f "ig_service.py" >/dev/null 2>&1 || true
    nohup python3 "${PREFIX}/ig/ig_service.py" >"${STATE_DIR}/service.log" 2>&1 &
  fi
  say "回滚完成（注意：回滚不还原授权状态文件，license.json/trial_state.json 始终保留）"
  exit 0
fi

need python3

# ---------------------------------------------------------------------------
# 准备安装素材
# ---------------------------------------------------------------------------
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT

if [ -n "$LOCAL_PKG" ]; then
  MANIFEST="$LOCAL_MANIFEST"; SIG="$LOCAL_SIG"; PKG="$LOCAL_PKG"
  [ -f "$MANIFEST" ] || die "manifest 不存在: $MANIFEST"
  [ -f "$SIG" ] || die "签名文件不存在: $SIG"
  [ -f "$PKG" ] || die "安装包不存在: $PKG"
else
  [ -n "$PKG_URL" ] || die "未提供 --package，也未设置 IG_PKG_URL"
  BASE_URL="$(dirname "$PKG_URL")"
  say "下载清单与包：$BASE_URL"
  curl -fsSL "$BASE_URL/manifest.json" -o "$TMP/manifest.json"
  curl -fsSL "$BASE_URL/manifest.sig" -o "$TMP/manifest.sig"
  curl -fsSL "$PKG_URL" -o "$TMP/pkg.tar.gz"
  MANIFEST="$TMP/manifest.json"; SIG="$TMP/manifest.sig"; PKG="$TMP/pkg.tar.gz"
fi

# ---------------------------------------------------------------------------
# 1) 验签 manifest 原始字节（内嵌公钥 + 纯 Python Ed25519）
# ---------------------------------------------------------------------------
PUBKEY_PEM='-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAdjlHBWGR1py2UrEINcbaS5ymG9Wvu7g6mkHVIKS5w5g=
-----END PUBLIC KEY-----'

export IG_PUBKEY_PEM="$PUBKEY_PEM"
say "校验清单签名"
python3 - "$MANIFEST" "$SIG" <<'PYEOF' || die "清单签名校验失败（清单可能被替换）"
import base64, hashlib, os, sys

P = 2**255 - 19
L = 2**252 + 27742317777372353535851937790883648493
D = -121665 * pow(121666, P - 2, P) % P
I = pow(2, (P - 1) // 4, P)

def inv(x): return pow(x, P - 2, P)
def xrec(y):
    xx = (y * y - 1) * inv(D * y * y + 1)
    x = pow(xx, (P + 3) // 8, P)
    if (x * x - xx) % P != 0: x = (x * I) % P
    if x % 2 != 0: x = P - x
    return x

BY = 4 * inv(5) % P
BX = xrec(BY)
B = (BX % P, BY % P)

def add(p1, p2):
    x1, y1 = p1; x2, y2 = p2
    return ((x1*y2 + x2*y1) * inv(1 + D*x1*x2*y1*y2) % P,
            (y1*y2 + x1*x2) * inv(1 - D*x1*x2*y1*y2) % P)

def mul(pt, e):
    q = (0, 1); p = pt
    while e > 0:
        if e & 1: q = add(q, p)
        p = add(p, p); e >>= 1
    return q

def enc(pt):
    x, y = pt
    return (y | ((x & 1) << 255)).to_bytes(32, 'little')

def dec(s):
    v = int.from_bytes(s, 'little')
    xb = v >> 255; y = v & ((1 << 255) - 1)
    x = xrec(y)
    if x & 1 != xb: x = P - x
    if (-x*x + y*y - 1 - D*x*x*y*y) % P != 0: raise ValueError('not_on_curve')
    return (x, y)

def bit(h, i): return (h[i // 8] >> (i % 8)) & 1

def hint(m):
    h = hashlib.sha512(m).digest()
    return sum(2**i * bit(h, i) for i in range(512))

def verify(sig, msg, pk):
    if len(sig) != 64 or len(pk) != 32: return False
    try:
        rp = dec(sig[:32]); pkp = dec(pk)
    except ValueError:
        return False
    s = int.from_bytes(sig[32:], 'little')
    if s >= L: return False
    h = hint(sig[:32] + pk + msg)
    return enc(mul(B, s)) == enc(add(rp, mul(pkp, h)))

def pem_der(t):
    body = ''.join(l.strip() for l in t.strip().splitlines() if not l.startswith('-----'))
    return base64.b64decode(body)

man_path, sig_path = sys.argv[1], sys.argv[2]
pk = pem_der(os.environ['IG_PUBKEY_PEM'])[-32:]
manifest_bytes = open(man_path, 'rb').read()
sig = open(sig_path, 'rb').read()
if len(sig) >= 64 and sig[:64].decode('ascii', 'ignore').isprintable():
    try:
        sig = base64.b64decode(sig)
    except Exception:
        pass
if not verify(sig, manifest_bytes, pk):
    print('SIGNATURE_INVALID')
    sys.exit(1)
print('SIGNATURE_OK')
PYEOF

# ---------------------------------------------------------------------------
# 2) 比对包哈希
# ---------------------------------------------------------------------------
EXPECT="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1],encoding="utf-8"))["package_sha256"])' "$MANIFEST")"
ACTUAL="$(python3 -c 'import hashlib,sys;print(hashlib.sha256(open(sys.argv[1],"rb").read()).hexdigest())' "$PKG")"
[ "$EXPECT" = "$ACTUAL" ] || die "包哈希不符（期望 ${EXPECT:0:12}…，实际 ${ACTUAL:0:12}…）"
say "包哈希一致 ${ACTUAL:0:12}…"

if [ "$VERIFY_ONLY" = "1" ]; then
  say "仅验证模式：信任链校验通过（未安装任何文件）"
  exit 0
fi

# ---------------------------------------------------------------------------
# 3) 解包
# ---------------------------------------------------------------------------
mkdir -p "$TMP/unpack"
tar -xzf "$PKG" -C "$TMP/unpack"
[ -f "$TMP/unpack/ig/ig_core.py" ] || die "包结构不符：缺 ig/ig_core.py"
[ -f "$TMP/unpack/ig/ig_service.py" ] || die "包结构不符：缺 ig/ig_service.py"

# ---------------------------------------------------------------------------
# 4) 覆盖安装：备份旧目录，保留授权/试用状态
# ---------------------------------------------------------------------------
mkdir -p "$STATE_DIR" "$CONF_DIR"
if [ -d "$PREFIX" ]; then
  say "备份旧版本 -> ${PREFIX}.prev"
  rm -rf "${PREFIX}.prev"
  cp -a "$PREFIX" "${PREFIX}.prev"
fi
# 状态文件不在 PREFIX 内，但仍显式确认一次，避免误删
for f in license.json trial_state.json license_pub.pem; do
  if [ -f "$STATE_DIR/$f" ]; then
    say "保留状态文件 $STATE_DIR/$f（重装不重置试用/授权）"
  fi
done

rm -rf "$PREFIX"
mkdir -p "$PREFIX"
cp -a "$TMP/unpack/ig" "$PREFIX/ig"
cp -f "$TMP/unpack/README.md" "$PREFIX/README.md" 2>/dev/null || true

# ---------------------------------------------------------------------------
# 5) 端口探测（不写死，避免"装成功起不来"）
# ---------------------------------------------------------------------------
PORT="$(python3 - "$DEFAULT_BASE_PORT" <<'PYEOF'
import socket, sys
base = int(sys.argv[1])
for p in range(base, base + 40):
    s = socket.socket()
    try:
        s.bind(("127.0.0.1", p))
        s.close()
        print(p)
        break
    except OSError:
        s.close()
else:
    sys.exit(1)
PYEOF
)" || die "找不到空闲端口"

# ---------------------------------------------------------------------------
# 6) 写配置（只在缺失时初始化，已有则保留 watch 列表）
# ---------------------------------------------------------------------------
if [ ! -f "$CONF_DIR/ig.json" ]; then
  cat > "$CONF_DIR/ig.json" <<EOF
{
 "paths": {
  "registry": "$STATE_DIR/efuse_registry.json",
  "baseline": "$STATE_DIR/baseline.json",
  "privkey": "$STATE_DIR/ig_ed25519.pem"
 },
 "watch": []
}
EOF
  chmod 600 "$CONF_DIR/ig.json"
  say "已生成配置 $CONF_DIR/ig.json（watch 为空，需自行登记要守护的文件）"
fi

# 首次安装：生成密钥对
if [ ! -f "$STATE_DIR/ig_ed25519.pem" ]; then
  IG_BASE="$STATE_DIR" IG_CONF="$CONF_DIR/ig.json" python3 "$PREFIX/ig/ig_cli.py" init >/dev/null
  say "已生成签名密钥（0600）：$STATE_DIR/ig_ed25519.pem"
fi

# ---------------------------------------------------------------------------
# 7) 注册为常驻服务（有 systemd 用之，否则后台进程并明示限制）
# ---------------------------------------------------------------------------
if command -v systemctl >/dev/null 2>&1 && [ -d /run/systemd/system ]; then
  # 硬化选项须与实际路径相容，否则 systemd 会在命名空间阶段直接 226/NAMESPACE 起不来：
  #   PrivateTmp 会遮蔽宿主 /tmp，路径落在 /tmp、/var/tmp 时不能用；
  #   ProtectHome 会遮蔽 /home、/root，路径落在其下时降为只读。
  PRIVATE_TMP="PrivateTmp=yes"
  case "$STATE_DIR" in
    /tmp/*|/var/tmp/*) PRIVATE_TMP="" ;;
  esac
  PROTECT_HOME="ProtectHome=yes"
  case "$PREFIX $STATE_DIR" in
    *"/root/"*|*"/home/"*) PROTECT_HOME="ProtectHome=read-only" ;;
  esac
  cat > "/etc/systemd/system/${SERVICE_NAME}.service" <<EOF
[Unit]
Description=Integrity Guard (read-only asset integrity service)
After=network.target

[Service]
Type=simple
User=root
Environment=IG_BASE=$STATE_DIR
Environment=IG_CONF=$CONF_DIR/ig.json
Environment=IG_PORT=$PORT
ExecStart=/usr/bin/env python3 $PREFIX/ig/ig_service.py
Restart=always
RestartSec=3
NoNewPrivileges=yes
$PRIVATE_TMP
ProtectSystem=strict
$PROTECT_HOME
ReadWritePaths=$STATE_DIR
UMask=0077
LimitCORE=0

[Install]
WantedBy=multi-user.target
EOF
  systemctl daemon-reload
  systemctl enable "${SERVICE_NAME}" >/dev/null 2>&1 || true
  systemctl restart "${SERVICE_NAME}"
else
  say "无 systemd：降级为后台进程（重启后需重跑，非常驻）"
  pkill -f "ig_service.py" >/dev/null 2>&1 || true
  IG_BASE="$STATE_DIR" IG_CONF="$CONF_DIR/ig.json" IG_PORT="$PORT" \
    nohup python3 "$PREFIX/ig/ig_service.py" >"$STATE_DIR/service.log" 2>&1 &
fi

# ---------------------------------------------------------------------------
# 8) 健康轮询（不以 systemctl 返回 0 作为成功判据）
# ---------------------------------------------------------------------------
say "等待服务就绪 …"
OK=0
for i in $(seq 1 20); do
  if python3 -c "
import json,sys,urllib.request
try:
    r=urllib.request.urlopen('http://127.0.0.1:$PORT/api/ig/health',timeout=2)
    print(json.load(r)['ok'])
except Exception:
    sys.exit(1)
" >/dev/null 2>&1; then OK=1; break; fi
  sleep 1
done
[ "$OK" = "1" ] || die "服务未就绪，查看日志: journalctl -u ${SERVICE_NAME} -n 50 或 $STATE_DIR/service.log"

# ---------------------------------------------------------------------------
# 9) 汇总
# ---------------------------------------------------------------------------
python3 - "$PORT" "$STATE_DIR" "$PREFIX" "$CONF_DIR" <<'PYEOF'
import json, sys, urllib.request
port, state, prefix, confdir = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]
health = json.load(urllib.request.urlopen('http://127.0.0.1:%s/api/ig/health' % port, timeout=3))
lic = json.load(urllib.request.urlopen('http://127.0.0.1:%s/api/ig/status' % port, timeout=3))['license']
print()
print("安装完成")
print("  服务地址   http://127.0.0.1:%s" % port)
print("  状态目录   %s" % state)
print("  授权状态   %s（valid=%s，剩余 %s 天）" % (lic.get('mode'), lic.get('valid'), lic.get('days_left')))
print("  健康检查   curl -s http://127.0.0.1:%s/api/ig/health" % port)
print()
print("下一步：登记要守护的文件")
print("  IG_BASE=%s IG_CONF=%s/ig.json python3 %s/ig/ig_cli.py add /path/to/file IMMUTABLE" % (state, confdir, prefix))
print("  IG_BASE=%s IG_CONF=%s/ig.json python3 %s/ig/ig_cli.py verify" % (state, confdir, prefix))
PYEOF
