#!/usr/bin/env bash
# Security Center 本地部署与 E2E（全 Docker）
# 用法：./local.sh <command>，详见 usage() 或 README.md
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
ENV_FILE="${SCRIPT_DIR}/.env"
COMPOSE_FILE="${SCRIPT_DIR}/docker-compose.local.yml"
PROJECT="sc-local"

# ---------- 基础工具 ----------

die()  { echo "ERROR: $*" >&2; exit 1; }
info() { echo "INFO: $*" >&2; }   # 一律走 stderr：token 类命令的 stdout 必须干净（command substitution 会捕获）
warn() { echo "WARN: $*" >&2; }

usage() {
  cat <<'EOF'
Security Center 本地部署与 E2E（全 Docker）

Usage: ./local.sh <command> [args]

部署生命周期:
  up [--with-frontend] [--no-build]   构建并启动（默认不含 frontend，首次会构建镜像）
  down                                停止并删除容器（保留数据卷，下次 up 数据还在）
  clean                               彻底清理：容器 + 数据卷 + 本地镜像 + .env（测试完成后用）
  status                              查看各服务状态与健康
  logs <mysql|redis|backend|frontend> 跟踪服务日志

E2E 辅助:
  token                               获取开发 admin token（bypass_sso 快捷会话）
  mint-token <email> <user_id> [权限csv]
                                      创建本地测试用户并自签 JWT（默认权限:
                                      agent:manage_own_keys,caasm:view_assets,vuln:view_findings）
  e2e [phase1|phase2|phase3|all]      跑 tests/e2e 验收脚本（默认 all）
  mysql                               打开 MySQL 客户端会话

环境信息（up 成功后）:
  backend  http://localhost:18080   frontend http://localhost:13000（--with-frontend 时）
  MySQL    127.0.0.1:13306          Redis    127.0.0.1:16379
EOF
}

require_command() { command -v "$1" >/dev/null 2>&1 || die "required command not found: $1"; }

compose() {
  if docker compose version >/dev/null 2>&1; then
    docker compose -p "${PROJECT}" -f "${COMPOSE_FILE}" --env-file "${ENV_FILE}" "$@"
  elif command -v docker-compose >/dev/null 2>&1; then
    docker-compose -p "${PROJECT}" -f "${COMPOSE_FILE}" --env-file "${ENV_FILE}" "$@"
  else
    die "docker compose (v2) 或 docker-compose (v1) 未安装"
  fi
}

env_value() { # 从 .env 读值（不 source，避免特殊字符问题）
  local key="$1"
  [[ -f "${ENV_FILE}" ]] || return 0
  grep -E "^${key}=" "${ENV_FILE}" | tail -1 | cut -d= -f2-
}

ensure_env() {
  if [[ ! -f "${ENV_FILE}" ]]; then
    cp "${SCRIPT_DIR}/local.env.example" "${ENV_FILE}"
    info "已生成 ${ENV_FILE}（默认值仅适用于本地，可按需修改）"
  fi
}

ensure_prereqs() {
  require_command docker
  require_command curl
  docker info >/dev/null 2>&1 || die "Docker daemon 未运行（请启动 Docker Desktop / dockerd）"
  ensure_env
}

BACKEND_BASE_URL() { printf 'http://localhost:%s' "$(env_value SC_LOCAL_BACKEND_PORT)"; }

wait_backend_healthy() {
  local url; url="$(BACKEND_BASE_URL)"
  info "等待 backend 就绪（${url}/health）..."
  local i
  for i in $(seq 1 90); do
    if curl -fsS "${url}/health" >/dev/null 2>&1; then
      info "backend 已就绪"
      return 0
    fi
    sleep 2
  done
  die "backend 健康检查超时，用 './local.sh logs backend' 排查"
}

# 应用索引类 SQL 迁移（幂等）。开发环境 auto-migrate 已建全部表（含 api_keys），
# 历史/建表类迁移不重放（多数非幂等，且 auto-migrate 已覆盖）；
# 这里只补 auto-migrate 覆盖不到的索引（Base.CreatedAt 无 index tag、generated column 无 GORM 模型）。
INDEX_MIGRATIONS=(
  202607270002_add_audit_logs_created_at_index.sql
  202607270003_add_audit_logs_agent_id_index.sql
)
apply_sql_migrations() {
  local mdir="${ROOT_DIR}/security-center-backend/migrations"
  local db root_pw
  db="$(env_value SC_LOCAL_MYSQL_DATABASE)"
  root_pw="$(env_value SC_LOCAL_MYSQL_ROOT_PASSWORD)"
  local name f
  for name in "${INDEX_MIGRATIONS[@]}"; do
    f="${mdir}/${name}"
    [[ -f "${f}" ]] || { warn "迁移文件不存在，跳过: ${name}"; continue; }
    info "应用迁移 ${name}"
    compose exec -T mysql mysql -uroot -p"${root_pw}" "${db}" < "${f}" \
      || die "迁移失败: ${name}"
  done
  # 本地专用补充迁移（scripts/local/migrations/，全部幂等）
  local ldir="${SCRIPT_DIR}/migrations"
  if [[ -d "${ldir}" ]]; then
    for f in "${ldir}"/*.sql; do
      [[ -f "${f}" ]] || continue
      info "应用本地迁移 $(basename "${f}")"
      compose exec -T mysql mysql -uroot -p"${root_pw}" "${db}" < "${f}" \
        || die "本地迁移失败: ${f}"
    done
  fi
}

get_dev_token() { # bypass_sso 快捷会话 → access_token
  local url; url="$(BACKEND_BASE_URL)"
  curl -fsS -X POST "${url}/api/v1/auth/google/login" \
    | python3 -c 'import json,sys
d=json.load(sys.stdin)
assert d.get("code")==0, f"login failed: {d}"
s=d["data"]["session"]
print(s.get("access_token") or s.get("accessToken") or "")' \
    | { read -r t; [[ -n "${t}" ]] || die "未能解析 access_token"; printf '%s' "${t}"; }
}

# ---------- 命令 ----------

cmd_up() {
  local with_frontend="false" do_build="true"
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --with-frontend) with_frontend="true"; shift ;;
      --no-build)      do_build="false"; shift ;;
      *) die "up: unknown arg $1" ;;
    esac
  done
  ensure_prereqs

  local -a services=(mysql redis)
  # SC_LOCAL_S3_ENABLED=true 时启用本地 MinIO（漏洞插图上传 E2E；minio-init 一次性建 bucket）
  if [[ "$(env_value SC_LOCAL_S3_ENABLED)" == "true" ]]; then
    services+=(minio minio-init)
  fi
  services+=(backend)
  [[ "${with_frontend}" == "true" ]] && services+=(frontend)

  if [[ "${do_build}" == "true" ]]; then
    info "构建镜像（首次较慢）..."
    compose build "${services[@]}"
  fi
  info "启动服务: ${services[*]}"
  if [[ "${with_frontend}" == "true" ]]; then
    compose --profile frontend up -d "${services[@]}"
  else
    compose up -d "${services[@]}"
  fi

  wait_backend_healthy
  apply_sql_migrations

  cat <<EOF

✅ 本地环境已就绪
  backend   $(BACKEND_BASE_URL)        （E2E/MCP 直连）
  MySQL     127.0.0.1:$(env_value SC_LOCAL_MYSQL_PORT)  db=$(env_value SC_LOCAL_MYSQL_DATABASE)
  Redis     127.0.0.1:$(env_value SC_LOCAL_REDIS_PORT)
$( [[ "${with_frontend}" == "true" ]] && echo "  frontend  http://localhost:$(env_value SC_LOCAL_FRONTEND_PORT)       （浏览器打开，点登录即得开发会话）" )

下一步:
  获取 admin token:   ./local.sh token
  跑 E2E:             ./local.sh e2e all
  测试完成后清理:      ./local.sh clean
EOF
}

cmd_down() { ensure_prereqs; compose --profile frontend down; info "已停止（数据卷保留）"; }

cmd_clean() {
  ensure_prereqs
  warn "将删除容器、数据卷（MySQL/Redis 数据全丢）、本地镜像 sc-local-*、${ENV_FILE}"
  [[ "${1:-}" == "--yes" ]] || { read -r -p "确认继续? [y/N] " ans; [[ "${ans}" == "y" || "${ans}" == "Y" ]] || { info "已取消"; exit 0; }; }
  compose --profile frontend down -v --rmi local --remove-orphans
  rm -f "${ENV_FILE}"
  info "清理完成"
}

cmd_status() { ensure_prereqs; compose --profile frontend ps; }

cmd_logs() {
  ensure_prereqs
  local svc="${1:-}"; [[ -n "${svc}" ]] || die "logs 需要服务名: mysql|redis|backend|frontend"
  compose logs -f --tail=200 "${svc}"
}

cmd_token() {
  ensure_prereqs; wait_backend_healthy
  local t; t="$(get_dev_token)"
  echo "${t}"
  info "导出使用: export SC_ADMIN_TOKEN=${t}" >&2
}

cmd_mint_token() {
  ensure_prereqs; wait_backend_healthy
  local email="${1:-}" uid="${2:-}" perms_csv="${3:-agent:manage_own_keys,caasm:view_assets,vuln:view_findings}"
  [[ -n "${email}" && -n "${uid}" ]] || die "用法: ./local.sh mint-token <email> <user_id> [权限csv]"
  [[ "${uid}" =~ ^[0-9]+$ ]] || die "user_id 必须是数字: ${uid}"
  case "${email}${perms_csv}" in *"'"*) die "email/权限含单引号，拒绝执行" ;; esac

  # 1) 确保用户行存在且 id == uid（api_keys.owner_user_id 有 FK；auto-increment 行的 id 未必等于 uid）
  local db root_pw
  db="$(env_value SC_LOCAL_MYSQL_DATABASE)"; root_pw="$(env_value SC_LOCAL_MYSQL_ROOT_PASSWORD)"
  compose exec -T mysql mysql -uroot -p"${root_pw}" "${db}" <<SQL
INSERT INTO users (id, external_id, email, name, status, created_at, updated_at)
VALUES (${uid}, 'local|${email}', '${email}', '${email%%@*}(local)', 'active', NOW(3), NOW(3))
ON DUPLICATE KEY UPDATE id = id;
SQL
  local actual_uid
  actual_uid="$(compose exec -T mysql mysql -uroot -p"${root_pw}" -N -B -e "SELECT id FROM users WHERE email='${email}'" "${db}" | tail -1)"
  [[ "${actual_uid}" == "${uid}" ]] || die "email ${email} 已被占用且 id=${actual_uid}（期望 ${uid}），换个 email 或 user_id"

  # 2) 自签 HS256 JWT（claims 与 jwt.go 一致；iss 不校验）
  local secret; secret="$(env_value SC_LOCAL_JWT_ACCESS_SECRET)"
  [[ -n "${secret}" ]] || die "SC_LOCAL_JWT_ACCESS_SECRET 为空"
  SC_MINT_SECRET="${secret}" SC_MINT_EMAIL="${email}" SC_MINT_UID="${uid}" SC_MINT_PERMS="${perms_csv}" \
  python3 - <<'PY'
import base64, hashlib, hmac, json, os, time

def b64u(b): return base64.urlsafe_b64encode(b).rstrip(b"=")

secret = os.environ["SC_MINT_SECRET"].encode()
perms = [p.strip() for p in os.environ["SC_MINT_PERMS"].split(",") if p.strip()]
payload = {
    "user_id": int(os.environ["SC_MINT_UID"]),
    "email": os.environ["SC_MINT_EMAIL"],
    "roles": ["agent_user"],
    "permissions": perms,
    "iss": "security-center",
    "exp": int(time.time()) + 24 * 3600,
}
head = b64u(json.dumps({"alg": "HS256", "typ": "JWT"}, separators=(",", ":")).encode())
body = b64u(json.dumps(payload, separators=(",", ":")).encode())
sig = b64u(hmac.new(secret, head + b"." + body, hashlib.sha256).digest())
print(f"{head.decode()}.{body.decode()}.{sig.decode()}")
PY
}

cmd_e2e() {
  ensure_prereqs; wait_backend_healthy
  local target="${1:-all}"
  local e2e_dir="${ROOT_DIR}/security-center-backend/tests/e2e"
  local db root_pw mysql_port
  db="$(env_value SC_LOCAL_MYSQL_DATABASE)"; root_pw="$(env_value SC_LOCAL_MYSQL_ROOT_PASSWORD)"
  mysql_port="$(env_value SC_LOCAL_MYSQL_PORT)"

  local admin_token; admin_token="$(get_dev_token)"; [[ -n "${admin_token}" ]] || die "获取 admin token 失败"

  # bypass_sso 开发会话不落库，api_keys.owner_user_id 有 FK——先 upsert 一个本地用户并取其真实 id
  compose exec -T mysql mysql -uroot -p"${root_pw}" "${db}" <<SQL
INSERT INTO users (external_id, email, name, status, created_at, updated_at)
VALUES ('local|admin.local@example.com', 'admin.local@example.com', 'Local Admin', 'active', NOW(3), NOW(3))
ON DUPLICATE KEY UPDATE id = id;
SQL
  local owner_uid
  owner_uid="$(compose exec -T mysql mysql -uroot -p"${root_pw}" -N -B -e "SELECT id FROM users WHERE email='admin.local@example.com'" "${db}" | tail -1)"
  [[ -n "${owner_uid}" ]] || die "未能确定本地 owner user id"

  export BASE_URL="$(BACKEND_BASE_URL)"
  export ADMIN_TOKEN="${admin_token}"
  export MYSQL_HOST=127.0.0.1 MYSQL_PORT="${mysql_port}" MYSQL_USER=root MYSQL_PASSWORD="${root_pw}" MYSQL_DATABASE="${db}"
  export MYSQL_DSN="root:${root_pw}@tcp(127.0.0.1:${mysql_port})/${db}"
  export OWNER_USER_ID="${owner_uid}"

  run_phase1() { info "=== phase1_auth.sh ==="; bash "${e2e_dir}/phase1_auth.sh"; }
  run_phase2() {
    info "=== phase2_keys.sh ==="
    export USER_A_TOKEN; USER_A_TOKEN="$(cmd_mint_token "alice.local@example.com" 101)"
    export USER_B_TOKEN; USER_B_TOKEN="$(cmd_mint_token "bob.local@example.com" 102)"
    export LOW_PRIV_TOKEN; LOW_PRIV_TOKEN="$(cmd_mint_token "noperm.local@example.com" 103 "caasm:view_assets")"
    bash "${e2e_dir}/phase2_keys.sh"
  }
  run_phase3() {
    info "=== phase3_quota_openapi.sh ==="
    # phase3 脚本契约：USER_TOKEN 必填（agent:manage_own_keys+caasm:view_assets），
    # QUOTA_RPM 必填且必须与 server 的 agent.default_quota_rpm 一致（.env 里的 SC_LOCAL_QUOTA_RPM）
    export USER_TOKEN; USER_TOKEN="$(cmd_mint_token "phase3.local@example.com" 104)"
    export QUOTA_EXPECTED=enabled
    export QUOTA_RPM="$(env_value SC_LOCAL_QUOTA_RPM)"
    bash "${e2e_dir}/phase3_quota_openapi.sh"
  }
  run_phase5() {
    info "=== phase5_usage.sh ==="
    export USER_TOKEN; USER_TOKEN="$(cmd_mint_token "phase5.local@example.com" 105)"
    export USER_B_TOKEN; USER_B_TOKEN="$(cmd_mint_token "phase5b.local@example.com" 106)"
    bash "${e2e_dir}/phase5_usage.sh"
  }

  case "${target}" in
    phase1) run_phase1 ;;
    phase2) run_phase2 ;;
    phase3) run_phase3 ;;
    phase5) run_phase5 ;;
    all)    run_phase1; run_phase2; run_phase3; run_phase5 ;;
    *) die "e2e: unknown target ${target}（phase1|phase2|phase3|phase5|all）" ;;
  esac
  info "E2E 完成: ${target}"
}

cmd_mysql() {
  ensure_prereqs
  compose exec mysql mysql -uroot -p"$(env_value SC_LOCAL_MYSQL_ROOT_PASSWORD)" "$(env_value SC_LOCAL_MYSQL_DATABASE)"
}

main() {
  local cmd="${1:-}"; shift || true
  case "${cmd}" in
    up)         cmd_up "$@" ;;
    down)       cmd_down ;;
    clean)      cmd_clean "$@" ;;
    status)     cmd_status ;;
    logs)       cmd_logs "$@" ;;
    token)      cmd_token ;;
    mint-token) cmd_mint_token "$@" ;;
    e2e)        cmd_e2e "$@" ;;
    mysql)      cmd_mysql ;;
    ""|-h|--help|help) usage ;;
    *) die "unknown command: ${cmd}（--help 查看用法）" ;;
  esac
}

main "$@"
