#!/usr/bin/env bash
set -Eeuo pipefail

CHANNEL=""
SOURCE_DIR=""
INSTALL_DOCKER=0

BASE_URL="${BTCPROXY_BASE_URL:-https://files.shenqianqian.com/btcproxy/vps}"

ROOT="/opt/btcproxy"
SHARED="$ROOT/shared"
RELEASES="$ROOT/releases"
CURRENT="$ROOT/current"
STATE="/var/lib/btcproxy"

CONTAINER="btcproxy"
RUNTIME_IMAGE="btcproxy-btcproxy:latest"

usage() {
  cat <<'EOF'
用法:
  btcproxy-install stable|test --source-dir DIR [--install-docker]

DIR 必须采用以下结构:
  DIR/shared/config.yaml
  DIR/shared/.env
  DIR/shared/tls/server.crt
  DIR/shared/tls/fullchain.pem
  DIR/shared/tls/privkey.pem

可选恢复:
  DIR/state/capacity.db
  DIR/state/pool-routes.json

说明:
  --install-docker
      仅在 Debian/Ubuntu 上，当 Docker/Compose 缺失时安装官方 Docker CE。

安全规则:
  - 本工具只用于首次安装。
  - 不生成 .env、token、TLS 私钥或 CA 私钥。
  - 不覆盖不同内容的现有生产配置/状态。
  - 已存在 btcproxy 容器时拒绝执行；已有生产 VPS 请使用 btcproxy-update。
EOF
}

die() {
  echo "ERROR: $*" >&2
  exit 1
}

need_value() {
  [ "$#" -ge 2 ] || die "参数 $1 缺少值"
}

if [ "$#" -eq 0 ]; then
  usage
  exit 2
fi

case "${1:-}" in
  -h|--help)
    usage
    exit 0
    ;;
  stable|test)
    CHANNEL="$1"
    shift
    ;;
  *)
    usage >&2
    die "channel 必须是 stable 或 test"
    ;;
esac

while [ "$#" -gt 0 ]; do
  case "$1" in
    --source-dir)
      need_value "$@"
      SOURCE_DIR="$2"
      shift 2
      ;;
    --install-docker)
      INSTALL_DOCKER=1
      shift
      ;;
    -h|--help)
      usage
      exit 0
      ;;
    *)
      die "未知参数: $1"
      ;;
  esac
done

[ -n "$SOURCE_DIR" ] || die "必须指定 --source-dir DIR"

if [ "$(id -u)" != "0" ]; then
  die "必须使用 root 执行"
fi

[ -d "$SOURCE_DIR" ] || die "source dir 不存在: $SOURCE_DIR"
SOURCE_DIR="$(readlink -f "$SOURCE_DIR")"

SRC_SHARED="$SOURCE_DIR/shared"
SRC_STATE="$SOURCE_DIR/state"

for f in   "$SRC_SHARED/config.yaml"   "$SRC_SHARED/.env"   "$SRC_SHARED/tls/server.crt"   "$SRC_SHARED/tls/fullchain.pem"   "$SRC_SHARED/tls/privkey.pem"
do
  [ -f "$f" ] || die "缺少 bootstrap 文件: $f"
done

for cmd in curl python3 sha256sum tar awk openssl readlink cmp install; do
  command -v "$cmd" >/dev/null || die "缺少命令: $cmd"
done

install_docker_debian() {
  [ -r /etc/os-release ] || die "无法识别 Linux 发行版"
  # shellcheck disable=SC1091
  . /etc/os-release

  case "${ID:-}" in
    debian|ubuntu) ;;
    *) die "--install-docker 仅支持 Debian/Ubuntu，当前 ID=${ID:-unknown}" ;;
  esac

  command -v apt-get >/dev/null || die "找不到 apt-get"

  echo
  echo "===== 安装 Docker 官方仓库 ====="

  apt-get update
  apt-get install -y ca-certificates curl gnupg

  install -m 0755 -d /etc/apt/keyrings
  curl -fsSL "https://download.docker.com/linux/$ID/gpg" -o /etc/apt/keyrings/docker.asc
  chmod a+r /etc/apt/keyrings/docker.asc

  local arch codename
  arch="$(dpkg --print-architecture)"
  codename="${VERSION_CODENAME:-}"
  [ -n "$codename" ] || die "无法取得 VERSION_CODENAME"

  printf '%s\n'     "deb [arch=$arch signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/$ID $codename stable"     > /etc/apt/sources.list.d/docker.list

  apt-get update
  apt-get install -y     docker-ce     docker-ce-cli     containerd.io     docker-buildx-plugin     docker-compose-plugin

  systemctl enable --now docker
}

docker_ready() {
  command -v docker >/dev/null 2>&1 &&
  docker version >/dev/null 2>&1 &&
  docker compose version >/dev/null 2>&1
}

if ! docker_ready; then
  if [ "$INSTALL_DOCKER" = "1" ]; then
    install_docker_debian
  else
    die "Docker 或 Docker Compose 不可用。请先安装 Docker，或重新执行并加入 --install-docker"
  fi
fi

docker_ready || die "Docker 安装后仍不可用"

if docker ps -a --format '{{.Names}}' | grep -Fxq "$CONTAINER"; then
  die "已存在 btcproxy 容器；首次安装器拒绝覆盖。已有环境请使用 btcproxy-update"
fi

case "$(uname -m)" in
  x86_64|amd64) ARCH="amd64" ;;
  aarch64|arm64) ARCH="arm64" ;;
  *) die "不支持的 CPU 架构: $(uname -m)" ;;
esac

validate_tls_pair() {
  local cert_hash key_hash
  cert_hash="$(
    openssl x509 -in "$SRC_SHARED/tls/server.crt" -pubkey -noout       | openssl pkey -pubin -outform DER 2>/dev/null       | sha256sum | awk '{print $1}'
  )"
  key_hash="$(
    openssl pkey -in "$SRC_SHARED/tls/privkey.pem" -pubout -outform DER 2>/dev/null       | sha256sum | awk '{print $1}'
  )"

  [ -n "$cert_hash" ] || die "无法读取 server.crt 公钥"
  [ "$cert_hash" = "$key_hash" ] || die "server.crt 与 privkey.pem 不匹配"
}

validate_state_db() {
  local db="$SRC_STATE/capacity.db"
  [ -f "$db" ] || return 0

  python3 - "$db" <<'PY'
import sqlite3
import sys

db = sqlite3.connect(f"file:{sys.argv[1]}?mode=ro", uri=True)
result = db.execute("PRAGMA integrity_check").fetchone()[0]
db.close()
if result != "ok":
    raise SystemExit(f"capacity.db integrity_check failed: {result}")
print("capacity.db integrity_check = ok")
PY
}

validate_pool_routes() {
  local routes="$SRC_STATE/pool-routes.json"
  [ -f "$routes" ] || return 0

  python3 - "$routes" <<'PY'
import json
import sys

with open(sys.argv[1], encoding="utf-8") as f:
    data = json.load(f)

if not isinstance(data, dict):
    raise SystemExit("pool-routes.json root must be an object")

schema = data.get("schema")
if schema not in (1, 2):
    raise SystemExit(f"unsupported pool-routes schema: {schema!r}")

overrides = data.get("overrides", {})
if not isinstance(overrides, dict):
    raise SystemExit("pool-routes overrides must be an object")

print(f"pool-routes.json schema = {schema} : ok")
PY
}

copy_file_safe() {
  local src="$1"
  local dst="$2"
  local mode="$3"

  if [ -e "$dst" ]; then
    cmp -s "$src" "$dst" || die "拒绝覆盖不同内容的现有文件: $dst"
    chmod "$mode" "$dst"
    echo "已有相同文件，保留: $dst"
    return
  fi

  install -D -m "$mode" "$src" "$dst"
}

validate_tls_pair
validate_state_db
validate_pool_routes

TMP="$(mktemp -d)"

cleanup() {
  rm -rf "$TMP"
}
trap cleanup EXIT

echo
echo "========================================"
echo " BTCProxy First Installer"
echo " Channel: $CHANNEL"
echo " Source : $SOURCE_DIR"
echo " Arch   : $ARCH"
echo "========================================"

echo
echo "===== 1. 获取 manifest ====="

curl -fsSL --retry 3 "$BASE_URL/$CHANNEL/manifest.json" -o "$TMP/manifest.json"

mapfile -t META < <(
python3 - "$TMP/manifest.json" <<'PY'
import json
import sys

d = json.load(open(sys.argv[1], encoding="utf-8"))
print(d["version"])
print(d["commit"])
print(d["release"])
PY
)

VERSION="${META[0]}"
COMMIT="${META[1]}"
RELEASE_URL="${META[2]}"

[[ "$VERSION" =~ ^[0-9A-Za-z._-]+$ ]] || die "非法版本号: $VERSION"
[[ "$COMMIT" =~ ^[0-9a-f]{40}$ ]] || die "非法 commit: $COMMIT"

echo "目标版本 : $VERSION"
echo "目标 Commit: $COMMIT"

echo
echo "===== 2. 获取 release.json ====="

curl -fsSL --retry 3 "$RELEASE_URL" -o "$TMP/release.json"

mapfile -t ART < <(
python3 - "$TMP/release.json" "$ARCH" <<'PY'
import json
import sys

d = json.load(open(sys.argv[1], encoding="utf-8"))
arch = "linux-" + sys.argv[2]
a = d["artifacts"][arch]
print(d["version"])
print(d["commit"])
print(a["filename"])
print(a["sha256"])
PY
)

RELEASE_VERSION="${ART[0]}"
RELEASE_COMMIT="${ART[1]}"
FILE="${ART[2]}"
SHA="${ART[3]}"

[ "$RELEASE_VERSION" = "$VERSION" ] || die "manifest 与 release.json 版本不一致"
[ "$RELEASE_COMMIT" = "$COMMIT" ] || die "manifest 与 release.json commit 不一致"
[[ "$SHA" =~ ^[0-9a-f]{64}$ ]] || die "非法 artifact SHA256"

DOWNLOAD_URL="${RELEASE_URL%/release.json}/$FILE"

echo
echo "===== 3. 下载并校验 release ====="

curl -fL --retry 3 "$DOWNLOAD_URL" -o "$TMP/$FILE"
printf '%s  %s\n' "$SHA" "$TMP/$FILE" | sha256sum -c -

echo
echo "===== 4. 检查压缩包路径安全 ====="

python3 - "$TMP/$FILE" <<'PY'
import sys
import tarfile
from pathlib import PurePosixPath

with tarfile.open(sys.argv[1], "r:gz") as tf:
    for m in tf.getmembers():
        p = PurePosixPath(m.name)
        if p.is_absolute() or ".." in p.parts:
            raise SystemExit("unsafe archive path: " + m.name)

print("ARCHIVE_PATH_OK")
PY

validate_binary() {
  local bin="$1"
  local expect_version="$2"
  local expect_commit="$3"

  [ -x "$bin" ] || return 1

  local got_version got_commit
  got_version="$("$bin" -version 2>/dev/null | awk -F= '$1=="version"{print $2}')"
  got_commit="$("$bin" -version 2>/dev/null | awk -F= '$1=="commit"{print $2}')"

  [ "$got_version" = "$expect_version" ] &&
  [ "$got_commit" = "$expect_commit" ]
}

echo
echo "===== 5. 准备 immutable release ====="

mkdir -p "$RELEASES"
TARGET="$RELEASES/$VERSION"

if [ -d "$TARGET" ]; then
  validate_binary "$TARGET/btcproxy" "$VERSION" "$COMMIT"     || die "现有 release 目录身份不匹配: $TARGET"
  echo "已有正确 release，复用: $TARGET"
else
  STAGE="$RELEASES/.incoming-$VERSION-$$"
  rm -rf "$STAGE"
  mkdir -p "$STAGE"

  tar -xzf "$TMP/$FILE" -C "$STAGE"

  (
    cd "$STAGE"
    sha256sum -c btcproxy.sha256
  )

  validate_binary "$STAGE/btcproxy" "$VERSION" "$COMMIT"     || die "解压后的二进制版本/commit 不正确"

  mv "$STAGE" "$TARGET"
fi

echo
echo "===== 6. 安装生产 shared / state ====="

mkdir -p "$SHARED/tls" "$STATE"
chmod 700 "$SHARED" "$SHARED/tls" "$STATE"

copy_file_safe "$SRC_SHARED/config.yaml" "$SHARED/config.yaml" 600
copy_file_safe "$SRC_SHARED/.env" "$SHARED/.env" 600
copy_file_safe "$SRC_SHARED/tls/privkey.pem" "$SHARED/tls/privkey.pem" 600
copy_file_safe "$SRC_SHARED/tls/server.crt" "$SHARED/tls/server.crt" 644
copy_file_safe "$SRC_SHARED/tls/fullchain.pem" "$SHARED/tls/fullchain.pem" 644

if [ -f "$SRC_STATE/capacity.db" ]; then
  copy_file_safe "$SRC_STATE/capacity.db" "$STATE/capacity.db" 600
fi

if [ -f "$SRC_STATE/pool-routes.json" ]; then
  copy_file_safe "$SRC_STATE/pool-routes.json" "$STATE/pool-routes.json" 600
fi

echo
echo "===== 7. 配置 runtime links ====="

ln -sfn ../../shared/config.yaml "$TARGET/config.yaml"
ln -sfn ../../shared/.env "$TARGET/.env"
rm -rf "$TARGET/tls"
ln -s ../../shared/tls "$TARGET/tls"
cp -f "$TARGET/docker-compose.example.yml" "$TARGET/docker-compose.yml"

if [ -e "$CURRENT" ] || [ -L "$CURRENT" ]; then
  CURRENT_REAL="$(readlink -f "$CURRENT" 2>/dev/null || true)"
  [ "$CURRENT_REAL" = "$TARGET" ] || die "current 已指向其它 release: $CURRENT_REAL"
else
  ln -s "releases/$VERSION" "$CURRENT"
fi

echo "current -> $(readlink -f "$CURRENT")"

echo
echo "===== 8. 检查 Compose ====="

(
  cd "$TARGET"
  docker compose -p btcproxy config >/dev/null
)
echo "COMPOSE_CONFIG_OK"

echo
echo "===== 9. 构建隔离 candidate image ====="

BUILD_CONTEXT="$TMP/docker-build"
mkdir -p "$BUILD_CONTEXT"
cp "$TARGET/btcproxy" "$BUILD_CONTEXT/btcproxy"
cp "$TARGET/Dockerfile" "$BUILD_CONTEXT/Dockerfile"

CANDIDATE_IMAGE="btcproxy-install-candidate:$VERSION"

docker build   --no-cache   --progress=plain   -t "$CANDIDATE_IMAGE"   "$BUILD_CONTEXT"

IMAGE_INFO="$(docker run --rm --entrypoint /app/btcproxy "$CANDIDATE_IMAGE" -version)"
echo "$IMAGE_INFO"

IMAGE_VERSION="$(printf '%s\n' "$IMAGE_INFO" | awk -F= '$1=="version"{print $2}')"
IMAGE_COMMIT="$(printf '%s\n' "$IMAGE_INFO" | awk -F= '$1=="commit"{print $2}')"

[ "$IMAGE_VERSION" = "$VERSION" ] || die "candidate image version 不匹配"
[ "$IMAGE_COMMIT" = "$COMMIT" ] || die "candidate image commit 不匹配"

echo "CANDIDATE_IMAGE_OK"

echo
echo "===== 10. 启动首次生产容器 ====="

docker tag "$CANDIDATE_IMAGE" "$RUNTIME_IMAGE"

if ! (
  cd "$TARGET"
  docker compose -p btcproxy up -d --no-build
); then
  docker rm -f "$CONTAINER" >/dev/null 2>&1 || true
  die "首次启动失败；shared/state/release 已保留供排查"
fi

echo
echo "===== 11. Runtime 身份检查 ====="

OK=0
for _ in $(seq 1 30); do
  RUNNING="$(docker inspect -f '{{.State.Running}}' "$CONTAINER" 2>/dev/null || true)"
  ACTUAL_VERSION="$(docker exec "$CONTAINER" /app/btcproxy -version 2>/dev/null | awk -F= '$1=="version"{print $2}' || true)"
  ACTUAL_COMMIT="$(docker exec "$CONTAINER" /app/btcproxy -version 2>/dev/null | awk -F= '$1=="commit"{print $2}' || true)"

  if [ "$RUNNING" = "true" ] &&
     [ "$ACTUAL_VERSION" = "$VERSION" ] &&
     [ "$ACTUAL_COMMIT" = "$COMMIT" ]; then
    OK=1
    break
  fi

  sleep 1
done

if [ "$OK" != "1" ]; then
  docker logs --tail 100 "$CONTAINER" || true
  docker rm -f "$CONTAINER" >/dev/null 2>&1 || true
  die "runtime 身份/运行状态检查失败；配置和状态文件未删除"
fi

echo
echo "===== 12. Web listener 检查 ====="

HTTP_CODE="$(curl -sS -o "$TMP/http.out" -w '%{http_code}' http://127.0.0.1:8080/api/version || true)"

case "$HTTP_CODE" in
  200|401)
    echo "Web API HTTP $HTTP_CODE : OK"
    ;;
  *)
    docker logs --tail 100 "$CONTAINER" || true
    docker rm -f "$CONTAINER" >/dev/null 2>&1 || true
    die "Web API HTTP $HTTP_CODE；首次安装容器已移除，配置和状态文件保留"
    ;;
esac

echo
echo "===== 13. 安装运维命令 ====="

SCRIPT_PATH="$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null || true)"

if [ -n "$SCRIPT_PATH" ] && [ -f "$SCRIPT_PATH" ]; then
  install -m 755 "$SCRIPT_PATH" /usr/local/sbin/btcproxy-install
  echo "btcproxy-install installed: /usr/local/sbin/btcproxy-install"

  SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)"
  if [ -f "$SCRIPT_DIR/btcproxy-update" ]; then
    install -m 755 "$SCRIPT_DIR/btcproxy-update" /usr/local/sbin/btcproxy-update
    echo "btcproxy-update installed: /usr/local/sbin/btcproxy-update"
  else
    echo "NOTICE: 同目录未发现 btcproxy-update；BTCProxy 已正常安装，但 updater 尚未安装"
  fi
else
  echo "NOTICE: 当前执行方式没有可复制的 installer 实体文件"
  echo "NOTICE: BTCProxy 已正常安装，但 btcproxy-install / btcproxy-update 尚未安装到 /usr/local/sbin"
fi

echo
echo "========================================"
echo " BTCProxy 首次安装成功"
echo "========================================"
echo
echo "Channel : $CHANNEL"
echo "Version : $VERSION"
echo "Commit  : $COMMIT"
echo "Current : $(readlink -f "$CURRENT")"
echo

docker exec "$CONTAINER" /app/btcproxy -version

echo
echo "===== Docker ====="
(
  cd "$TARGET"
  docker compose -p btcproxy ps
)

echo
echo "下一步：确认 N1 重连、矿池路由和 accepted shares。"
