#!/usr/bin/env bash
# check-env-coverage.sh —— env 三层一致性检查
#
# 三层检查（5 条规则·规则 1：契约面唯一真相源 + 派生位钉死 + 自动校验）：
#
#   ① 正向（默认 mode）：代码 process.env.X → .env.example 必须声明
#   ② 反向（默认 mode 一并跑）：.env.example 列了但代码不引（zombie）
#   ③ 部署对齐（--check-deploy <env> [path]）：实际 .env.{uat,pro} key 集
#       vs .env.example 期望集（按"环境必填"标签分类，只查 key 不查 value）
#
# .env.example 注释标签语法（向后兼容，无标签 = 全环境必填）：
#   行内：KEY=value  # [prod-only]   仅 production 必填
#   行内：KEY=value  # [dev-only]    仅 dev 必填
#   行内：KEY=value  # [optional]    所有环境可空（外部集成功能等）
#   段头：# === [prod-only] xxx ===   覆盖整段（直到下一个段头）
#
# 不依赖任何本地 .env（除非 --check-deploy 显式指定），纯静态扫描，CI 友好。
# 不能识别 process.env[dynamicKey] 这种动态访问（项目里几乎没有）。

set -eo pipefail
# 不开 -u：bash 对空的 declare -A 关联数组的 ${#x[@]} 在 set -u 下会报 unbound variable

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
EXAMPLE_FILE="${ROOT_DIR}/.env.example"
BASELINE_FILE="${ROOT_DIR}/scripts/ops/env-coverage-baseline.txt"

# ----- mode 解析 -----
MODE="audit"           # audit | check-deploy
DEPLOY_ENV=""          # uat | pro
DEPLOY_FILE=""

usage() {
  cat <<'EOF'
check-env-coverage.sh —— env 三层一致性检查

用法:
  check-env-coverage.sh                                # 默认 audit：① 正向 + ② 反向（zombie）
  check-env-coverage.sh --check-deploy <uat|pro> [<env-file>]
                                                       # ③ 部署对齐：传入实际 env 文件，验 key 集
  check-env-coverage.sh -h | --help

audit mode 退出码:
  0  全部对齐 / 仅有 baseline 缓刑或 zombie WARN
  1  代码引了但 .env.example 未声明（baseline 也没有）

check-deploy mode 退出码:
  0  实际 env 文件覆盖了期望必填集
  1  实际 env 缺少必填 key（按 .env.example 注释标签算期望集）
  2  参数错误 / 文件不存在

EOF
}

while [[ $# -gt 0 ]]; do
  case "$1" in
    --check-deploy)
      MODE="check-deploy"
      DEPLOY_ENV="${2:-}"
      [[ "${DEPLOY_ENV}" == "uat" || "${DEPLOY_ENV}" == "pro" ]] \
        || { echo "❌ --check-deploy 后必须跟 uat 或 pro" >&2; exit 2; }
      shift 2
      # 可选第三个参数 = env 文件路径（缺省按 ROOT_DIR/.env.<env>）
      if [[ $# -gt 0 && "$1" != -* ]]; then
        DEPLOY_FILE="$1"; shift
      else
        DEPLOY_FILE="${ROOT_DIR}/.env.${DEPLOY_ENV}"
      fi
      ;;
    -h|--help) usage; exit 0 ;;
    *) echo "❌ 未知参数: $1" >&2; usage; exit 2 ;;
  esac
done

if [[ ! -f "${EXAMPLE_FILE}" ]]; then
  echo "❌ .env.example 不存在: ${EXAMPLE_FILE}" >&2
  exit 1
fi

# Baseline：历史遗留的 missing key 暂不阻断（ratchet 模式收紧）
# 新增违规仍会 fail；清理后从 baseline 删行即可永久收紧。
declare -A baseline_keys
if [[ -f "${BASELINE_FILE}" ]]; then
  while read -r key; do
    [[ -z "${key}" || "${key}" =~ ^# ]] && continue
    baseline_keys[${key}]=1
  done < "${BASELINE_FILE}"
fi

# Linux/Node 自动注入的环境变量，不要求 .env.example 列出
AMBIENT_ENVS=(
  NODE_ENV NODE_OPTIONS PATH HOME USER PWD SHELL
  npm_lifecycle_event npm_package_version
  CI GITHUB_ACTIONS
  TZ LANG LC_ALL
)

is_ambient() {
  local key="$1"
  for a in "${AMBIENT_ENVS[@]}"; do
    [[ "${key}" == "${a}" ]] && return 0
  done
  return 1
}

# 1. 提取代码里所有 process.env.X 引用
declare -A code_envs
while read -r key; do
  [[ -z "${key}" ]] && continue
  is_ambient "${key}" && continue
  code_envs[${key}]=1
done < <(
  grep -rhoE "process\.env\.[A-Z_][A-Z0-9_]*" \
    --include='*.ts' --include='*.tsx' \
    --include='*.js' --include='*.jsx' \
    "${ROOT_DIR}/backend/src" "${ROOT_DIR}/frontend/src" 2>/dev/null \
    | sed 's/^process\.env\.//' \
    | sort -u
)

# 2. 提取 .env.example 里所有 KEY= + 解析环境标签
#    段头（=== 大段 / --- 小段，自动识别）：
#      # === [prod-only] xxx ===   或   # --- xxx [可选] ---
#    行内：KEY=value  # [prod-only]
#    标签别名：[可选] = [optional]（保留中文用语兼容）
#    无标签 = 全环境必填
declare -A example_envs
declare -A example_tags    # key → "prod-only" / "dev-only" / "optional" / ""
current_section_tag=""

# 把任意标签字面量归一化到内部规范名
# 返回值：
#   "optional" / "prod-only" / "dev-only" → 环境分类 tag（影响 deploy mode 必填集计算）
#   "_doc"                                 → .env.example 老标注体系（[自动生成]/[默认可用]），不分类、不警告
#   ""                                     → 未识别（视为分类未知，audit 报告里 warn）
normalize_tag() {
  case "$1" in
    optional|可选*)   echo "optional" ;;   # `可选*` 兼容 `可选 - 通知功能` 等说明性变体
    prod-only|仅生产) echo "prod-only" ;;
    dev-only|仅开发)  echo "dev-only" ;;
    自动生成|默认可用) echo "_doc" ;;
    *) echo "" ;;
  esac
}

unknown_tags=()    # 拼写错的 tag 收集，audit 报告里 warn（避免静默归零成"全环境必填"）

while IFS= read -r line; do
  # 段头：# === xxx ===  或  # --- xxx ---  → 先重置 tag，再看段头是否带 [tag]
  if [[ "${line}" =~ ^#[[:space:]]*(={3,}|-{3,}) ]]; then
    current_section_tag=""
    if [[ "${line}" =~ \[([^\]]+)\] ]]; then
      lit="${BASH_REMATCH[1]}"
      norm="$(normalize_tag "${lit}")"
      case "${norm}" in
        _doc|"") [[ "${norm}" == "" ]] && unknown_tags+=("[${lit}] (in section header)") ;;
        *) current_section_tag="${norm}" ;;
      esac
    fi
    continue
  fi
  # KEY=value 行
  if [[ "${line}" =~ ^([A-Z_][A-Z0-9_]*)= ]]; then
    k="${BASH_REMATCH[1]}"
    example_envs[${k}]=1
    if [[ "${line}" =~ \#[[:space:]]*\[([^\]]+)\] ]]; then
      lit="${BASH_REMATCH[1]}"
      norm="$(normalize_tag "${lit}")"
      case "${norm}" in
        _doc) example_tags[${k}]="${current_section_tag}" ;;       # _doc 等于继承段头
        "")   unknown_tags+=("[${lit}] (key=${k})")
              example_tags[${k}]="${current_section_tag}" ;;
        *)    example_tags[${k}]="${norm}" ;;
      esac
    else
      example_tags[${k}]="${current_section_tag}"
    fi
  fi
done < "${EXAMPLE_FILE}"

# ==== check-deploy mode 分支 ====
if [[ "${MODE}" == "check-deploy" ]]; then
  if [[ ! -f "${DEPLOY_FILE}" ]]; then
    echo "❌ deploy env file 不存在: ${DEPLOY_FILE}" >&2
    exit 2
  fi

  # 提取实际 env 文件的 key 集（只看 ^KEY=，不看 value）
  # 不过滤 ambient：.env.{uat,pro} 显式写 NODE_ENV=production 是合理且必要的，
  # 应该跟 .env.example 的声明对齐，不能在这里偷偷剔除。
  declare -A deploy_envs
  while IFS= read -r line; do
    if [[ "${line}" =~ ^([A-Z_][A-Z0-9_]*)= ]]; then
      k="${BASH_REMATCH[1]}"
      deploy_envs[${k}]=1
    fi
  done < "${DEPLOY_FILE}"

  # 算期望"必填集"：example 里所有 key，排除 optional 和不属于本环境的标签
  required=()
  for k in "${!example_envs[@]}"; do
    tag="${example_tags[${k}]:-}"
    case "${tag}" in
      optional) continue ;;
      prod-only) [[ "${DEPLOY_ENV}" == "pro" ]] || continue ;;
      dev-only) continue ;;  # dev-only 在 uat/pro 都不必填
      *) ;;  # 默认（全环境必填）
    esac
    required+=("${k}")
  done

  # 必填但 deploy 缺
  missing_required=()
  for k in "${required[@]}"; do
    [[ -z "${deploy_envs[${k}]:-}" ]] && missing_required+=("${k}")
  done

  # deploy 多出（不在 example）
  extra_in_deploy=()
  for k in "${!deploy_envs[@]}"; do
    [[ -z "${example_envs[${k}]:-}" ]] && extra_in_deploy+=("${k}")
  done

  echo "=== check-deploy report (env=${DEPLOY_ENV}, file=${DEPLOY_FILE}) ==="
  echo "期望必填 key 数: ${#required[@]}"
  echo "实际 env 文件 key 数: ${#deploy_envs[@]}"
  echo ""

  if [[ ${#missing_required[@]} -gt 0 ]]; then
    echo "❌ 实际 env 缺少必填 key（${#missing_required[@]} 项，必须在机器 .env 上补）："
    printf '%s\n' "${missing_required[@]}" | sort | sed 's/^/   - /'
    echo ""
  fi

  if [[ ${#extra_in_deploy[@]} -gt 0 ]]; then
    echo "⚠️  实际 env 有但 .env.example 未列（${#extra_in_deploy[@]} 项，建议补 example 或从机器删除）："
    printf '%s\n' "${extra_in_deploy[@]}" | sort | sed 's/^/   - /'
    echo ""
  fi

  if [[ ${#missing_required[@]} -eq 0 ]]; then
    echo "✅ 必填 key 集对齐（extra 项不阻断，仅提示）"
    exit 0
  fi
  exit 1
fi

# ==== audit mode（默认）====

# 3. 对比
missing=()
grandfathered=()
zombie=()
stale_baseline=()
for k in "${!code_envs[@]}"; do
  if [[ -z "${example_envs[${k}]:-}" ]]; then
    if [[ -n "${baseline_keys[${k}]:-}" ]]; then
      grandfathered+=("${k}")
    else
      missing+=("${k}")
    fi
  fi
done

# 反向：baseline 列了但代码已不引用 → 鼓励删 baseline 行（不阻断）
for k in "${!baseline_keys[@]}"; do
  if [[ -z "${code_envs[${k}]:-}" ]]; then
    stale_baseline+=("${k}")
  fi
done

zombie=()
for k in "${!example_envs[@]}"; do
  if [[ -z "${code_envs[${k}]:-}" ]]; then
    zombie+=("${k}")
  fi
done

# 4. 输出报告
echo "=== env-coverage report (audit mode) ==="
echo "代码引用的 env: ${#code_envs[@]}"
echo ".env.example 声明的 env: ${#example_envs[@]}"
echo "baseline 缓刑: ${#grandfathered[@]} / ${#baseline_keys[@]}"

# tag 分布统计
tag_default=0; tag_prod=0; tag_dev=0; tag_optional=0
for k in "${!example_tags[@]}"; do
  case "${example_tags[${k}]}" in
    prod-only) tag_prod=$((tag_prod+1)) ;;
    dev-only)  tag_dev=$((tag_dev+1)) ;;
    optional)  tag_optional=$((tag_optional+1)) ;;
    *)         tag_default=$((tag_default+1)) ;;
  esac
done
echo ".env.example 标签分布: 全环境必填=${tag_default} prod-only=${tag_prod} dev-only=${tag_dev} optional=${tag_optional}"
if [[ ${#unknown_tags[@]} -gt 0 ]]; then
  echo "⚠️  未识别 tag（拼写错？已静默归"全环境必填"，请人工修正）："
  printf '%s\n' "${unknown_tags[@]}" | sort -u | sed 's/^/   - /'
fi
echo ""

if [[ ${#missing[@]} -gt 0 ]]; then
  echo "❌ 新增：代码引用但 .env.example 未声明，且不在 baseline（${#missing[@]} 项，必须修）："
  printf '%s\n' "${missing[@]}" | sort | sed 's/^/   - /'
  echo ""
fi

if [[ ${#stale_baseline[@]} -gt 0 ]]; then
  echo "🧹 baseline 里这些 key 代码已不再引用，建议从 baseline 删除（${#stale_baseline[@]} 项）："
  printf '%s\n' "${stale_baseline[@]}" | sort | sed 's/^/   - /'
  echo ""
fi

if [[ ${#zombie[@]} -gt 0 ]]; then
  echo "⚠️  .env.example 声明但代码不再引用（${#zombie[@]} 项，建议清理 example）："
  printf '%s\n' "${zombie[@]}" | sort | sed 's/^/   - /'
  echo ""
fi

if [[ ${#missing[@]} -gt 0 ]]; then
  cat >&2 <<EOF
💡 修法二选一：
   1. 推荐：把缺失的 key 加到 .env.example，附注释说明用途和缺失症状
   2. 不便立即修：把 key 加到 ${BASELINE_FILE#${ROOT_DIR}/} 一行（仅在历史遗留时使用）
EOF
  exit 1
fi

if [[ ${#missing[@]} -eq 0 && ${#zombie[@]} -eq 0 && ${#grandfathered[@]} -eq 0 ]]; then
  echo "✅ 完全对齐"
fi

exit 0
