#!/usr/bin/env bash
set -Eeuo pipefail
umask 077
export LC_ALL=C.UTF-8
echo 'DGX Spark 远程接入安装器：将开启 SSH、安装 Tailscale，并设置开机自启。'
echo '请保持电源和网络连接。已有文件和系统密码不会被覆盖。'
die() { printf '\n[未完成 %s] %s\n下一步：%s\n' "$1" "$2" "$3"; exit 1; }
[[ "$(uname -s)" == Linux ]] || die E_OS '请在 DGX Spark 的 Linux 终端执行。' '不要在 Windows 或 Mac 上运行。'
for x in python3 openssl tar sha256sum; do
 command -v "$x" >/dev/null || die E_TOOL "系统缺少 $x。" '拍下这条提示并发回；请勿自行重装系统。'
done
if [[ $EUID -ne 0 ]]; then
 command -v sudo >/dev/null || die E_SUDO '找不到 sudo 管理员工具。' '请使用首次开机时创建的管理员账户。'
 echo '[1/6] 确认管理员权限：请输入这台机器的登录密码，输入时不会显示字符。'
 sudo -v || die E_SUDO '未取得管理员权限，密码可能不正确或此账户不是管理员。' '重新运行，输入本机登录密码。'
fi
TASK_TMP=$(mktemp -d)
trap 'rm -rf "$TASK_TMP"' EXIT
trap 'echo "[已中断] 可以重新执行同一条命令继续。"; exit 130' INT TERM
if [[ -n "${SPARK_CODE_FILE:-}" && -r "$SPARK_CODE_FILE" ]]; then
 cp "$SPARK_CODE_FILE" "$TASK_TMP/code"
else
 echo '[2/6] 请输入随安装命令提供的安装码（输入时不显示字符）：'
 IFS= read -r -s INSTALL_CODE </dev/tty || die E_CODE '未能读取安装码。' '在有键盘的终端里重新执行。'
 echo
 printf '%s' "$INSTALL_CODE" > "$TASK_TMP/code"
 unset INSTALL_CODE
fi
echo '[3/6] 自动选择下载节点并校验文件；首次约需下载 70 MB。'
python3 - "$TASK_TMP" <<'PY'
import concurrent.futures, hashlib, json, os, pathlib, shutil, socket, subprocess, sys, tarfile, time, urllib.request, urllib.error
dest = pathlib.Path(sys.argv[1])
manifest = {'config': {'name': 'deployment.enc', 'sha256': '769c04f7690d336b8ca7bdd17beba1b375a9089b3f230408dea313870b049b13'}, 'installer': {'name': 'install.py', 'sha256': 'e1788373de8c857e7cb053481a5992a0b72555ada459221c17e52e8524804dab'}, 'arm64': {'tailscale': {'name': 'tailscale_1.104.7_arm64.tgz', 'sha256': 'd6bed2720310b328b861f943d7b4016c5358a9bad60575eb51e8143b79068484'}, 'proxy': {'name': 'sing-box-1.14.4-linux-arm64.tar.gz', 'sha256': '436441f76ee2f7fe21b5b2d426d3cb261a480455a704865789ad533e4273ec26'}}, 'amd64': {'tailscale': {'name': 'tailscale_1.104.7_amd64.tgz', 'sha256': 'e56693ff74a64f2591b7e819c280635a100aae1ddfb0ef058fcd052b7cd8d16c'}, 'proxy': {'name': 'sing-box-1.14.4-linux-amd64.tar.gz', 'sha256': '699a1b510aa4396b2176d32d1181920399ebf1e267835da6f3b598d0f7c24968'}}}
mirrors = ['https://spark.sysu-sam.com', 'https://cloud.20020606.xyz/spark-bootstrap', 'https://monitor.43-134-78-71.sslip.io/spark-bootstrap', 'http://104.168.255.41:18089']
arch = {'aarch64': 'arm64', 'arm64': 'arm64', 'x86_64': 'amd64'}.get(os.uname().machine)
def stop(code, text, action):
    print('\n[未完成 '+code+'] '+text+'\n下一步：'+action, flush=True); sys.exit(1)
if arch is None: stop('E_ARCH','不支持此处理器架构。','确认正在 DGX Spark 上运行，并拍照发回。')
# Disable inherited proxy settings: the fallback proxy is installed separately.
opener=urllib.request.build_opener(urllib.request.ProxyHandler({}))
def fetch(item):
    name, expected = item['name'], item['sha256']
    for base in mirrors:
        host=urllib.request.urlparse(base).hostname if hasattr(urllib.request,'urlparse') else base.split('/')[2]
        print('下载 '+name+'，节点 '+host, flush=True)
        target=dest/name
        try:
            req=urllib.request.Request(base+'/'+name,headers={'User-Agent':'SparkBootstrap/1'})
            started=time.monotonic()
            with opener.open(req,timeout=15) as response, target.open('wb') as out:
                h=hashlib.sha256()
                while True:
                    chunk=response.read(1024*1024)
                    if not chunk: break
                    out.write(chunk); h.update(chunk)
                    if time.monotonic()-started > 240: raise TimeoutError('download deadline')
            if h.hexdigest()!=expected:
                print('该节点文件校验不符，自动尝试下一节点。',flush=True); continue
            print(name+' 下载及校验通过。',flush=True);return target
        except urllib.error.HTTPError as e:
            print('节点返回 HTTP '+str(e.code)+'，自动切换。',flush=True)
        except (OSError,ValueError,urllib.error.URLError) as e:
            reason='网络超时或连接失败'
            if 'CERTIFICATE' in str(e): reason='证书校验失败，请留意机器时间'
            if 'Name or service' in str(e) or 'resolve' in str(e): reason='域名解析失败'
            print(reason+'，自动尝试下一节点。',flush=True)
    stop('E_DOWNLOAD','所有下载节点均失败（可能未联网、网络需网页登录、DNS错误或证书时间错误）。','确认能打开普通网页；检查日期和时间；把以上提示拍照发回。')
if shutil.disk_usage(dest).free < 500*1024**2: stop('E_DISK','临时目录剩余空间不足 500 MB。','请联系负责人清理磁盘，不要随意删除系统文件。')
files=[manifest['config'],manifest['installer'],manifest[arch]['tailscale'],manifest[arch]['proxy']]
for item in files: fetch(item)
print('[4/6] 校验安装码并解锁私有部署配置。',flush=True)
result=subprocess.run(['openssl','enc','-d','-aes-256-cbc','-pbkdf2','-iter','200000','-in',str(dest/manifest['config']['name']),'-out',str(dest/'config.json'),'-pass','file:'+str(dest/'code')],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
(dest/'code').unlink(missing_ok=True)
if result.returncode: stop('E_CODE','安装码不正确，或复制时多了空格。','重新执行同一条命令，完整输入安装码。')
try:
    config=json.loads((dest/'config.json').read_text())
except (ValueError,UnicodeError): stop('E_CODE','安装码不正确。','重新执行并核对安装码。')
if time.time()>config['expires_at']: stop('E_EXPIRED','本次安装包已超过使用期限。','联系负责人更新安装包；无需重装系统。')
for key in ['tailscale','proxy']:
    archive=dest/manifest[arch][key]['name']
    with tarfile.open(archive,'r:gz') as tf:
        for member in tf.getmembers():
            basename=pathlib.PurePosixPath(member.name).name
            if basename not in ('tailscale','tailscaled','sing-box') or not member.isfile(): continue
            with tf.extractfile(member) as src,(dest/basename).open('wb') as out: shutil.copyfileobj(src,out)
            (dest/basename).chmod(0o700)
print('[5/6] 安装文件已备齐，即将配置系统服务。',flush=True)
PY
if [[ $EUID -eq 0 ]]; then
 python3 "$TASK_TMP/install.py" --assets-dir "$TASK_TMP"
else
 sudo python3 "$TASK_TMP/install.py" --assets-dir "$TASK_TMP"
fi
