#!/usr/bin/env bash
# Geto v2.1.0
# Ubuntu / Debian / CentOS 安装面板（协议在网页里创建）
#
#   bash <(curl -fsSL https://vpn.kinomotom.com/panel/install.sh)
#   装好后: geto   或浏览器打开面板
#
# 仅安装到你自己的 VPS。必须用 bash <(curl ...)，不要用 curl | bash。

set -o pipefail
set +o histexpand
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH"
export PYTHONIOENCODING=utf-8
export PYTHONUTF8=1

NP_VERSION="2.1.0"
NODE_HOME="/usr/local/geto"
GETO_FILE_BASE="https://vpn.kinomotom.com/panel"
GETO_FILES=(
    "requirements.json"
    "panel/__init__.py"
    "panel/__main__.py"
    "panel/auth.py"
    "panel/cli.py"
    "panel/db.py"
    "panel/paths.py"
    "panel/server.py"
    "panel/core/__init__.py"
    "panel/core/certs.py"
    "panel/core/clash.py"
    "panel/core/firewall.py"
    "panel/core/hosts.py"
    "panel/core/hy2.py"
    "panel/core/net.py"
    "panel/core/protocols.py"
    "panel/core/shareparse.py"
    "panel/core/system.py"
    "panel/core/traffic.py"
    "panel/core/util.py"
    "panel/core/xray.py"
    "panel/web/index.html"
    "panel/web/assets/app.css"
    "panel/web/assets/app.js"
    "panel/web/assets/antd/antd.min.css"
)

RED='\033[0;31m'
GREEN='\033[1;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'

ok()   { echo -e "${GREEN}[OK]${NC} $*" >&2; }
info() { echo -e "${CYAN}[INFO]${NC} $*" >&2; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*" >&2; }
err()  { echo -e "${RED}[ERR]${NC} $*" >&2; }
die()  { err "$*"; exit 1; }

need_root() { [[ $(id -u) -eq 0 ]] || die "请使用 root 运行：sudo -i 后再执行"; }

need_tty() {
    if [[ ! -t 0 ]]; then
        cat >&2 <<'EOF'
请使用进程替换运行（不要 curl | bash，否则无法交互）：
  bash <(curl -fsSL https://vpn.kinomotom.com/panel/install.sh)
EOF
        exit 1
    fi
}

ask() {
    local prompt="$1" default="${2-}" input
    if [[ -n "$default" ]]; then
        read -rp "$prompt [$default]: " input
        printf '%s' "${input:-$default}"
    else
        read -rp "$prompt: " input
        printf '%s' "$input"
    fi
}

rand_alnum() { tr -dc 'A-Za-z0-9' </dev/urandom 2>/dev/null | head -c "${1:-16}"; }

valid_port() {
    local p="$1"
    [[ "$p" =~ ^[0-9]+$ ]] && (( p >= 1 && p <= 65535 )) && [[ "$p" != "22" ]]
}

detect_os() {
    OS_ID=""; OS_VER=""; OS_PRETTY=""; PKG=""
    if [[ -f /etc/os-release ]]; then
        OS_ID="$(. /etc/os-release; printf '%s' "${ID:-unknown}")"
        OS_VER="$(. /etc/os-release; printf '%s' "${VERSION_ID:-}")"
        OS_PRETTY="$(. /etc/os-release; printf '%s' "${PRETTY_NAME:-}")"
    fi
    [[ -n "$OS_PRETTY" ]] || OS_PRETTY="${OS_ID:-unknown} ${OS_VER}"
    case "$OS_ID" in
        ubuntu|debian) PKG="apt" ;;
        centos|rhel|rocky|almalinux|fedora|ol|anolis|opencloudos|alinux) PKG="yum" ;;
        *)
            if command -v apt-get >/dev/null 2>&1; then PKG="apt"
            elif command -v dnf >/dev/null 2>&1 || command -v yum >/dev/null 2>&1; then PKG="yum"
            else die "不支持的系统，仅支持 Ubuntu / Debian / CentOS 系"
            fi
            ;;
    esac
}

install_deps() {
    if [[ "$PKG" == "apt" ]]; then
        export DEBIAN_FRONTEND=noninteractive
        apt-get update -y
        apt-get install -y curl wget ca-certificates unzip openssl python3 python3-venv python3-pip \
            jq qrencode iproute2 net-tools tar gzip coreutils procps
    else
        local yumcmd="yum"
        command -v dnf >/dev/null 2>&1 && yumcmd="dnf"
        $yumcmd install -y curl wget ca-certificates unzip openssl python3 python3-pip tar gzip \
            iproute net-tools procps-ng || true
        $yumcmd install -y epel-release || true
        $yumcmd install -y jq qrencode python3 || true
    fi
    command -v python3 >/dev/null || die "python3 安装失败"
    command -v openssl >/dev/null || die "openssl 安装失败"
    command -v curl >/dev/null || die "curl 安装失败"
    mkdir -p "$NODE_HOME" "$NODE_HOME/meta" "$NODE_HOME/clash" "$NODE_HOME/data" "$NODE_HOME/certs" /var/log/xray
    ok "依赖就绪"
}

_np_realpath() { readlink -f "$1" 2>/dev/null || printf '%s' "$1"; }

_np_is_self_script() {
    local f="$1" n
    [[ -f "$f" && -s "$f" ]] || return 1
    n="$(wc -c < "$f" | tr -d '[:space:]')"
    [[ "$n" =~ ^[0-9]+$ ]] && (( n > 4000 )) || return 1
    grep -q '^#!/usr/bin/env bash' "$f" 2>/dev/null || return 1
    grep -q 'NP_VERSION=' "$f" 2>/dev/null || return 1
}

copy_panel_from() {
    local src="$1"
    [[ -d "$src/panel" ]] || return 1
    mkdir -p "$NODE_HOME"
    rm -rf "$NODE_HOME/panel"
    cp -a "$src/panel" "$NODE_HOME/panel"
    [[ -f "$src/requirements.json" ]] && cp -f "$src/requirements.json" "$NODE_HOME/requirements.json"
    if _np_is_self_script "$src/install.sh"; then
        local dest="$NODE_HOME/install.sh" dest_real src_real
        dest_real="$(_np_realpath "$dest")"
        src_real="$(_np_realpath "$src/install.sh")"
        if [[ "$src_real" != "$dest_real" ]]; then
            cp -f "$src/install.sh" "$dest"
            chmod +x "$dest"
        fi
    fi
    return 0
}

download_from_base() {
    local dest="$1" rel missing=0 f dir
    mkdir -p "$dest"
    for f in "${GETO_FILES[@]}"; do
        dir="$(dirname "$dest/$f")"
        mkdir -p "$dir"
        if curl -fsSL --max-time 30 "${GETO_FILE_BASE}/${f}" -o "$dest/$f"; then
            continue
        fi
        if [[ "$f" == "requirements.json" ]]; then
            warn "未找到 requirements.json，将使用内置依赖列表"
            rm -f "$dest/$f"
            continue
        fi
        err "下载失败：${GETO_FILE_BASE}/${f}"
        missing=1
    done
    curl -fsSL --max-time 30 "${GETO_FILE_BASE}" -o "$dest/install.sh" >/dev/null 2>&1 \
        || curl -fsSL --max-time 30 "${GETO_FILE_BASE}/install.sh" -o "$dest/install.sh" >/dev/null 2>&1 || true
    [[ "$missing" == 0 && -f "$dest/panel/server.py" && -f "$dest/panel/web/index.html" && -f "$dest/panel/web/assets/antd/antd.min.css" ]]
}

fetch_panel_files() {
    local src="${BASH_SOURCE[0]:-}" src_dir tmp
    if [[ -n "$src" && -f "$src" ]]; then
        src_dir="$(cd "$(dirname "$src")" 2>/dev/null && pwd)"
        if copy_panel_from "$src_dir"; then
            ok "已从本地复制面板文件"
            return 0
        fi
    fi
    tmp="$(mktemp -d)"
    if download_from_base "$tmp"; then
        copy_panel_from "$tmp"
        rm -rf "$tmp"
        ok "已从 ${GETO_FILE_BASE} 下载面板文件"
        return 0
    fi
    rm -rf "$tmp"
    die "未能获取面板文件，请确认 ${GETO_FILE_BASE}/panel/ 可以访问"
}

persist_self() {
    mkdir -p "$NODE_HOME"
    local dest="$NODE_HOME/install.sh"
    local src="${BASH_SOURCE[0]:-}"
    if [[ -n "$src" && -f "$src" && -s "$src" ]] && _np_is_self_script "$src"; then
        if [[ "$(_np_realpath "$src")" != "$(_np_realpath "$dest")" ]]; then
            cp -f "$src" "$dest"
        fi
        chmod +x "$dest"
    fi
}

write_geto_wrapper() {
    cat > /usr/local/bin/geto <<EOF
#!/usr/bin/env bash
export NODE_HOME="$NODE_HOME"
export PYTHONPATH="$NODE_HOME"
export PYTHONIOENCODING=utf-8
if [[ ! -x "$NODE_HOME/venv/bin/python3" ]]; then
    echo "面板尚未安装完整，请重新执行 install.sh"
    exit 1
fi
exec "$NODE_HOME/venv/bin/python3" -m panel.cli "\$@"
EOF
    chmod +x /usr/local/bin/geto
    ln -sf /usr/local/bin/geto /usr/local/bin/np
}

enable_bbr() {
    mkdir -p /etc/sysctl.d
    if sysctl -w net.ipv4.tcp_congestion_control=bbr >/dev/null 2>&1; then
        printf 'net.ipv4.tcp_congestion_control=bbr\n' > /etc/sysctl.d/99-bbr.conf
        ok "BBR 已启用"
    else
        printf 'net.ipv4.tcp_congestion_control=bbr\n' > /etc/sysctl.d/99-bbr.conf
        warn "当前内核可能不支持 BBR，已写入 /etc/sysctl.d/99-bbr.conf"
    fi
}

enable_net_tune() {
    mkdir -p /etc/sysctl.d
    cat > /etc/sysctl.d/99-geto-net.conf <<'EOF'
net.core.default.qdisc=fq
net.ipv4.tcp_slow_start_after_idle=0
net.ipv4.tcp_mtu_probing=1
net.ipv4.tcp_tw_reuse=1
net.ipv4.tcp_fin_timeout=15
net.ipv4.tcp_keepalive_time=600
net.ipv4.tcp_keepalive_intvl=30
net.ipv4.tcp_keepalive_probes=3
net.ipv4.tcp_max_syn_backlog=8192
net.core.somaxconn=4096
net.core.netdev_max_backlog=16384
net.core.rmem_max=16777216
net.core.wmem_max=16777216
net.ipv4.tcp_rmem=4096 87380 16777216
net.ipv4.tcp_wmem=4096 65536 16777216
net.ipv4.tcp_notsent_lowat=16384
net.ipv4.ip_local_port_range=1024 65535
net.ipv4.conf.all.rp_filter=2
net.ipv4.conf.default.rp_filter=2
EOF
    sysctl -p /etc/sysctl.d/99-geto-net.conf >/dev/null 2>&1 || true
    ok "已应用网络参数"
}

open_firewall() {
    local port="$1" proto="${2:-tcp}"
    if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -qi active; then
        ufw allow "${port}/${proto}" >/dev/null 2>&1 || true
    fi
    if command -v firewall-cmd >/dev/null 2>&1 && firewall-cmd --state 2>/dev/null | grep -qi running; then
        firewall-cmd --add-port="${port}/${proto}" >/dev/null 2>&1 || true
        firewall-cmd --permanent --add-port="${port}/${proto}" >/dev/null 2>&1 || true
        firewall-cmd --reload >/dev/null 2>&1 || true
    fi
    if command -v iptables >/dev/null 2>&1; then
        iptables -C INPUT -p "$proto" --dport "$port" -j ACCEPT >/dev/null 2>&1 || \
            iptables -I INPUT -p "$proto" --dport "$port" -j ACCEPT >/dev/null 2>&1 || true
    fi
}

setup_venv() {
    if [[ ! -x "$NODE_HOME/venv/bin/python3" ]]; then
        python3 -m venv "$NODE_HOME/venv" || die "创建 Python 虚拟环境失败，请安装 python3-venv"
    fi
    "$NODE_HOME/venv/bin/pip" install -U pip >/dev/null
    if [[ -f "$NODE_HOME/requirements.json" ]]; then
        if ! "$NODE_HOME/venv/bin/python3" - "$NODE_HOME/requirements.json" <<'PY'
import json, subprocess, sys
from pathlib import Path
raw = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
if isinstance(raw, dict):
    raw = raw.get("packages") or raw.get("dependencies") or []
pkgs = [str(x).strip() for x in raw if str(x).strip()]
if not pkgs:
    sys.exit(2)
raise SystemExit(subprocess.call([sys.executable, "-m", "pip", "install", *pkgs]))
PY
        then
            die "安装 Python 依赖失败"
        fi
    else
        "$NODE_HOME/venv/bin/pip" install fastapi uvicorn python-multipart || die "安装 Python 依赖失败"
    fi
    ok "Python 环境就绪"
}

bootstrap_admin() {
    local user="$1" pass="$2" port="$3"
    NP_ADMIN_USER="$user" NP_ADMIN_PASS="$pass" NP_ADMIN_PORT="$port" \
    NODE_HOME="$NODE_HOME" PYTHONPATH="$NODE_HOME" "$NODE_HOME/venv/bin/python3" - <<'PY'
import os
from panel import db
db.bootstrap_admin(os.environ["NP_ADMIN_USER"], os.environ["NP_ADMIN_PASS"], int(os.environ["NP_ADMIN_PORT"]))
print("admin ready")
PY
}

start_cores() {
    info "正在安装并启动 Xray / Hysteria2（首次需下载，可能稍等）"
    if NODE_HOME="$NODE_HOME" PYTHONPATH="$NODE_HOME" "$NODE_HOME/venv/bin/python3" - <<'PY'
from panel.core.system import ensure_cores
ensure_cores()
print("cores ready")
PY
    then
        ok "Xray / Hysteria2 已启动"
    else
        warn "核心启动未完成，稍后打开面板时会自动再试"
    fi
}

write_service() {
    cat > /etc/systemd/system/np-panel.service <<EOF
[Unit]
Description=Geto Panel
After=network.target

[Service]
Type=simple
WorkingDirectory=$NODE_HOME
Environment=NODE_HOME=$NODE_HOME
Environment=PYTHONPATH=$NODE_HOME
Environment=PYTHONIOENCODING=utf-8
Environment=HOME=/root
ExecStart=$NODE_HOME/venv/bin/python3 -m panel.server
Restart=always
RestartSec=2
LimitNOFILE=1048576

[Install]
WantedBy=multi-user.target
EOF
    systemctl daemon-reload
    systemctl enable np-panel >/dev/null 2>&1 || true
    systemctl restart np-panel
    sleep 1
    if systemctl is-active --quiet np-panel; then
        ok "面板服务已启动"
    else
        journalctl -u np-panel -n 40 --no-pager
        die "面板启动失败"
    fi
}

get_public_ip() {
    local ip="" src
    for src in https://api.ipify.org https://ipv4.icanhazip.com https://ifconfig.me/ip https://ip.sb; do
        ip="$(curl -4 -fsS --max-time 6 "$src" 2>/dev/null | tr -d '[:space:]')"
        [[ "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] && { echo "$ip"; return 0; }
    done
    ip="$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src"){print $(i+1); exit}}')"
    echo "${ip:-127.0.0.1}"
}

panel_installed() {
    [[ -f "$NODE_HOME/panel/server.py" && -x "$NODE_HOME/venv/bin/python3" && -f "$NODE_HOME/data/panel.db" ]]
}

do_install() {
    detect_os
    echo
    echo -e "${BOLD}Geto 面板安装 v${NP_VERSION}${NC}"
    echo "系统：${OS_PRETTY:-unknown}"
    echo
    local port user pass
    while true; do
        port="$(ask "面板端口" "2053")"
        if valid_port "$port"; then
            break
        fi
        warn "端口不合法（1-65535，不要用 22）"
    done
    user="$(ask "面板用户名" "admin")"
    [[ -n "$user" ]] || user="admin"
    pass="$(ask "面板密码（回车随机）" "")"
    if [[ -z "$pass" ]]; then
        pass="$(rand_alnum 16)"
        info "已生成随机密码"
    fi
    if (( ${#pass} < 6 )); then
        die "密码至少 6 位"
    fi

    install_deps
    persist_self
    fetch_panel_files
    setup_venv
    bootstrap_admin "$user" "$pass" "$port"
    enable_bbr
    enable_net_tune
    write_geto_wrapper
    write_service
    open_firewall "$port" tcp
    start_cores

    local ip
    ip="$(get_public_ip)"
    echo
    echo "=================================================="
    echo -e "${GREEN}面板已安装${NC}"
    echo "  地址   : http://${ip}:${port}"
    echo "  用户名 : ${user}"
    echo "  密码   : ${pass}"
    echo "=================================================="
    echo "协议请在面板里创建。输入 geto 可查看地址和卸载面板"
    echo
}

run_cli() {
    export NODE_HOME PYTHONPATH="$NODE_HOME" PYTHONIOENCODING=utf-8
    exec "$NODE_HOME/venv/bin/python3" -m panel.cli
}

main() {
    need_root
    need_tty
    mkdir -p "$NODE_HOME"
    if [[ "${1-}" == "--reinstall" ]]; then
        do_install
        exit 0
    fi
    if panel_installed; then
        run_cli
    else
        do_install
    fi
}

main "$@"
