#!/bin/bash
# Panel 一键安装 v3:优先下载预编译成品(约 30 秒),失败自动回落源码构建。
# 预检/全程日志/下载重试/完成信息面板借鉴宝塔安装器。
# 用法(目标 Linux 服务器 root 执行):
#   curl -fsSL https://download.liliang.xyz/quick_start.sh | bash
#   (或从已装面板下载:PANEL_DL_BASE=http://面板IP:18080 curl -fsSL $PANEL_DL_BASE/api/v1/downloads/quick_start.sh | bash)
# 可选环境变量:
#   PANEL_PORT=xxxx        面板端口(缺省 18080,仅全新安装生效)
#   ADMIN_PASSWORD=xxx     初始管理员密码(缺省随机生成并打印;仅全新安装生效)
#   PANEL_DL_BASE=...      额外的面板自托管成品源(优先尝试)
#   PANEL_COS_BASE=...     覆盖下载站源(缺省 https://download.liliang.xyz;设为 none 跳过;http 回退与官方面板 API 始终兜底)
#   FORCE_SOURCE=1         强制源码构建
#   PANEL_REPORT_URL=...   安装统计上报地址(缺省打官方面板;设为 none 关闭上报)
set -e

REPO_URL="${REPO_URL:-https://gitee.com/liliang13/panel.git}"
SRC=/opt/panel-src
DEPLOY=/opt/panel
GOARCH=$(uname -m); [ "$GOARCH" = "x86_64" ] && GOARCH=amd64
[ "$GOARCH" = "aarch64" ] && GOARCH=arm64
case "$GOARCH" in amd64|arm64) ;; *) echo "不支持的架构: $(uname -m)"; exit 1 ;; esac

[ "$(id -u)" = "0" ] || { echo "请以 root 执行: curl -fsSL https://download.liliang.xyz/quick_start.sh | sudo bash"; exit 1; }
HAS_SYSTEMD=1
command -v systemctl >/dev/null 2>&1 || HAS_SYSTEMD=0
PANEL_PORT="${PANEL_PORT:-18080}"
IS_UPGRADE=0
[ -f "$DEPLOY/config.json" ] && IS_UPGRADE=1
SECONDS=0

# 全程记录到日志,失败时提示带上日志求助
INSTALL_LOG=/tmp/panel-install.log
exec > >(tee -a "$INSTALL_LOG") 2>&1
trap 'report failed "$(tail -c 2000 "$INSTALL_LOG" 2>/dev/null)"; echo ""; echo "✗ 安装中断/失败。完整日志: $INSTALL_LOG(反馈请附: tail -50 $INSTALL_LOG)"' ERR INT TERM
echo "== Panel 一键安装开始 $(date '+%F %T') =="

# ---------- 安装量/错误上报(打到面板自己的统计接口;借鉴宝塔,不外传第三方;失败静默) ----------
. /etc/os-release 2>/dev/null || true
OS_ID="${ID:-}"; OS_VER="${VERSION_ID:-}"
EVENT=install; [ "$IS_UPGRADE" = "1" ] && EVENT=upgrade
INSTALL_SRC=""
if [ -n "${PANEL_REPORT_URL:-}" ]; then REPORT_URL="$PANEL_REPORT_URL"
elif [ -n "${PANEL_DL_BASE:-}" ]; then REPORT_URL="$PANEL_DL_BASE/api/v1/stats/install"
else REPORT_URL="http://179.255.119.31:18080/api/v1/stats/install"
fi
report() { # report <status> [error_tail]
    case "${REPORT_URL:-}" in ""|none) return 0 ;; esac
    command -v curl >/dev/null 2>&1 || return 0
    curl -sS -m 5 -o /dev/null "$REPORT_URL" \
        --data-urlencode "status=${1:-installing}" \
        --data-urlencode "event=$EVENT" \
        --data-urlencode "source=$INSTALL_SRC" \
        --data-urlencode "arch=$GOARCH" \
        --data-urlencode "os_id=$OS_ID" \
        --data-urlencode "os_ver=$OS_VER" \
        --data-urlencode "port=$PANEL_PORT" \
        --data-urlencode "error=${2:-}" 2>/dev/null || true
}
die() { echo "✗ $1"; report failed "preflight: $1"; exit 1; }

# ---------- 预检(内存/磁盘/端口,不满足最小条件直接拒绝) ----------
if [ "$(uname -s)" != "Linux" ]; then
    die "当前不是 Linux 系统,无法安装"
fi
MEM_TOTAL=$(free -m 2>/dev/null | grep Mem | awk '{print $2}')
if [ -n "${MEM_TOTAL}" ]; then
    if [ "${MEM_TOTAL}" -lt 300 ]; then
        die "内存仅 ${MEM_TOTAL}MB,无法安装(至少 300MB,建议 ≥1GB)"
    fi
    [ "${MEM_TOTAL}" -lt 800 ] && echo "⚠ 内存 ${MEM_TOTAL}MB 偏小:面板本身可运行,跑重型应用会吃力"
fi
DISK_AVAIL=$(df -Pk "$(dirname "$DEPLOY")" 2>/dev/null | awk 'NR==2{print $4}')
if [ -n "${DISK_AVAIL}" ] && [ "${DISK_AVAIL}" -lt 512000 ]; then
    df -h "$(dirname "$DEPLOY")" 2>/dev/null || true
    die "磁盘剩余空间不足 500MB,无法安装"
fi
if [ "$IS_UPGRADE" = "0" ]; then
    if (exec 3<>/dev/tcp/127.0.0.1/"$PANEL_PORT") 2>/dev/null; then
        die "端口 $PANEL_PORT 已被占用。请先停掉占用进程,或换端口安装: PANEL_PORT=8443 curl -fsSL https://download.liliang.xyz/quick_start.sh | bash"
    fi
    echo "预检通过: 内存 ${MEM_TOTAL:-?}MB / 磁盘可用 $((DISK_AVAIL/1024))MB / 端口 $PANEL_PORT 空闲"
else
    echo "检测到已有安装,按升级处理(数据与账号保留)"
fi
if command -v getenforce >/dev/null 2>&1 && [ "$(getenforce 2>/dev/null)" = "Enforcing" ]; then
    echo "⚠ SELinux 为 Enforcing:若装完后端口无法访问,可 setenforce 0 或配置对应策略"
fi
report installing

fetch() { # fetch <url> <out> <最小字节数>:curl/wget 二选一,速度过低自动断开,重试 2 次
    local url=$1 out=$2 min=${3:-1} i
    for i in 1 2; do
        rm -f "$out"
        if command -v curl >/dev/null 2>&1; then
            curl -fsSL --connect-timeout 10 --speed-limit 10240 --speed-time 15 -m 600 "$url" -o "$out" || { sleep 2; continue; }
        elif command -v wget >/dev/null 2>&1; then
            wget -q -T 10 --tries=1 "$url" -O "$out" || { sleep 2; continue; }
        else
            echo "  ✗ 缺少 curl 或 wget"; return 1
        fi
        [ -f "$out" ] && [ "$(wc -c < "$out")" -ge "$min" ] && return 0
    done
    return 1
}

# ---------- 方式 A:预编译成品(依次尝试:面板自托管 → 下载站 https → 下载站 http → 官方面板 API)----------
try_prebuilt() {
    TARBALL=/tmp/panel-release.tar.gz
    CANDIDATES=""
    [ -n "$PANEL_DL_BASE" ] && CANDIDATES="$PANEL_DL_BASE/api/v1/downloads"
    DL_BASE="${PANEL_COS_BASE:-https://download.liliang.xyz}"
    [ "$DL_BASE" != "none" ] && CANDIDATES="$CANDIDATES $DL_BASE http://download.liliang.xyz"
    CANDIDATES="$CANDIDATES http://179.255.119.31:18080/api/v1/downloads"
    for BASE in $CANDIDATES; do
        echo "== 下载预编译成品($BASE)=="
        fetch "$BASE/panel-linux-$GOARCH.tar.gz" "$TARBALL" 1048576 || { echo "  下载失败,换下一源"; continue; }
        SUM=$(fetch "$BASE/panel-linux-$GOARCH.tar.gz.sha256" /tmp/panel-release.sha256 10 && cat /tmp/panel-release.sha256 || true)
        if [ -n "$SUM" ]; then
            echo "$SUM  $TARBALL" | sha256sum -c - >/dev/null 2>&1 || { echo "  ✗ SHA256 校验失败,换下一源"; continue; }
            echo "  SHA256 校验通过"
        fi
        mkdir -p "$DEPLOY"
        tar -C "$DEPLOY" -xzf "$TARBALL"
        chmod +x "$DEPLOY/bin/"panel-* 2>/dev/null || true
        echo "  成品解压完成"
        INSTALL_SRC=prebuilt
        return 0
    done
    return 1
}

# ---------- apt/dpkg 轻量解锁(停自动更新定时器+等锁;不抄宝塔的强杀逻辑) ----------
apt_unblock() {
    command -v apt-get >/dev/null 2>&1 || return 0
    systemctl stop apt-daily.timer apt-daily-upgrade.timer unattended-upgrades 2>/dev/null || true
    if command -v fuser >/dev/null 2>&1; then
        local i=0
        while fuser /var/lib/dpkg/lock-frontend /var/lib/dpkg/lock /var/lib/apt/lists/lock /var/cache/apt/archives/lock >/dev/null 2>&1; do
            [ "$i" -ge 60 ] && { echo "⚠ apt 锁等待超时,继续尝试安装"; break; }
            [ "$i" -eq 0 ] && echo "apt/dpkg 正在使用中,等待锁释放..."
            sleep 3; i=$((i+3))
        done
    fi
    dpkg --configure -a 2>/dev/null || true
}

# ---------- apt/yum 源优化(借鉴宝塔但更保守:仅源码构建时调用,当前源实测慢/坏才换,备份可回滚) ----------
probe_host() { # <host> -> "<http_code> <毫秒>";curl 缺失视为不可用
    if ! command -v curl >/dev/null 2>&1; then echo "000 9999"; return; fi
    curl -sS --connect-timeout 3 -m 3 -o /dev/null -w '%{http_code} %{time_total}' "https://$1" 2>/dev/null | awk '{printf "%s %d", $1, $2*1000}'
}

swap_apt_mirror() {
    command -v apt-get >/dev/null 2>&1 || return 0
    local srcs="/etc/apt/sources.list"
    [ -f "$srcs" ] || srcs=$(ls /etc/apt/sources.list.d/*.sources 2>/dev/null | head -1)
    [ -n "$srcs" ] && [ -f "$srcs" ] || return 0
    local cur
    if [ "${srcs##*.}" = "sources" ]; then
        cur=$(grep -m1 '^URIs:' "$srcs" 2>/dev/null | sed -E 's|^URIs: *https?://([^/ ]+).*$|\1|')
    else
        cur=$(grep -m1 -E '^deb(-src)? ' "$srcs" 2>/dev/null | sed -E 's|^[^ ]+ https?://([^/ ]+).*$|\1|')
    fi
    [ -n "$cur" ] || return 0
    local chk code ms
    chk=$(probe_host "$cur"); code=${chk%% *}; ms=${chk##* }
    if { [ "$code" = "200" ] || [ "$code" = "301" ]; } && [ "${ms:-9999}" -lt 500 ] 2>/dev/null; then
        return 0   # 当前源又通又快(<500ms),不动用户的源
    fi
    local m best="" bms=99999 code2 ms2
    for m in mirrors.cloud.tencent.com mirrors.aliyun.com mirrors.ustc.edu.cn repo.huaweicloud.com mirrors.163.com mirrors.tuna.tsinghua.edu.cn; do
        chk=$(probe_host "$m"); code2=${chk%% *}; ms2=${chk##* }
        case "$code2" in 200|301) ;; *) continue ;; esac
        [ "${ms2:-99999}" -lt 150 ] && { best=$m; break; }        # 极快直接采用
        [ "${ms2:-99999}" -lt "$bms" ] && { best=$m; bms=$ms2; }  # 否则记录最快的备用
    done
    [ -n "$best" ] || { echo "  (apt 源慢/不可用,但未找到更快镜像,保留原源)"; return 0; }
    echo "  apt 源 $cur(${code:-无响应}${ms:+, ${ms}ms})较慢,切换到 $best(原文件已备份)"
    local bak="${srcs}.panelbak.$(date +%s)"
    cp -a "$srcs" "$bak" || { echo "  (备份失败,保留原源)"; return 0; }
    # 首选当前主机名,再覆盖常见官方源;deb822(.sources)与老格式(.list)通用
    local hosts="${cur//./\\.}|archive\\.ubuntu\\.com|security\\.ubuntu\\.com|cn\\.archive\\.ubuntu\\.com|deb\\.debian\\.org|security\\.debian\\.org|ftp\\.debian\\.org|mirrors\\.tuna\\.tsinghua\\.edu\\.cn"
    sed -i -E "s#(https?://)($hosts)/#\1$best/#g" "$srcs" || true
    if ! apt-get update -qq 2>/dev/null; then
        echo "  ⚠ 切换后 update 失败,回滚原源"
        local lastbak; lastbak=$(ls -t "${srcs}".panelbak.* 2>/dev/null | head -1)
        [ -n "$lastbak" ] && cp -a "$lastbak" "$srcs"
        apt-get update -qq 2>/dev/null || true
    fi
    return 0
}

fix_centos_vault() {
    [ -d /etc/yum.repos.d ] || return 0
    grep -q "[^#]mirror.centos.org" /etc/yum.repos.d/CentOS-*.repo 2>/dev/null || return 0
    echo "  检测到已停维护的 mirror.centos.org,切换到 vault.epel.cloud"
    cp -a /etc/yum.repos.d "/etc/yum.repos.d.panelbak.$(date +%s)" 2>/dev/null || true
    sed -i -E 's|^mirrorlist|#mirrorlist|; s|^#?baseurl=http://mirror\.centos\.org|baseurl=http://vault.epel.cloud|' /etc/yum.repos.d/CentOS-*.repo 2>/dev/null || true
    return 0
}

# ---------- 方式 B:源码构建(回落) ----------
try_source() {
    echo "== 源码构建(较慢,约 5-10 分钟)=="
    INSTALL_SRC=source
    export DEBIAN_FRONTEND=noninteractive
    PKGS=""
    for c in git node npm curl; do command -v $c >/dev/null || PKGS="$PKGS $( [ $c = node ] && echo nodejs || echo $c)"; done
    if command -v apt-get >/dev/null 2>&1; then
        apt_unblock
        swap_apt_mirror
        [ -n "$PKGS" ] && apt-get update -qq && apt-get install -y -qq $PKGS 2>&1 | tail -1
    elif command -v yum >/dev/null 2>&1; then
        fix_centos_vault
        [ -n "$PKGS" ] && yum install -y -q $PKGS 2>&1 | tail -1
    fi
    export PATH=/usr/local/go/bin:$PATH
    if ! command -v go >/dev/null 2>&1; then
        curl -fsSL -m 300 "https://go.dev/dl/go1.24.5.linux-$GOARCH.tar.gz" -o /tmp/go.tgz \
            || curl -fsSL -m 300 "https://golang.google.cn/dl/go1.24.5.linux-$GOARCH.tar.gz" -o /tmp/go.tgz
        tar -C /usr/local -xzf /tmp/go.tgz
    fi
    go version
    if [ -d "$SRC/.git" ]; then
        cd "$SRC" && git fetch --all -q && git reset --hard origin/main
    else
        git clone -q "$REPO_URL" "$SRC"; cd "$SRC"
    fi
    echo "== 构建前端 =="
    (cd web && { [ -d node_modules ] || npm install --no-audit --no-fund; } && npm run build) 2>&1 | tail -1
    echo "== 构建后端 =="
    mkdir -p "$DEPLOY/bin"
    go build -o "$DEPLOY/bin/panel-core" ./cmd/core
    go build -o "$DEPLOY/bin/panel-agent" ./cmd/agent
    mkdir -p "$DEPLOY/web/dist"
    cp -r web/dist/* "$DEPLOY/web/dist/"
}

if [ "${FORCE_SOURCE:-0}" = "1" ] || ! try_prebuilt; then
    try_source
fi

# ---------- 安装 ----------
mkdir -p "$DEPLOY/data" "$DEPLOY/logs"
if [ ! -f "$DEPLOY/config.json" ] && [ -f "$DEPLOY/config.example.json" ]; then
    sed "s|/opt/panel/data|$DEPLOY/data|" "$DEPLOY/config.example.json" > "$DEPLOY/config.json"
    if [ -n "${PANEL_PORT:-}" ] && [ "${PANEL_PORT}" != "18080" ]; then
        # 只改第一处 http_addr(面板);网关段的 http_addr(:80)不能动
        sed -i -E "0,/\"http_addr\"/s/:[0-9]+\"/:${PANEL_PORT}\"/" "$DEPLOY/config.json"
        echo "面板端口设置为 $PANEL_PORT"
    fi
fi

systemctl stop panel 2>/dev/null || true
if [ "$HAS_SYSTEMD" = "1" ]; then
cat > /etc/systemd/system/panel.service <<UNIT
[Unit]
Description=Panel 运维面板
After=network.target

[Service]
Type=simple
WorkingDirectory=$DEPLOY
Environment=PANEL_ADMIN_PASSWORD=${ADMIN_PASSWORD}
ExecStart=$DEPLOY/bin/panel-core -config $DEPLOY/config.json
Restart=always
RestartSec=3
LimitNOFILE=65535

[Install]
WantedBy=multi-user.target
UNIT
systemctl daemon-reload
systemctl enable --now panel
sleep 2
if systemctl is-active --quiet panel; then echo "✓ 面板已启动"; else echo "✗ 启动异常: journalctl -u panel -n 30"; exit 1; fi
else
    echo "(无 systemd,nohup 启动)"
    pkill -f "panel-core -config" 2>/dev/null || true
    (cd "$DEPLOY" && PANEL_ADMIN_PASSWORD=${ADMIN_PASSWORD} nohup bin/panel-core -config config.json > logs/panel.out 2>&1 &)
    sleep 2
fi

PORT=$(grep -m1 -oE '"http_addr": *":[0-9]*' "$DEPLOY/config.json" 2>/dev/null | grep -oE '[0-9]+' || echo 18080)
# 本机 HTTP 自检(借鉴宝塔:装完 curl 验证服务真的可访问)
HTTP_CODE=""
command -v curl >/dev/null 2>&1 && HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' -m 5 "http://127.0.0.1:${PORT}/" || true)

# 全新安装时把后端生成的随机密码从日志里提出来,直接打印(借鉴宝塔的登录信息面板)
PASS_HINT="journalctl -u panel --no-pager | grep 管理员"
[ "$HAS_SYSTEMD" != "1" ] && PASS_HINT="grep 管理员 $DEPLOY/logs/panel.out"
PASS_LINE=""
if [ "$IS_UPGRADE" = "0" ] && [ -z "${ADMIN_PASSWORD:-}" ]; then
    sleep 1
    if [ "$HAS_SYSTEMD" = "1" ]; then
        PASS_LINE=$(journalctl -u panel --no-pager -n 50 2>/dev/null | grep -m1 管理员 || true)
    fi
    [ -z "$PASS_LINE" ] && PASS_LINE=$(grep -m1 管理员 "$DEPLOY/logs/panel.out" 2>/dev/null || true)
    [ -n "$PASS_LINE" ] && PASS_HINT="$PASS_LINE"
fi
IPS=$(hostname -I 2>/dev/null | awk '{for(i=1;i<=2&&i<=NF;i++) printf "%s%s",(i>1?" 或 ":""),$i}')

echo ""
echo "============================================"
echo "  ✓ Panel 安装完成!(耗时 ${SECONDS}s)"
[ -n "$HTTP_CODE" ] && echo "  本机自检: HTTP $HTTP_CODE (127.0.0.1:$PORT)"
echo "  访问:  http://${IPS:-<本机IP>}:$PORT"
echo "  账号:  admin"
if [ "$IS_UPGRADE" = "1" ]; then
echo "  密码:  升级安装,沿用原密码"
elif [ -n "${ADMIN_PASSWORD:-}" ]; then
echo "  密码:  (你指定的 ADMIN_PASSWORD)"
else
echo "  $PASS_HINT"
fi
echo "  【云服务器】请在安全组放行 $PORT 端口"
echo "  升级: 重跑同一命令即可(数据/配置保留)"
echo "============================================"
report success
