#!/usr/bin/env bash
# ============================================================
# AnyTuck 中继一键安装脚本（兼容 Ubuntu / Debian / CentOS / RHEL / Rocky / Alma / Fedora / openSUSE / Arch）
#
# 两种运行方式：
#   1) 已上传本项目目录：cd 进目录后  sudo bash install.sh
#   2) 远程一键：脚本与项目 tar 包托管在你的站点（如 anytuck-relay.aican.app）：
#        curl -fsSL https://anytuck-relay.aican.app/install.sh | sudo bash
#      脚本会自动下载项目源码到 /opt/anytuck-relay 再安装。
#      可用环境变量覆盖源地址：ANYTUCK_SRC_URL、安装目录：ANYTUCK_INSTALL_DIR。
#
# 完成后：写好 .env、构建并启动容器、安装 `anytuck` 管理命令，并打印客户端连接地址与密码。
# ============================================================
set -euo pipefail

# 远程一键运行时下载项目源码的默认地址与安装目录（可用环境变量覆盖）
SRC_URL="${ANYTUCK_SRC_URL:-https://anytuck-relay.aican.app/anytuck-relay.tar.gz}"
INSTALL_DIR="${ANYTUCK_INSTALL_DIR:-/opt/anytuck-relay}"

PROJECT_DIR=""  # 由 resolveProject 决定（在地目录或下载解压目录）
ENV_FILE=""     # resolveProject 后据 PROJECT_DIR 设定
CONFIG_DIR="/etc/anytuck"
CONFIG_FILE="$CONFIG_DIR/config"

# 运行期变量（camelCase）
osId=""
osName=""
pkgMgr=""
composeFile="docker-compose.yml"
composeProfiles=""
sharedNetwork=""
dnsHint=""

info() { printf '\033[36m%s\033[0m\n' "$*"; }
ok() { printf '\033[32m%s\033[0m\n' "$*"; }
warn() { printf '\033[33m%s\033[0m\n' "$*"; }
err() { printf '\033[31m%s\033[0m\n' "$*" >&2; }

# requireRoot 确认以 root 运行（需要写 /usr/local/bin 与 /etc、装包、控 docker）。
requireRoot() {
	if [[ $EUID -ne 0 ]]; then
		err "请用 root 运行：sudo bash install.sh"
		exit 1
	fi
}

# requireTty 确保有可交互的控制终端。经 `curl | sudo bash` 运行时 stdin 是脚本本身，
# 交互 read 必须从 /dev/tty 读；若连 /dev/tty 都没有则提示改用下载后运行。
requireTty() {
	if [[ ! -e /dev/tty ]]; then
		err "非交互环境无法读取输入。请改为下载后运行："
		err "  curl -fsSL $SRC_URL/../install.sh -o install.sh && sudo bash install.sh"
		exit 1
	fi
}

# detectOs 从 /etc/os-release 读取发行版并选定包管理器。
detectOs() {
	if [[ -r /etc/os-release ]]; then
		# shellcheck disable=SC1091
		. /etc/os-release
		osId="${ID:-}"
		osName="${PRETTY_NAME:-$osId}"
	fi
	for m in apt-get dnf yum zypper pacman; do
		if command -v "$m" >/dev/null 2>&1; then pkgMgr="$m"; break; fi
	done
	info "系统：${osName:-未知}  包管理器：${pkgMgr:-未知}"
	if [[ -z "$pkgMgr" ]]; then
		warn "未识别包管理器，若缺少 curl/docker 需你手动安装。"
	fi
}

# pkgInstall 用检测到的包管理器安装软件包（失败不致命，交由调用方判断）。
pkgInstall() {
	case "$pkgMgr" in
		apt-get) apt-get update -y >/dev/null 2>&1 || true; apt-get install -y "$@" ;;
		dnf) dnf install -y "$@" ;;
		yum) yum install -y "$@" ;;
		zypper) zypper -n install "$@" ;;
		pacman) pacman -Sy --noconfirm "$@" ;;
		*) return 1 ;;
	esac
}

# ensureTool 确保某命令存在，缺失则尝试装对应包。
ensureTool() {
	local bin="$1" pkg="${2:-$1}"
	command -v "$bin" >/dev/null 2>&1 && return 0
	warn "缺少 $bin，尝试安装 $pkg ..."
	pkgInstall "$pkg" >/dev/null 2>&1 || true
	command -v "$bin" >/dev/null 2>&1
}

# checkDocker 确保 Docker 已装、守护进程在跑、且有 compose v2；缺 Docker 询问自动安装。
checkDocker() {
	if ! command -v docker >/dev/null 2>&1; then
		warn "未检测到 Docker。"
		read -rp "是否自动安装 Docker（官方 get.docker.com 脚本）？[Y/n] " ans </dev/tty
		if [[ -z "${ans}" || "${ans,,}" == "y" ]]; then
			ensureTool curl curl || { err "需要 curl 才能安装 Docker，请手动安装 curl 后重试。"; exit 1; }
			curl -fsSL https://get.docker.com | sh || { err "Docker 安装失败，请参考 https://docs.docker.com/engine/install/ 手动安装。"; exit 1; }
		else
			err "请先安装 Docker（https://docs.docker.com/engine/install/）后重试。"
			exit 1
		fi
	fi

	# 守护进程是否在运行（未运行则尝试拉起）
	if ! docker info >/dev/null 2>&1; then
		warn "Docker 守护进程未运行，尝试启动 ..."
		if command -v systemctl >/dev/null 2>&1; then
			systemctl enable --now docker >/dev/null 2>&1 || true
		elif command -v service >/dev/null 2>&1; then
			service docker start >/dev/null 2>&1 || true
		fi
		docker info >/dev/null 2>&1 || { err "Docker 守护进程无法启动，请检查 systemctl status docker。"; exit 1; }
	fi

	# compose v2
	if ! docker compose version >/dev/null 2>&1; then
		warn "未检测到 Docker Compose v2 插件，尝试安装 ..."
		pkgInstall docker-compose-plugin >/dev/null 2>&1 || true
		docker compose version >/dev/null 2>&1 || {
			err "缺少 docker compose v2。请安装 docker-compose-plugin（或升级 Docker）后重试。"
			exit 1
		}
	fi
	ok "Docker 就绪：$(docker --version)"
}

# resolveProject 定位项目源码：就地目录含源码则用之；否则从 SRC_URL 下载解压到 INSTALL_DIR。
resolveProject() {
	local here
	here="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd || true)"
	if [[ -n "$here" && -f "$here/relay/Dockerfile" ]]; then
		PROJECT_DIR="$here"
		info "使用就地项目目录：$PROJECT_DIR"
	else
		info "未在本地发现项目源码，从 $SRC_URL 下载 ..."
		ensureTool curl curl || ensureTool wget wget || { err "需要 curl 或 wget 才能下载项目源码。"; exit 1; }
		ensureTool tar tar || true
		local tmp; tmp="$(mktemp -d)"
		if command -v curl >/dev/null 2>&1; then
			curl -fsSL "$SRC_URL" -o "$tmp/src.tgz" || { err "下载失败：$SRC_URL"; exit 1; }
		else
			wget -qO "$tmp/src.tgz" "$SRC_URL" || { err "下载失败：$SRC_URL"; exit 1; }
		fi
		tar xzf "$tmp/src.tgz" -C "$tmp" || { err "解压失败，请确认 $SRC_URL 是 .tar.gz。"; exit 1; }
		local dockerfile root
		dockerfile="$(find "$tmp" -maxdepth 4 -type f -path '*/relay/Dockerfile' | head -n1)"
		[[ -n "$dockerfile" ]] || { err "包内未找到 relay/Dockerfile，包结构不对。"; exit 1; }
		root="$(cd "$(dirname "$dockerfile")/.." && pwd)"
		mkdir -p "$INSTALL_DIR"
		cp -a "$root/." "$INSTALL_DIR/"
		rm -rf "$tmp"
		PROJECT_DIR="$INSTALL_DIR"
		ok "项目源码已就绪：$PROJECT_DIR"
	fi
	ENV_FILE="$PROJECT_DIR/.env"
}

# genToken 生成 32 字节十六进制随机令牌（优先 openssl，回退 /dev/urandom）。
genToken() {
	if command -v openssl >/dev/null 2>&1; then
		openssl rand -hex 32
	else
		head -c32 /dev/urandom | od -An -tx1 | tr -d ' \n'
	fi
}

# setEnv 更新（或追加）.env 中某个键。
setEnv() {
	local key="$1" val="$2" escaped
	escaped=$(printf '%s' "$val" | sed -e 's/[\\|&]/\\&/g')
	if grep -qE "^${key}=" "$ENV_FILE"; then
		sed -i "s|^${key}=.*|${key}=${escaped}|" "$ENV_FILE"
	else
		printf '%s=%s\n' "$key" "$val" >>"$ENV_FILE"
	fi
}

# ask 读取一个输入项（可带默认值）。始终从 /dev/tty 读，兼容 curl|bash 一键运行。
ask() {
	local prompt="$1" default="${2:-}" var
	if [[ -n "$default" ]]; then
		read -rp "$prompt [$default]: " var </dev/tty; var="${var:-$default}"
	else
		read -rp "$prompt: " var </dev/tty
	fi
	printf '%s' "$var"
}

# listInterfaces 列出候选网卡（排除 lo），辅助动态 IPv6 选择。
listInterfaces() {
	if command -v ip >/dev/null 2>&1; then
		ip -o link show 2>/dev/null | awk -F': ' '{print $2}' | grep -v '^lo$' | tr '\n' ' '
	fi
}

# portInUse 判断某 TCP 端口是否已被监听（ss / netstat / lsof 依次回退）。
portInUse() {
	local p="$1"
	if command -v ss >/dev/null 2>&1; then
		ss -ltn 2>/dev/null | awk '{print $4}' | grep -qE "[:.]$p\$" && return 0
	elif command -v netstat >/dev/null 2>&1; then
		netstat -ltn 2>/dev/null | awk '{print $4}' | grep -qE "[:.]$p\$" && return 0
	elif command -v lsof >/dev/null 2>&1; then
		lsof -iTCP:"$p" -sTCP:LISTEN >/dev/null 2>&1 && return 0
	fi
	return 1
}

# whoHoldsPort 打印占用端口的进程信息（尽力而为）。
whoHoldsPort() {
	local p="$1"
	if command -v ss >/dev/null 2>&1; then
		ss -ltnp 2>/dev/null | grep -E "[:.]$p " || true
	elif command -v lsof >/dev/null 2>&1; then
		lsof -iTCP:"$p" -sTCP:LISTEN 2>/dev/null || true
	fi
}

# ensureSharedNetwork 确保共享外部网络存在，可选把已有反代容器接入。
ensureSharedNetwork() {
	docker network inspect "$sharedNetwork" >/dev/null 2>&1 || {
		docker network create "$sharedNetwork" >/dev/null
		ok "已创建共享网络 $sharedNetwork"
	}
	local proxyName
	proxyName=$(ask "已有反代容器名（接入共享网络；不填留空）" "")
	if [[ -n "$proxyName" ]]; then
		docker network connect "$sharedNetwork" "$proxyName" >/dev/null 2>&1 \
			&& ok "已把 $proxyName 接入 $sharedNetwork" \
			|| warn "接入 $proxyName 失败（可能已在网络中），可稍后手动 docker network connect。"
	fi
}

# chooseFreePort 让用户选端口，若被占用则给出占用者并循环重选；
# 结果写入全局 chosenPort（不用命令替换，以免日志被吞进返回值）。
chosenPort=""
chooseFreePort() {
	local defaultPort="$1" port
	while true; do
		port=$(ask "对外 HTTPS 端口" "$defaultPort")
		if portInUse "$port"; then
			warn "端口 $port 已被占用："
			whoHoldsPort "$port"
			warn "若被 Nginx Proxy Manager / nginx 占用，建议改用「与已有反代共存」模式（重跑脚本选 4），或换一个空闲端口。"
			read -rp "换其它端口重试？[Y/n] " retry </dev/tty
			if [[ -n "$retry" && "${retry,,}" != "y" ]]; then
				chosenPort="$port"; return 0
			fi
		else
			chosenPort="$port"; return 0
		fi
	done
}

# collectConfig 交互收集配置并写入 .env，同时确定编排文件 / profile / DNS 提醒。
collectConfig() {
	info "==== 基础配置 ===="
	local domain token port
	domain=$(ask "完整中继域名（客户端连它，如 anytuck-api.aican.app）")

	info "\n==== 选择部署方式 ===="
	cat <<'EOF'
 1) 固定 IPv4（VPS，本机占用 443/自定义端口，自动签证书）
 2) 固定 IPv6（本机占用端口，host 网络，自动签证书）
 3) 动态 IPv6（家宽，自动维护 AAAA + 自动签证书）
 4) 与已有反代共存（已装 Nginx Proxy Manager 等，本项目不占端口，TLS 交给反代）
EOF
	local mode
	mode=$(ask "选择 1/2/3/4" "1")

	token=$(genToken)
	[[ -f "$ENV_FILE" ]] || cp "$PROJECT_DIR/.env.example" "$ENV_FILE"
	setEnv RELAY_DOMAIN "$domain"
	setEnv RELAY_TOKEN "$token"

	case "$mode" in
		1|2|3)
			# 需自带 Caddy 签证书：收集 ACME 邮箱与 Cloudflare Token
			local email cfToken
			email=$(ask "ACME 证书邮箱")
			cfToken=$(ask "Cloudflare API Token（Zone:Read + DNS:Edit）")
			setEnv ACME_EMAIL "$email"
			setEnv CLOUDFLARE_API_TOKEN "$cfToken"
			;;
	esac

	case "$mode" in
		1)
			chooseFreePort "443"; port="$chosenPort"
			setEnv RELAY_HTTPS_PORT "$port"
			composeFile="docker-compose.yml"; composeProfiles=""
			dnsHint="请在 Cloudflare 给 ${domain} 添加 A 记录 -> 本机公网 IPv4（DNS only，关闭橙色代理）"
			;;
		2)
			chooseFreePort "443"; port="$chosenPort"
			setEnv RELAY_HTTPS_PORT "$port"
			composeFile="docker-compose.home.yml"; composeProfiles=""
			dnsHint="请在 Cloudflare 给 ${domain} 添加 AAAA 记录 -> 本机公网 IPv6（DNS only）"
			;;
		3)
			chooseFreePort "16666"; port="$chosenPort"
			setEnv RELAY_HTTPS_PORT "$port"
			composeFile="docker-compose.home.yml"; composeProfiles="ddns"
			info "可用网卡：$(listInterfaces)"
			local iface
			iface=$(ask "对外网卡名（读取全局 IPv6）")
			setEnv DDNS_RECORD_TYPE "AAAA"
			setEnv DDNS_IP_SOURCE "interface"
			setEnv DDNS_INTERFACE "$iface"
			dnsHint="AAAA 记录将由 ddns 自动维护（该域名需托管在同一 Cloudflare 账户）"
			;;
		4)
			# 共存模式：不占端口、不签证书；TLS/路由交给已有反代
			setEnv RELAY_HTTPS_PORT "443" # 仅用于展示地址（实际由反代对外）
			sharedNetwork=$(ask "共享 docker 网络名" "edge_shared")
			setEnv SHARED_NETWORK "$sharedNetwork"
			composeFile="docker-compose.npm.yml"; composeProfiles=""
			dnsHint="在反代（如 NPM）后台加 Proxy Host：${domain} -> 转发到 anytuck_relay:8443 (http)，务必开启 Websockets/WSS；证书由反代签发（Let's Encrypt）。"
			;;
		*)
			err "无效选择"; exit 1 ;;
	esac
	ok ".env written -> $ENV_FILE"
}

# startStack 构建并启动容器（共存模式先备好共享网络）。
startStack() {
	info "\n==== 构建并启动（首次编译需数分钟）===="
	cd "$PROJECT_DIR"
	if [[ "$composeFile" == "docker-compose.npm.yml" ]]; then
		ensureSharedNetwork
		SHARED_NETWORK="$sharedNetwork" docker compose -f "$composeFile" up -d --build
	elif [[ -n "$composeProfiles" ]]; then
		COMPOSE_PROFILES="$composeProfiles" docker compose -f "$composeFile" up -d --build
	else
		docker compose -f "$composeFile" up -d --build
	fi
	ok "容器已启动"
}

# installCommand 安装 anytuck 管理命令并写入部署配置。
installCommand() {
	install -m 0755 "$PROJECT_DIR/anytuck" /usr/local/bin/anytuck
	mkdir -p "$CONFIG_DIR"
	cat >"$CONFIG_FILE" <<EOF
PROJECT_DIR=$PROJECT_DIR
COMPOSE_FILE=$composeFile
COMPOSE_PROFILES=$composeProfiles
SHARED_NETWORK=$sharedNetwork
EOF
	chmod 0644 "$CONFIG_FILE"
	ok "已安装管理命令：anytuck"
}

# finish 打印连接信息与后续提示。
finish() {
	local addr port domain token
	domain=$(grep -E '^RELAY_DOMAIN=' "$ENV_FILE" | cut -d= -f2-)
	port=$(grep -E '^RELAY_HTTPS_PORT=' "$ENV_FILE" | cut -d= -f2-); port=${port:-443}
	token=$(grep -E '^RELAY_TOKEN=' "$ENV_FILE" | cut -d= -f2-)
	if [[ "$port" == "443" ]]; then addr="wss://${domain}/ws"; else addr="wss://${domain}:${port}/ws"; fi

	echo
	ok  "=================================================="
	ok  " 安装完成！请在 AnyTuck App 的中继设置中填写："
	echo "--------------------------------------------------"
	echo "   中继地址(WebSocket) : $addr"
	echo "   连接密码(Token)     : $token"
	ok  "=================================================="
	warn "下一步：$dnsHint"
	echo
	echo "证书：由 Let's Encrypt 自动签发并自动续期（无需手动操作），首次约需 30-60s。"
	echo
	echo "常用管理："
	echo "  anytuck            进入管理菜单"
	echo "  anytuck info       查看连接地址与密码"
	echo "  anytuck newpass    随机更换密码"
	echo "  anytuck reconfig   修改中继域名 / 端口"
	echo "  anytuck logs       查看日志（观察证书签发）"
}

main() {
	requireRoot
	requireTty
	detectOs
	checkDocker
	resolveProject
	collectConfig
	startStack
	installCommand
	finish
}

main "$@"
