#!/usr/bin/env bash
# ==============================================================================
#  frida_workphone_oneclick.sh — 工作手机·Frida 主控一键启动
#
#  目标：插上 Type-C 数据线 → 自动完成"Frida 主、ADB 辅"的全链路启动
#       1) 探测 USB 设备、确认 ADB 授权
#       2) 锁定主机 frida 版本（默认 16.5.6）
#       3) 在设备上推送/启动 frida-server-16（Root 模式 = 主路径）
#       4) Root 不可用时自动尝试 Frida Gadget 备背端口转发
#       5) 启动 SDK 服务（uvicorn 8899）
#       6) 通过 /api/v3/hook/probe/{device_id} 探测 Hook 是否就绪
#       7) 启动 Agent（WebSocket 通道，带 hook=enabled）
#       8) 输出对接清单（Type-C 模式下存客宝直接调用 8899）
#
#  设计参考：
#   - 开发文档/8、部署/设备端Hook安装部署.md（Root + frida-server 标准）
#   - 机擎/references/快速入机部署_Root_Frida_Hook.md（10/10 验证基线）
#   - sdk/agent/hook/frida_manager.py（usb / gadget / remote 三模式）
#   - sdk/app/routers/unified.py（/hook/probe/{id}, /hook/execute, channel=hook）
#
#  用法：
#    ./scripts/frida_workphone_oneclick.sh                  # 默认设备 + 默认参数
#    ./scripts/frida_workphone_oneclick.sh -d <serial>      # 指定 ADB 序列号
#    ./scripts/frida_workphone_oneclick.sh --gadget         # 强制走 Gadget 备背
#    ./scripts/frida_workphone_oneclick.sh --no-agent       # 仅起 SDK + Frida，不起 Agent
#    ./scripts/frida_workphone_oneclick.sh --probe-only     # 只做探测，不启服务
# ==============================================================================
set -euo pipefail

# ── 路径与默认值 ──────────────────────────────────────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SDK_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
PROJ_ROOT="$(cd "${SDK_ROOT}/.." && pwd)"

FRIDA_VERSION_DEFAULT="16.5.6"
FRIDA_VERSION="${WORKPHONE_FRIDA_VERSION:-${FRIDA_VERSION_DEFAULT}}"
SDK_PORT="${WORKPHONE_SDK_PORT:-8899}"
SDK_BASE="http://127.0.0.1:${SDK_PORT}"
GADGET_DEFAULT_PORT="${WORKPHONE_GADGET_PORT:-27042}"
export ANDROID_ADB_SERVER_PORT="${ANDROID_ADB_SERVER_PORT:-5038}"
LOG_DIR="${SDK_ROOT}/logs"
mkdir -p "${LOG_DIR}"

DEVICE_SERIAL=""
FORCE_GADGET=0
NO_AGENT=0
PROBE_ONLY=0

# 所有 ADB 调用固定到同一个 server 端口，避免 SDK/CLI/脚本各看一个设备列表。
adb() {
  command adb -P "${ANDROID_ADB_SERVER_PORT}" "$@"
}

# ── 参数解析 ──────────────────────────────────────────────────────────────────
while [[ $# -gt 0 ]]; do
  case "$1" in
    -d|--device) DEVICE_SERIAL="$2"; shift 2 ;;
    --gadget) FORCE_GADGET=1; shift ;;
    --no-agent) NO_AGENT=1; shift ;;
    --probe-only) PROBE_ONLY=1; shift ;;
    -h|--help)
      sed -n '2,30p' "$0"; exit 0 ;;
    *) echo "[!] 未知参数: $1"; exit 1 ;;
  esac
done

# ── 颜色与日志 ────────────────────────────────────────────────────────────────
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'
log()   { echo -e "${GREEN}[+]${NC} $*"; }
warn()  { echo -e "${YELLOW}[!]${NC} $*"; }
fail()  { echo -e "${RED}[✗]${NC} $*" >&2; }
fatal() { fail "$*"; exit 1; }

banner() {
  echo "================================================================"
  echo "  工作手机 · Frida 主控一键启动 (Type-C / 主：Frida，辅：ADB)"
  echo "  SDK_ROOT     = ${SDK_ROOT}"
  echo "  FRIDA_TARGET = ${FRIDA_VERSION}"
  echo "  SDK_PORT     = ${SDK_PORT}"
  echo "================================================================"
}

# ── 步骤 1：检查 ADB ──────────────────────────────────────────────────────────
step_check_adb() {
  command -v adb >/dev/null 2>&1 || fatal "未找到 adb，请先安装 platform-tools 并加入 PATH"
  log "ADB version: $(adb version | head -1) port=${ANDROID_ADB_SERVER_PORT}"

  # 启动 server
  adb start-server >/dev/null 2>&1 || true

  if [[ -n "${DEVICE_SERIAL}" ]]; then
    bash "${SDK_ROOT}/scripts/wait_adb_device.sh" "${DEVICE_SERIAL}" "${ADB_WAIT_SEC:-90}" || {
      fail "ADB 未连接: ${DEVICE_SERIAL}"
      echo "    无线：开发者选项 → 无线调试 → 配对后把 IP:端口 设为 DEVICE_SERIAL"
      echo "    或有线：Type-C 连接并授权 USB 调试"
      exit 1
    }
  fi

  if [[ -z "${DEVICE_SERIAL}" ]]; then
    DEVICE_SERIAL="$(adb devices | awk '/\tdevice$/ {print $1; exit}')"
  fi
  if [[ -z "${DEVICE_SERIAL}" ]]; then
    fail "未检测到已授权的 USB 设备"
    echo "    1) 请用 Type-C 数据线连接手机，并在手机上点'始终允许此电脑调试'"
    echo "    2) 然后重新执行 $0"
    exit 1
  fi
  log "目标设备: ${DEVICE_SERIAL}"

  # 设备能力快照
  ABI="$(adb -s "${DEVICE_SERIAL}" shell getprop ro.product.cpu.abi | tr -d '\r')"
  MODEL="$(adb -s "${DEVICE_SERIAL}" shell getprop ro.product.model | tr -d '\r')"
  ANDROID="$(adb -s "${DEVICE_SERIAL}" shell getprop ro.build.version.release | tr -d '\r')"
  log "Model=${MODEL}  ABI=${ABI}  Android=${ANDROID}"

  case "${ABI}" in
    arm64-v8a) FRIDA_ARCH="arm64" ;;
    armeabi-v7a) FRIDA_ARCH="arm" ;;
    x86_64) FRIDA_ARCH="x86_64" ;;
    x86) FRIDA_ARCH="x86" ;;
    *) fatal "不支持的架构: ${ABI}" ;;
  esac
}

# ── 步骤 2：锁定主机 frida 版本 ───────────────────────────────────────────────
step_lock_host_frida() {
  command -v python3 >/dev/null 2>&1 || fatal "未找到 python3"

  HOST_VER="$(python3 -c 'import frida; print(frida.__version__)' 2>/dev/null || true)"
  if [[ "${HOST_VER}" != "${FRIDA_VERSION}" ]]; then
    warn "主机 frida 版本=${HOST_VER:-(未安装)} ≠ ${FRIDA_VERSION}，开始锁定…"
    pip install --quiet "frida==${FRIDA_VERSION}" "frida-tools==12.5.0" || \
      fatal "pip 安装 frida==${FRIDA_VERSION} 失败"
    HOST_VER="$(python3 -c 'import frida; print(frida.__version__)')"
  fi
  log "主机 frida=${HOST_VER}"
}

# ── 步骤 3：检测 Root，部署反检测 frida-server（随机名+随机端口）────────────
step_setup_frida_server_root() {
  ROOT_RAW="$(adb -s "${DEVICE_SERIAL}" shell 'su -c id' 2>/dev/null || true)"
  if [[ "${ROOT_RAW}" != *"uid=0"* ]]; then
    warn "Root 不可用，将切换到 Gadget 备背模式"
    return 1
  fi
  log "Root 已确认 (${ROOT_RAW//$'\r'/})"

  LOCAL_BIN_DIR="${SDK_ROOT}/scripts"
  LOCAL_CACHED="${LOCAL_BIN_DIR}/frida-server-${FRIDA_VERSION}-android-${FRIDA_ARCH}"
  REMOTE_BIN="/data/local/tmp/frida-server-16"

  if [[ ! -f "${LOCAL_CACHED}" ]]; then
    log "下载 frida-server-${FRIDA_VERSION}-${FRIDA_ARCH}…"
    TMP="$(mktemp -d)"
    URL="https://github.com/frida/frida/releases/download/${FRIDA_VERSION}/frida-server-${FRIDA_VERSION}-android-${FRIDA_ARCH}.xz"
    curl -fL "${URL}" -o "${TMP}/fs.xz" || fatal "下载 frida-server 失败：${URL}"
    xz -d -f "${TMP}/fs.xz"
    mv "${TMP}/fs" "${LOCAL_CACHED}"
    chmod 755 "${LOCAL_CACHED}"
  fi

  if [[ -z "$(adb -s "${DEVICE_SERIAL}" shell "su -c 'ls ${REMOTE_BIN} 2>/dev/null'" | tr -d '\r')" ]]; then
    log "推送 ${LOCAL_CACHED} → ${REMOTE_BIN}"
    adb -s "${DEVICE_SERIAL}" push "${LOCAL_CACHED}" "${REMOTE_BIN}" >/dev/null
    adb -s "${DEVICE_SERIAL}" shell "su -c 'chmod 755 ${REMOTE_BIN}'"
  fi

  # 反检测：随机二进制名 + 随机监听端口（避免 stock frida-server 被微信 attach 检测）
  if command -v shuf >/dev/null 2>&1; then
    PHANTOM_PORT=$(shuf -i 10000-60000 -n 1)
  else
    PHANTOM_PORT=$((10000 + RANDOM % 50001))
  fi
  PHANTOM_NAME="fs_$(head -c 3 /dev/urandom | xxd -p 2>/dev/null || echo $$)"
  PHANTOM_REMOTE="/data/local/tmp/${PHANTOM_NAME}"
  PHANTOM_CONFIG="${SDK_ROOT}/scripts/anti_detect/phantom_frida_config.json"

  log "部署反检测 Frida：${PHANTOM_NAME} @ 0.0.0.0:${PHANTOM_PORT}"
  adb -s "${DEVICE_SERIAL}" shell "su -c 'cp ${REMOTE_BIN} ${PHANTOM_REMOTE} && chmod 755 ${PHANTOM_REMOTE}'"
  adb -s "${DEVICE_SERIAL}" shell "su -c 'killall frida-server-16 frida-server 2>/dev/null; pkill -f \"^/data/local/tmp/fs_\" 2>/dev/null; true'"
  sleep 1
  # 用 setsid 彻底脱离 adb shell 会话，避免 shell 退出时 frida-server 随之被 SIGHUP 杀掉
  # （本会话踩坑：nohup & 经 adb shell 启动仍随会话结束被杀 → 后续 attach connection closed）
  adb -s "${DEVICE_SERIAL}" shell "su -c 'setsid ${PHANTOM_REMOTE} -l 0.0.0.0:${PHANTOM_PORT} </dev/null >/dev/null 2>&1 &'"
  sleep 2
  PH_PID="$(adb -s "${DEVICE_SERIAL}" shell "su -c 'pidof ${PHANTOM_NAME}'" 2>/dev/null | tr -d '\r')"
  [[ -n "${PH_PID}" ]] || fatal "反检测 frida-server 启动失败 (port=${PHANTOM_PORT})"
  log "反检测 frida-server 已运行 (pid=${PH_PID}, port=${PHANTOM_PORT})"

  adb -s "${DEVICE_SERIAL}" forward "tcp:${PHANTOM_PORT}" "tcp:${PHANTOM_PORT}" >/dev/null
  mkdir -p "$(dirname "${PHANTOM_CONFIG}")"
  python3 -c "import json; json.dump({'device_serial':'${DEVICE_SERIAL}','listen_port':${PHANTOM_PORT},'binary_name':'${PHANTOM_NAME}','binary_path':'${PHANTOM_REMOTE}'}, open('${PHANTOM_CONFIG}','w'), indent=2)"
  log "phantom 配置已写入 ${PHANTOM_CONFIG}（unified.py 自动读取）"

  # 确保微信运行（Hook 需要附着）
  WX_PID="$(adb -s "${DEVICE_SERIAL}" shell pidof com.tencent.mm 2>/dev/null | tr -d '\r' || true)"
  if [[ -z "${WX_PID}" ]]; then
    log "拉起微信..."
    adb -s "${DEVICE_SERIAL}" shell am start -n com.tencent.mm/.ui.LauncherUI >/dev/null 2>&1 || true
    sleep 5
  fi
  return 0
}

# ── 步骤 4：Gadget 备背（无 Root 时） ─────────────────────────────────────────
step_setup_gadget_fallback() {
  warn "进入 Gadget 备背模式 — 请确保已用 sdk/scripts/inject_frida_gadget.sh 重打包并安装微信"
  PORT="${GADGET_DEFAULT_PORT}"
  log "adb forward tcp:${PORT} tcp:${PORT}"
  adb -s "${DEVICE_SERIAL}" forward "tcp:${PORT}" "tcp:${PORT}" >/dev/null
  log "Gadget endpoint = 127.0.0.1:${PORT}（FridaManager 选 mode=gadget 时可用）"
  export WORKPHONE_FRIDA_MODE="gadget"
  export WORKPHONE_FRIDA_GADGET_PORT="${PORT}"
}

# ── 步骤 5：启动 SDK 服务（8899）──────────────────────────────────────────────
step_start_sdk() {
  if curl -sS "${SDK_BASE}/health" >/dev/null 2>&1; then
    log "SDK 已在 ${SDK_BASE} 监听，跳过启动"
    return 0
  fi

  log "启动 SDK 服务（uvicorn 0.0.0.0:${SDK_PORT}）…"
  (
    cd "${SDK_ROOT}/app"
    export SDK_MATRIX_VERIFY=1
    export WECHAT_WS_HOOK_ONLY=1
    export WECHAT_BACKEND_ONLY=1
    nohup env ANDROID_ADB_SERVER_PORT="${ANDROID_ADB_SERVER_PORT:-5038}" \
      python3 -m uvicorn main:app --host 0.0.0.0 --port "${SDK_PORT}" \
      > "${LOG_DIR}/sdk.log" 2>&1 &
    echo $! > "${LOG_DIR}/sdk.pid"
  )
  for i in {1..15}; do
    if curl -sS "${SDK_BASE}/health" >/dev/null 2>&1; then
      log "SDK 已就绪 (pid=$(cat "${LOG_DIR}/sdk.pid"))"
      return 0
    fi
    sleep 1
  done
  fatal "SDK 启动失败，请查看 ${LOG_DIR}/sdk.log"
}

# ── 步骤 6：调用 /hook/probe 让服务端正式承认 Hook 能力 ───────────────────────
step_hook_probe() {
  log "调用 ${SDK_BASE}/api/v3/hook/probe/${DEVICE_SERIAL}"
  RESP=""
  SUPPORTS="False"
  for _i in $(seq 1 20); do
    RESP="$(curl -sS "${SDK_BASE}/api/v3/hook/probe/${DEVICE_SERIAL}" || true)"
    SUPPORTS="$(echo "${RESP}" | python3 -c 'import json,sys
try:
    d=json.load(sys.stdin); print(d.get("data",d).get("supports_hook",False))
except Exception:
    print(False)' 2>/dev/null || echo False)"
    if [[ "${SUPPORTS}" == "True" ]]; then
      break
    fi
    sleep 2
  done
  echo "${RESP}" | python3 -m json.tool 2>/dev/null || echo "${RESP}"

  if [[ "${SUPPORTS}" == "True" ]]; then
    log "Hook 通道就绪 ✅（服务端已写入 device_modules）"
  else
    warn "Hook 探测未通过（设备能力为 supports_hook=false）"
    warn "可能原因：Root/frida-server 未起、微信未运行、frida 版本错配"
    return 1
  fi
}

# ── 步骤 7：启动设备端 Agent（USB-reverse + WS）──────────────────────────────
step_start_agent() {
  if [[ "${NO_AGENT}" == "1" ]]; then
    warn "--no-agent 已指定，跳过 Agent 启动"
    return 0
  fi
  log "adb reverse tcp:${SDK_PORT} tcp:${SDK_PORT}"
  adb -s "${DEVICE_SERIAL}" reverse "tcp:${SDK_PORT}" "tcp:${SDK_PORT}" >/dev/null || true

  AGENT_DIR="${SDK_ROOT}/agent"
  if [[ ! -f "${AGENT_DIR}/agent.py" ]]; then
    warn "未找到 ${AGENT_DIR}/agent.py，跳过 Agent 启动"
    return 0
  fi

  log "启动 Agent（device=${DEVICE_SERIAL}，WebSocket + Frida remote）…"
  PH_PORT=""
  PH_CFG="${SDK_ROOT}/scripts/anti_detect/phantom_frida_config.json"
  AGENT_LOG="${LOG_DIR}/agent.log"
  LAN_ENV="${SDK_ROOT}/config/lan.env"
  [[ -f "${LAN_ENV}" ]] && source "${LAN_ENV}" || true
  # Mac 侧 Agent 连本机 SDK；lan.env 的 WORKPHONE_LAN_IP 供手机 Termux 寻服
  WS_HOST="127.0.0.1"
  if [[ -f "${PH_CFG}" ]]; then
    PH_PORT=$(python3 -c "import json;print(json.load(open('${PH_CFG}')).get('listen_port',''))" 2>/dev/null || true)
  fi

  # 避免重复 Agent 抢同一 device_id
  if [[ -f "${LOG_DIR}/agent.pid" ]]; then
    kill "$(cat "${LOG_DIR}/agent.pid" 2>/dev/null)" 2>/dev/null || true
  fi
  pkill -f "agent.py -d ${DEVICE_SERIAL} -s ws://127.0.0.1:${SDK_PORT}" 2>/dev/null || true
  sleep 1

  # Agent 启动前再次确认微信已运行（Frida attach 依赖）
  WX_PID="$(adb -s "${DEVICE_SERIAL}" shell pidof com.tencent.mm 2>/dev/null | tr -d '\r' || true)"
  if [[ -z "${WX_PID}" ]]; then
    log "拉起微信（Agent 启动前）..."
    adb -s "${DEVICE_SERIAL}" shell am start -n com.tencent.mm/.ui.LauncherUI >/dev/null 2>&1 || true
    sleep 6
  fi

  : > "${AGENT_LOG}"
  {
    echo "=== $(date '+%Y-%m-%d %H:%M:%S') oneclick agent start ==="
    echo "device=${DEVICE_SERIAL} sdk=${SDK_BASE} frida_port=${PH_PORT:-auto}"
  } >> "${AGENT_LOG}"

  AGENT_PID="$(
    cd "${AGENT_DIR}"
    export WP_FRIDA_MODE=remote
    export WP_DEVICE_SERIAL="${DEVICE_SERIAL}"
    export WORKPHONE_AGENT_LOG="${AGENT_LOG}"
    export WORKPHONE_SDK_PORT="${SDK_PORT}"
    export WORKPHONE_WS_HOST="${WS_HOST}"
    if [[ -n "${PH_PORT}" ]]; then
      export WP_FRIDA_PORT="${PH_PORT}"
      adb -s "${DEVICE_SERIAL}" forward "tcp:${PH_PORT}" "tcp:${PH_PORT}" >/dev/null 2>&1 || true
    fi
    python3 - <<'PY'
import json
import os
import subprocess
import sys

log_path = os.environ["WORKPHONE_AGENT_LOG"]
device_serial = os.environ["WP_DEVICE_SERIAL"]
sdk_port = os.environ["WORKPHONE_SDK_PORT"]
sdk_root = os.path.abspath(os.path.join(os.getcwd(), ".."))
build = os.path.join(sdk_root, "scripts", "build_agent_config.py")
public_servers = []
try:
    cfg = json.loads(
        subprocess.check_output(
            [sys.executable, build, "--device-id", device_serial],
            text=True,
        )
    )
    public_servers = [s for s in cfg.get("public_servers") or [] if s]
except Exception:
    pass

# Mac 本机 Agent 优先连本地 SDK；断连时按 BIND-03 公网/局域网候选回退
ws_host = os.environ.get("WORKPHONE_WS_HOST", "127.0.0.1")
if ws_host in ("127.0.0.1", "localhost"):
    server_url = f"ws://127.0.0.1:{sdk_port}/ws/device"
else:
    server_url = f"ws://{ws_host}:{sdk_port}/ws/device"

cmd = [
    sys.executable,
    "agent.py",
    "-d",
    device_serial,
    "-s",
    server_url,
    "--heartbeat",
    "10",
]
if public_servers:
    cmd.extend(["--public-servers", ",".join(public_servers)])
with open(log_path, "ab", buffering=0) as log:
    proc = subprocess.Popen(
        cmd,
        cwd=os.getcwd(),
        env=os.environ.copy(),
        stdin=subprocess.DEVNULL,
        stdout=log,
        stderr=subprocess.STDOUT,
        start_new_session=True,
        close_fds=True,
    )
print(proc.pid)
PY
  )"
  echo "${AGENT_PID}" > "${LOG_DIR}/agent.pid"
  log "Agent 已启动 (pid=${AGENT_PID})"

  # 等待 Frida RPC 与 WebSocket 注册同时就绪，避免 hook/execute 在 lazy attach 阶段超时
  for _i in $(seq 1 45); do
    if ! kill -0 "${AGENT_PID}" 2>/dev/null; then
      tail -80 "${AGENT_LOG}" >&2 || true
      fatal "Agent 启动后退出，请查看 ${AGENT_LOG}"
    fi
    if grep -q "RPC ping: pong" "${AGENT_LOG}" 2>/dev/null && \
       grep -q "服务器确认注册" "${AGENT_LOG}" 2>/dev/null; then
      log "Frida RPC + WebSocket 注册已就绪 ✅"
      return 0
    fi
    sleep 2
  done
  tail -80 "${AGENT_LOG}" >&2 || true
  fatal "90s 内未看到 RPC ping 或服务器确认注册，请检查微信、phantom 端口转发与 SDK WebSocket"
}

# ── 步骤 8：对接清单 ──────────────────────────────────────────────────────────
print_summary() {
  cat <<EOF

================================================================
  Frida 主控一键启动 ✅ 完成
================================================================
  设备序列号  : ${DEVICE_SERIAL}
  SDK API     : ${SDK_BASE}
  API 文档    : ${SDK_BASE}/docs
  控制中心    : ${SDK_BASE}/hub
  Hook 探测   : ${SDK_BASE}/api/v3/hook/probe/${DEVICE_SERIAL}
  Phantom配置 : ${SDK_ROOT}/scripts/anti_detect/phantom_frida_config.json
  Hook 执行   : POST ${SDK_BASE}/api/v3/hook/execute   (hook_only=true)
  Hook 取数   : ${SDK_BASE}/api/v3/hook/data/${DEVICE_SERIAL}
  通道选择    : 主 = WebSocket Agent + Frida Hook（WECHAT_WS_HOOK_ONLY，禁止 ADB UI）

  存客宝侧调用范例（PHP）:
    \$sdk = new \\Cunkebao\\WorkPhone\\WorkPhoneClient('${SDK_BASE}', 'TOKEN');
    \$sdk->sendMessage('${DEVICE_SERIAL}', 'wechat', '文件传输助手',
                       '[E2E] Frida 主控通道', 'text', null, null, null,
                       'hook'); // ← channel=hook，强制走 Frida

  日志:
    SDK   : ${LOG_DIR}/sdk.log
    Agent : ${LOG_DIR}/agent.log

  停止:
    kill \$(cat ${LOG_DIR}/sdk.pid 2>/dev/null) 2>/dev/null
    kill \$(cat ${LOG_DIR}/agent.pid 2>/dev/null) 2>/dev/null
================================================================
EOF
}

# ── 主流程 ────────────────────────────────────────────────────────────────────
banner
step_check_adb
step_lock_host_frida

if [[ "${FORCE_GADGET}" == "1" ]]; then
  step_setup_gadget_fallback
else
  if ! step_setup_frida_server_root; then
    step_setup_gadget_fallback
  fi
fi

if [[ "${PROBE_ONLY}" == "1" ]]; then
  step_start_sdk
  step_hook_probe
  exit 0
fi

step_start_sdk
step_start_agent
step_hook_probe
print_summary
