#!/usr/bin/env bash

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$SCRIPT_DIR"
SCRIPT_DIR_WINDOWS="$(pwd -W 2>/dev/null || true)"
SCRIPT_DIR_WINDOWS="${SCRIPT_DIR_WINDOWS//\\//}"

DEFAULT_DRIVERS=(mariadb oceanbase doris starrocks sphinx sqlserver sqlite duckdb dameng kingbase highgo vastbase opengauss iris mongodb tdengine clickhouse)
TARGET_PLATFORMS=(darwin/amd64 darwin/arm64 windows/amd64 windows/arm64 linux/amd64)

usage() {
  cat <<'EOF'
用法：
  ./tools/detect-changed-driver-agents.sh --base <ref> [--head <ref>]

输出：
  逗号分隔的 driver-agent 列表；没有 driver-agent 相关变更时输出空行。

说明：
  通过 go list -deps 计算每个 driver-agent 的真实源码依赖，再与 git diff 文件求交集。
  如果无法解析基准或依赖分析失败，会保守输出全部 driver。
EOF
}

join_drivers() {
  local IFS=,
  echo "$*"
}

all_drivers_csv() {
  join_drivers "${DEFAULT_DRIVERS[@]}"
}

is_dependency_source_file() {
  case "$1" in
    *.go|*.c|*.cc|*.cpp|*.cxx|*.h|*.hpp|*.m|*.mm|*.s|*.S|*.syso)
      return 0
      ;;
    *)
      return 1
      ;;
  esac
}

normalize_driver() {
  local value
  value="$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')"
  case "$value" in
    doris|diros) echo "doris" ;;
    open_gauss|open-gauss) echo "opengauss" ;;
    mariadb|oceanbase|starrocks|sphinx|sqlserver|sqlite|duckdb|dameng|kingbase|highgo|vastbase|opengauss|iris|mongodb|tdengine|clickhouse)
      echo "$value"
      ;;
    *)
      return 1
      ;;
  esac
}

build_driver_name() {
  case "$1" in
    doris) echo "diros" ;;
    *) echo "$1" ;;
  esac
}

driver_build_tags() {
  local driver="$1"
  local platform="$2"
  local goos="${platform%%/*}"
  local goarch="${platform##*/}"
  local build_driver tag
  build_driver="$(build_driver_name "$driver")"
  tag="gonavi_${build_driver}_driver"
  if [[ "$driver" == "duckdb" && "$goos" == "windows" && "$goarch" == "amd64" ]]; then
    tag="$tag duckdb_use_lib"
  fi
  echo "$tag"
}

all_driver_build_tags() {
  local platform="$1"
  local -a tags=()
  local driver
  for driver in "${DEFAULT_DRIVERS[@]}"; do
    tags+=("$(driver_build_tags "$driver" "$platform")")
  done
  local IFS=" "
  echo "${tags[*]}"
}

driver_cgo_enabled() {
  # Detection only needs the Go dependency graph; keeping CGO off avoids
  # cross-platform cgo toolchain requirements in the Ubuntu detection job.
  echo 0
}

relative_repo_path() {
  local path="$1"
  path="${path//\\//}"
  case "$path" in
    "$SCRIPT_DIR"/*)
      path="${path#$SCRIPT_DIR/}"
      ;;
  esac
  if [[ -n "$SCRIPT_DIR_WINDOWS" ]]; then
    case "$path" in
      "$SCRIPT_DIR_WINDOWS"/*)
        path="${path#$SCRIPT_DIR_WINDOWS/}"
        ;;
    esac
  fi
  path="${path#./}"
  printf '%s\n' "$path"
}

add_analysis_platform() {
  local platform="$1"
  if [[ "$analysis_platform_seen" == *"|$platform|"* ]]; then
    return 0
  fi
  analysis_platforms+=("$platform")
  analysis_platform_seen="${analysis_platform_seen}${platform}|"
}

add_forced_driver() {
  local driver
  driver="$(normalize_driver "$1")" || return 0
  if [[ "$forced_driver_seen" == *"|$driver|"* ]]; then
    return 0
  fi
  forced_changed_drivers+=("$driver")
  forced_driver_seen="${forced_driver_seen}${driver}|"
}

driver_tokens_from_text() {
  local text
  local emitted_seen
  text="${1,,}"
  emitted_seen="|"

  case "$text" in *mariadb*) emit_driver_token mariadb ;; esac
  case "$text" in *oceanbase*) emit_driver_token oceanbase ;; esac
  case "$text" in *doris*|*diros*) emit_driver_token doris ;; esac
  case "$text" in *starrocks*) emit_driver_token starrocks ;; esac
  case "$text" in *sphinx*) emit_driver_token sphinx ;; esac
  case "$text" in *sqlserver*) emit_driver_token sqlserver ;; esac
  case "$text" in *sqlite*) emit_driver_token sqlite ;; esac
  case "$text" in *duckdb*) emit_driver_token duckdb ;; esac
  case "$text" in *dameng*) emit_driver_token dameng ;; esac
  case "$text" in *kingbase*) emit_driver_token kingbase ;; esac
  case "$text" in *highgo*) emit_driver_token highgo ;; esac
  case "$text" in *vastbase*) emit_driver_token vastbase ;; esac
  case "$text" in *opengauss*) emit_driver_token opengauss ;; esac
  case "$text" in *iris*) emit_driver_token iris ;; esac
  case "$text" in *mongodb*) emit_driver_token mongodb ;; esac
  case "$text" in *tdengine*) emit_driver_token tdengine ;; esac
  case "$text" in *clickhouse*) emit_driver_token clickhouse ;; esac

  case "$text" in
    *github.com/go-sql-driver/mysql*)
      emit_driver_token mariadb
      emit_driver_token oceanbase
      emit_driver_token doris
      emit_driver_token starrocks
      emit_driver_token sphinx
      ;;
  esac
  case "$text" in *github.com/microsoft/go-mssqldb*) emit_driver_token sqlserver ;; esac
  case "$text" in *modernc.org/sqlite*) emit_driver_token sqlite ;; esac
  case "$text" in *github.com/duckdb/duckdb-go/v2*|*github.com/duckdb/duckdb-go-bindings*) emit_driver_token duckdb ;; esac
  case "$text" in *gitee.com/chunanyong/dm*) emit_driver_token dameng ;; esac
  case "$text" in *gitea.com/kingbase/gokb*) emit_driver_token kingbase ;; esac
  case "$text" in *github.com/highgo/pq-sm3*|*third_party/highgo-pq*) emit_driver_token highgo ;; esac
  case "$text" in
    *github.com/lib/pq*)
      emit_driver_token vastbase
      emit_driver_token opengauss
      ;;
  esac
  case "$text" in *github.com/caretdev/go-irisnative*|*third_party/go-irisnative*) emit_driver_token iris ;; esac
  case "$text" in *go.mongodb.org/mongo-driver*|*go.mongodb.org/mongo-driver/v2*) emit_driver_token mongodb ;; esac
  case "$text" in *github.com/taosdata/driver-go/v3*) emit_driver_token tdengine ;; esac
  case "$text" in *github.com/clickhouse/clickhouse-go/v2*|*github.com/clickhouse/ch-go*) emit_driver_token clickhouse ;; esac
}

emit_driver_token() {
  local driver
  driver="$(normalize_driver "$1")" || return 0
  if [[ "$emitted_seen" == *"|$driver|"* ]]; then
    return 0
  fi
  printf '%s\n' "$driver"
  emitted_seen="${emitted_seen}${driver}|"
}

shared_file_driver_delta() {
  local file="$1"
  local line text tokens token added_seen removed_seen
  local touched_seen emitted_seen
  local saw_unattributed_change=false
  added_seen="|"
  removed_seen="|"
  touched_seen="|"

  while IFS= read -r line; do
    case "$line" in
      +++*|---*|@@*)
        continue
        ;;
      +*|-*)
        text="${line:1}"
        case "$text" in
          *[![:space:]]*) ;;
          *) continue ;;
        esac
        tokens="$(driver_tokens_from_text "$text")"
        if [[ -z "$tokens" ]]; then
          saw_unattributed_change=true
          continue
        fi
        while IFS= read -r token; do
          [[ -n "$token" ]] || continue
          touched_seen="${touched_seen}${token}|"
          case "$line" in
            +*) added_seen="${added_seen}${token}|" ;;
            -*) removed_seen="${removed_seen}${token}|" ;;
          esac
        done <<<"$tokens"
        ;;
    esac
  done < <(git diff --unified=0 "$base_commit" "$head_commit" -- "$file")

  if [[ "$saw_unattributed_change" == "true" ]]; then
    return 1
  fi

  if [[ "$file" == "go.mod" || "$file" == "go.sum" ]]; then
    emitted_seen="|"
    for driver in "${DEFAULT_DRIVERS[@]}"; do
      if [[ "$touched_seen" == *"|$driver|"* && "$emitted_seen" != *"|$driver|"* ]]; then
        printf '%s\n' "$driver"
        emitted_seen="${emitted_seen}${driver}|"
      fi
    done
    return 0
  fi

  emitted_seen="|"
  for driver in "${DEFAULT_DRIVERS[@]}"; do
    if [[ "$added_seen" == *"|$driver|"* && "$removed_seen" != *"|$driver|"* ]]; then
      printf '%s\n' "$driver"
      emitted_seen="${emitted_seen}${driver}|"
      continue
    fi
    if [[ "$removed_seen" == *"|$driver|"* && "$added_seen" != *"|$driver|"* ]]; then
      printf '%s\n' "$driver"
      emitted_seen="${emitted_seen}${driver}|"
    fi
  done
  if [[ "$emitted_seen" != "|" ]]; then
    return 0
  fi

  for driver in "${DEFAULT_DRIVERS[@]}"; do
    if [[ "$touched_seen" == *"|$driver|"* ]]; then
      printf '%s\n' "$driver"
    fi
  done
}

source_file_driver_tokens() {
  local file="$1"
  local line text tokens token touched_seen emitted_seen
  touched_seen="|"

  while IFS= read -r line; do
    case "$line" in
      +++*|---*|@@*)
        continue
        ;;
      +*|-*)
        text="${line:1}"
        tokens="$(driver_tokens_from_text "$text")"
        while IFS= read -r token; do
          [[ -n "$token" ]] || continue
          touched_seen="${touched_seen}${token}|"
        done <<<"$tokens"
        ;;
    esac
  done < <(git diff --unified=0 "$base_commit" "$head_commit" -- "$file")

  emitted_seen="|"
  for driver in "${DEFAULT_DRIVERS[@]}"; do
    if [[ "$touched_seen" == *"|$driver|"* && "$emitted_seen" != *"|$driver|"* ]]; then
      printf '%s\n' "$driver"
      emitted_seen="${emitted_seen}${driver}|"
    fi
  done
}

add_forced_drivers_from_tokens() {
  local tokens="$1"
  local driver
  while IFS= read -r driver; do
    [[ -n "$driver" ]] || continue
    add_forced_driver "$driver"
  done <<<"$tokens"
}

add_all_forced_drivers() {
  local driver
  for driver in "${DEFAULT_DRIVERS[@]}"; do
    add_forced_driver "$driver"
  done
}

is_ignored_driver_agent_source_file() {
  case "$1" in
    *_test.go|frontend/*|internal/app/*|internal/db/driver_agent_revisions_gen.go)
      return 0
      ;;
  esac
  return 1
}

attribute_source_file_change() {
  local file="$1"
  local tokens

  if is_ignored_driver_agent_source_file "$file"; then
    return 0
  fi

  tokens="$(driver_tokens_from_text "$file")"
  if [[ -n "$tokens" ]]; then
    add_forced_drivers_from_tokens "$tokens"
    return 0
  fi

  tokens="$(source_file_driver_tokens "$file")"
  if [[ -n "$tokens" ]]; then
    add_forced_drivers_from_tokens "$tokens"
    return 0
  fi

  case "$file" in
    cmd/optional-driver-agent/*.go|internal/db/*.go)
      add_all_forced_drivers
      return 0
      ;;
  esac

  return 1
}

list_dependency_files() {
  local tags="$1"
  local cgo_enabled="$2"
  local goos="$3"
  local goarch="$4"
  local output="$5"

  CGO_ENABLED="$cgo_enabled" GOOS="$goos" GOARCH="$goarch" GOTOOLCHAIN=auto \
    go list -deps \
      -tags "$tags" \
      -f '{{if not .Standard}}{{range .GoFiles}}{{$.Dir}}/{{.}}{{"\n"}}{{end}}{{range .CgoFiles}}{{$.Dir}}/{{.}}{{"\n"}}{{end}}{{range .CFiles}}{{$.Dir}}/{{.}}{{"\n"}}{{end}}{{range .CXXFiles}}{{$.Dir}}/{{.}}{{"\n"}}{{end}}{{range .MFiles}}{{$.Dir}}/{{.}}{{"\n"}}{{end}}{{range .HFiles}}{{$.Dir}}/{{.}}{{"\n"}}{{end}}{{range .SFiles}}{{$.Dir}}/{{.}}{{"\n"}}{{end}}{{range .SysoFiles}}{{$.Dir}}/{{.}}{{"\n"}}{{end}}{{end}}' \
      ./cmd/optional-driver-agent | sort -u >"$output"
}

dependency_union_contains_changed_files() {
  local platform goos goarch tags tmp file rel
  local matched_any=false
  for platform in "${TARGET_PLATFORMS[@]}"; do
    goos="${platform%%/*}"
    goarch="${platform##*/}"
    tags="$(all_driver_build_tags "$platform")"
    tmp="$(mktemp "${TMPDIR:-/tmp}/gonavi-driver-dep-union.XXXXXX")"

    if ! list_dependency_files "$tags" 0 "$goos" "$goarch" "$tmp"; then
      rm -f "$tmp"
      return 2
    fi

    while IFS= read -r file; do
      [[ -n "$file" ]] || continue
      rel="$(relative_repo_path "$file")"
      if [[ -n "${changed_file_set[$rel]:-}" ]]; then
        dependency_matched_platforms["$rel"]="${dependency_matched_platforms[$rel]:-}|$platform|"
        matched_any=true
      fi
    done <"$tmp"

    rm -f "$tmp"
  done
  if [[ "$matched_any" == "true" ]]; then
    return 0
  fi
  return 1
}

base_ref=""
head_ref="HEAD"

while [[ $# -gt 0 ]]; do
  case "$1" in
    --base)
      base_ref="${2:-}"
      shift 2
      ;;
    --head)
      head_ref="${2:-}"
      shift 2
      ;;
    -h|--help)
      usage
      exit 0
      ;;
    *)
      echo "未知参数：$1" >&2
      usage >&2
      exit 1
      ;;
  esac
done

if [[ -z "$base_ref" ]]; then
  echo "缺少 --base 参数。" >&2
  usage >&2
  exit 1
fi

if [[ "$base_ref" == "all" ]]; then
  all_drivers_csv
  exit 0
fi

if ! git rev-parse --verify "${head_ref}^{commit}" >/dev/null 2>&1; then
  echo "无法解析 head ref：$head_ref" >&2
  exit 1
fi
head_commit="$(git rev-parse "${head_ref}^{commit}")"

if ! git rev-parse --verify "${base_ref}^{commit}" >/dev/null 2>&1; then
  echo "无法解析 base ref：$base_ref；保守构建全部 driver-agent。" >&2
  all_drivers_csv
  exit 0
fi
base_commit="$(git rev-parse "${base_ref}^{commit}")"

declare -A changed_file_set=()
while IFS= read -r -d '' file; do
  file="$(relative_repo_path "$file")"
  [[ -n "$file" ]] || continue
  changed_file_set["$file"]=1
done < <(git diff --name-only -z "$base_commit" "$head_commit")

if [[ ${#changed_file_set[@]} -eq 0 ]]; then
  echo ""
  exit 0
fi

declare -a forced_changed_drivers=()
forced_driver_seen="|"
for file in "${!changed_file_set[@]}"; do
  case "$file" in
    go.mod|go.sum|build-driver-agents.sh|tools/generate-driver-agent-revisions.sh)
      set +e
      shared_delta="$(shared_file_driver_delta "$file")"
      shared_status=$?
      set -e
      if [[ "$shared_status" -ne 0 ]]; then
        echo "检测到共享 driver-agent 输入存在无法归因的变更；保守构建全部 driver-agent：$file" >&2
        all_drivers_csv
        exit 0
      fi
      add_forced_drivers_from_tokens "$shared_delta"
      ;;
    tools/compress-driver-artifact.sh)
      echo "检测到 driver-agent 压缩脚本变更；保守构建全部 driver-agent：$file" >&2
      all_drivers_csv
      exit 0
      ;;
    tools/detect-changed-driver-agents.sh)
      # This script only selects CI work; it is not embedded in driver-agent binaries.
      ;;
  esac
done

for file in "${!changed_file_set[@]}"; do
  case "$file" in
    internal/db/duckdb_*.go)
      add_forced_driver duckdb
      ;;
  esac
done

has_unattributed_source_candidate=false
for file in "${!changed_file_set[@]}"; do
  if is_dependency_source_file "$file"; then
    if attribute_source_file_change "$file"; then
      continue
    fi
    has_unattributed_source_candidate=true
    break
  fi
done

if [[ "$has_unattributed_source_candidate" != "true" ]]; then
  join_drivers "${forced_changed_drivers[@]}"
  exit 0
fi

while IFS= read -r -d '' file; do
  file="$(relative_repo_path "$file")"
  if is_dependency_source_file "$file"; then
    if attribute_source_file_change "$file"; then
      continue
    fi
    echo "检测到源码依赖候选文件被删除；保守构建全部 driver-agent：$file" >&2
    all_drivers_csv
    exit 0
  fi
done < <(git diff --name-only --diff-filter=D -z "$base_commit" "$head_commit")

declare -A dependency_matched_platforms=()
set +e
dependency_union_contains_changed_files
dependency_union_status=$?
set -e
case "$dependency_union_status" in
  0)
    ;;
  1)
    if [[ ${#forced_changed_drivers[@]} -eq 0 ]]; then
      echo ""
      exit 0
    fi
    ;;
  *)
    echo "分析 driver-agent 依赖全集失败；保守构建全部 driver-agent。" >&2
    all_drivers_csv
    exit 0
    ;;
esac

declare -a analysis_platforms=()
analysis_platform_seen="|"
for file in "${!dependency_matched_platforms[@]}"; do
  matched_platforms="${dependency_matched_platforms[$file]}"
  if [[ "$matched_platforms" == *"|linux/amd64|"* ]]; then
    add_analysis_platform "linux/amd64"
    continue
  fi
  for platform in "${TARGET_PLATFORMS[@]}"; do
    if [[ "$matched_platforms" == *"|$platform|"* ]]; then
      add_analysis_platform "$platform"
    fi
  done
done
if [[ ${#analysis_platforms[@]} -eq 0 ]]; then
  analysis_platforms=("${TARGET_PLATFORMS[@]}")
fi

declare -a changed_drivers=()
driver_seen="|"

add_driver() {
  local driver
  driver="$(normalize_driver "$1")" || return 0
  if [[ "$driver_seen" == *"|$driver|"* ]]; then
    return 0
  fi
  changed_drivers+=("$driver")
  driver_seen="${driver_seen}${driver}|"
}

for driver in "${forced_changed_drivers[@]}"; do
  add_driver "$driver"
done

driver_depends_on_changed_files() {
  local driver="$1"
  local platform="$2"
  local goos="${platform%%/*}"
  local goarch="${platform##*/}"
  local tags cgo_enabled file rel tmp
  tags="$(driver_build_tags "$driver" "$platform")"
  cgo_enabled="$(driver_cgo_enabled "$driver")"
  tmp="$(mktemp "${TMPDIR:-/tmp}/gonavi-driver-deps.XXXXXX")"

  if ! list_dependency_files "$tags" "$cgo_enabled" "$goos" "$goarch" "$tmp"; then
    rm -f "$tmp"
    return 2
  fi

  while IFS= read -r file; do
    [[ -n "$file" ]] || continue
    rel="$(relative_repo_path "$file")"
    if [[ -n "${changed_file_set[$rel]:-}" ]]; then
      rm -f "$tmp"
      return 0
    fi
  done <"$tmp"

  rm -f "$tmp"
  return 1
}

for driver in "${DEFAULT_DRIVERS[@]}"; do
  driver_changed=false
  for platform in "${analysis_platforms[@]}"; do
    set +e
    driver_depends_on_changed_files "$driver" "$platform"
    status=$?
    set -e
    case "$status" in
      0)
        add_driver "$driver"
        driver_changed=true
        break
        ;;
      1)
        ;;
      *)
        echo "分析 $driver driver-agent 依赖失败；保守构建全部 driver-agent。" >&2
        all_drivers_csv
        exit 0
        ;;
    esac
  done
done

join_drivers "${changed_drivers[@]}"
