#!/usr/bin/env bash
set -euo pipefail

ROOT="${AGENTTEAM_MESSAGE_GUARDRAIL_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
cd "$ROOT"

failures=0

fail() {
  printf 'FAIL: %s\n' "$1" >&2
  failures=$((failures + 1))
}

existing_paths() {
  local path
  for path in "$@"; do
    [[ -e "$path" ]] && printf '%s\n' "$path"
  done
}

read_existing_paths() {
  local array_name="$1"
  shift
  local path
  eval "${array_name}=()"
  while IFS= read -r path; do
    [[ -n "$path" ]] || continue
    eval "${array_name}+=(\"\$path\")"
  done < <(existing_paths "$@")
}

check_no_matches() {
  local label="$1"
  local pattern="$2"
  shift 2
  local paths=()
  read_existing_paths paths "$@"
  (( ${#paths[@]} == 0 )) && return 0

  local matches
  matches="$(rg -n -P -- "$pattern" "${paths[@]}" 2>/dev/null || true)"
  if [[ -n "$matches" ]]; then
    fail "$label"
    printf '%s\n' "$matches" >&2
  fi
}

legacy_runtime_table_pattern='(?<![[:alnum:]_])(?:space_messages|space_message_sources|space_message_deliveries|targeted_delivery_events|message_search_docs_tidb|message_search_tidb_state|scope_delivery_events|scope_runtime_events|scope_message_routing_annotations)(?![[:alnum:]_])'
legacy_runtime_tail_table_names='channel_agent_entries|channel_share_cards'
legacy_runtime_tail_table_pattern="(?<![A-Za-z0-9_])(?:${legacy_runtime_tail_table_names})(?![A-Za-z0-9_])"
legacy_topology_table_names='channels|channel_profiles|threads|reply_threads|reply_thread_stats|dm_threads|spaces|space_participants|space_subscriptions|thread_subscriptions|thread_read_states|channel_agent_entries|channel_share_cards'
legacy_topology_sql_table_ref_pattern="(?:(?:(?:CREATE|DROP|ALTER|TRUNCATE)[[:space:]]+TABLE(?:[[:space:]]+IF[[:space:]]+(?:NOT[[:space:]]+)?EXISTS)?|FROM|JOIN|INSERT[[:space:]]+(?:IGNORE[[:space:]]+)?INTO|UPDATE|DELETE[[:space:]]+FROM|REFERENCES)[[:space:]]+[\"'\x60]?(${legacy_topology_table_names})[\"'\x60]?(?![[:alnum:]_])|(?:table_name|referenced_table_name)[[:space:]]*=[[:space:]]*[\"'\x60](${legacy_topology_table_names})[\"'\x60])"
legacy_topology_doc_table_ref_pattern="(?:(?:^[[:space:]]*(?:(?:CREATE|DROP|ALTER|TRUNCATE)[[:space:]]+TABLE(?:[[:space:]]+IF[[:space:]]+(?:NOT[[:space:]]+)?EXISTS)?|FROM|JOIN|INSERT[[:space:]]+(?:IGNORE[[:space:]]+)?INTO|UPDATE|DELETE[[:space:]]+FROM|REFERENCES)[[:space:]]+[\"'\x60]?(${legacy_topology_table_names})[\"'\x60]?(?![[:alnum:]_]))|(?i:(?:table|schema|migration|foreign[[:space:]]+key|index|表名|数据表|字段|索引|外键|迁移)[^\n]*[\"'\x60](${legacy_topology_table_names})[\"'\x60]|[\"'\x60](${legacy_topology_table_names})[\"'\x60][^\n]*(?:table|schema|migration|foreign[[:space:]]+key|index|表名|数据表|字段|索引|外键|迁移)))"
legacy_inventory_table_pattern="${legacy_runtime_table_pattern}|${legacy_topology_sql_table_ref_pattern}"

legacy_runtime_matches() {
  local matches
  matches="$(rg -n -P --glob '!**/*_test.go' --glob '!internal/tidb/**' -- "$legacy_runtime_table_pattern" internal cmd desktop web scripts 2>/dev/null || true)"
  printf '%s' "$matches"
}

legacy_runtime_tail_matches() {
  local matches
  matches="$(rg -n -P --glob '!**/*_test.go' --glob '!internal/tidb/**' -- "$legacy_runtime_tail_table_pattern" internal cmd desktop web scripts 2>/dev/null || true)"
  printf '%s' "$matches"
}

legacy_shipped_runtime_sql_matches() {
  local matches
  matches="$(rg -n -P --glob '!**/*_test.go' --glob '!internal/tidb/**' -- "$legacy_inventory_table_pattern" internal cmd desktop web scripts 2>/dev/null || true)"
  printf '%s' "$matches"
}

legacy_runtime_test_matches() {
  local matches
  matches="$(rg -n -P --glob '**/*_test.go' --glob '!internal/tidb/**' -- "$legacy_inventory_table_pattern" internal cmd desktop web scripts 2>/dev/null || true)"
  printf '%s' "$matches"
}

server_agent_receive_runtime_matches() {
  local pattern='AgentReceiveService|NewAgentReceiveService|scopeReceiveVisibilityCandidatesQuery|BuildAgentHasUnreadMessagesQuery|AgentReceiveMessagesResponse|ReceiveAgentMessages[[:space:]]*\(|AgentHasUnreadMessages[[:space:]]*\(|/internal/agent/:agentId/receive|/internal/agent/:id/receive|internal/waiters|package[[:space:]]+waiters'
  local matches
  matches="$(rg -n -P \
    --glob '!**/*_test.go' \
    --glob '!internal/api/testkit/**' \
    --glob '!internal/store/testkit/**' \
    --glob '!internal/store/message/testdelivery/**' \
    --glob '!e2e/testdata/**' \
    -- "$pattern" internal cmd desktop scripts e2e 2>/dev/null || true)"
  printf '%s' "$matches"
}

legacy_canonical_docs_matches() {
  local matches
  matches="$(rg -n -P -- "${legacy_runtime_table_pattern}|${legacy_topology_doc_table_ref_pattern}" SPECS.md specs docs docs/public internal/store/README.md 2>/dev/null || true)"
  printf '%s' "$matches"
}

print_legacy_inventory_section() {
  local label="$1"
  local paths=()
  shift
  printf '\n## %s\n' "$label"
  read_existing_paths paths "$@"
  if (( ${#paths[@]} == 0 )); then
    printf '(none)\n'
    return
  fi
  if ! rg -n -P -- "$legacy_inventory_table_pattern" "${paths[@]}" 2>/dev/null; then
    printf '(none)\n'
  fi
}

print_legacy_inventory_glob_section() {
  local label="$1"
  shift
  printf '\n## %s\n' "$label"
  if ! rg -n -P "$@" -- "$legacy_inventory_table_pattern" internal cmd desktop web scripts 2>/dev/null; then
    printf '(none)\n'
  fi
}

print_legacy_inventory_path_glob_section() {
  local label="$1"
  local path="$2"
  shift 2
  printf '\n## %s\n' "$label"
  if [[ ! -e "$path" ]]; then
    printf '(none)\n'
    return
  fi
  if ! rg -n -P "$@" -- "$legacy_inventory_table_pattern" "$path" 2>/dev/null; then
    printf '(none)\n'
  fi
}

print_legacy_docs_inventory_section() {
  local label="$1"
  local paths=()
  shift
  printf '\n## %s\n' "$label"
  read_existing_paths paths "$@"
  if (( ${#paths[@]} == 0 )); then
    printf '(none)\n'
    return
  fi
  if ! rg -n -P -- "${legacy_runtime_table_pattern}|${legacy_topology_doc_table_ref_pattern}" "${paths[@]}" 2>/dev/null; then
    printf '(none)\n'
  fi
}

print_legacy_inventory() {
  printf '# Message scope cutover legacy/deleted table inventory\n'
  printf 'SQL table-reference sections classify actual legacy/deleted table dependencies. Source-kind literals such as visible_reply_threads are listed separately because they are protocol identifiers, not table reads/writes.\n'
  printf 'Fresh bootstrap, shipped runtime, runtime tests, and canonical/final docs must stay empty; historical upgrade migrations may still contain old-database input until those migrations age out.\n'
  print_legacy_inventory_section 'fresh bootstrap schema (must be empty; fresh init starts from final scope schema baseline)' internal/tidb/bootstrap.sql
  print_legacy_inventory_section 'upgrade migrations / migration fixtures (allowed old-database input; not runtime or fresh-bootstrap evidence)' internal/tidb/migrations
  print_legacy_inventory_path_glob_section 'TiDB migration/admin tests (allowed only for old-database upgrade fixtures; fresh/admin required-schema assertions must be scope-native)' internal/tidb --glob '*_test.go'
  print_legacy_inventory_glob_section 'shipped runtime SQL table dependencies (must be empty)' --glob '!**/*_test.go' --glob '!internal/tidb/**'
  print_legacy_inventory_glob_section 'runtime tests / legacy fixtures (must be empty; old-database migration fixtures only live under internal/tidb)' --glob '**/*_test.go' --glob '!internal/tidb/**'
  print_legacy_docs_inventory_section 'canonical/final docs (must be empty)' SPECS.md specs docs docs/public internal/store/README.md
}

run_checks() {
  check_no_matches \
    "removed scope-message planning/fact tables must not reappear in scope runtime/docs" \
    'PrivateVisibilityMutations|scope_private_context_events|scope_routing_epochs|space_message_deliveries' \
    internal/store/message specs/message docs/public docs/tech_design.md

  check_no_matches \
    "active docs must use scoped workspace/scope message-search admin commands, not old unscoped message-search commands" \
    'agentteam-tidb-admin[[:space:]]+message-search-(backfill|finalize|promote-ready|status)|(?<!scope-)(?<!workspace-)message-search-(backfill|finalize|promote-ready|status)' \
    specs docs docs/public

  check_no_matches \
    "scope history docs must not describe old (space_id, seq) scan contract" \
    '\(space_id,[[:space:]]*seq\)' \
    docs/tech_design.md specs/message

  check_no_matches \
    "scope search API must not fall back to LOWER/LIKE instead of TiDB FULLTEXT" \
    'LOWER[[:space:]]*\(|[[:space:]]LIKE[[:space:]]' \
    internal/store/message/scope_api.go

  check_no_matches \
    "scope search migration must not inherit pre-scope read-ready/finalize state" \
    'state\.`(finalize_status|read_ready)`' \
    internal/tidb/migrations/00116_scope_message_backfill.sql

  check_no_matches \
    "message-backed delivery events must not remain in scope component docs/runtime" \
    'scope_delivery_events|DeliveryEventStore|DeliveryEvents|delivery_event_id' \
    internal/store/message/scope_*.go specs/message docs/tech_design.md specs/storage/storage.md

  check_no_matches \
    "message-backed routing annotation tables must not replace delivery events" \
    'scope_message_routing_annotations|message routing annotations?|routing annotations' \
    internal/store/message/scope_*.go internal/tidb/bootstrap.sql internal/tidb/migrations specs/message docs/tech_design.md specs/storage/storage.md

  check_no_matches \
    "unused scope runtime event ledger must not enter this cutover" \
    'scope_runtime_events|ScopeRuntimeEvent|RuntimeEvidenceStore' \
    internal/store/message/scope_*.go internal/tidb/bootstrap.sql internal/tidb/migrations specs/message docs/tech_design.md specs/storage/storage.md

  check_no_matches \
    "message diagnostics must not use runtime evidence or durable delivery ledger wording" \
    'LoadMessageDeliveryRuntimeEvidence|MessageDeliveryWakeInferenceHistoricalRuntimeEvidence|historical_runtime_evidence|durable delivery fact|agent ledger|message delivery runtime evidence|persisted runtime evidence' \
    internal/store/message internal/store/app internal/store/agentops specs/message docs/tech_design.md specs/storage/storage.md

  local runtime_table_matches
  runtime_table_matches="$(legacy_runtime_matches)"
  if [[ -n "$runtime_table_matches" ]]; then
    fail "shipped runtime code must not reference old/deleted message runtime tables"
    printf '%s\n' "$runtime_table_matches" >&2
  fi

  local shipped_runtime_sql_matches
  shipped_runtime_sql_matches="$(legacy_shipped_runtime_sql_matches)"
  if [[ -n "$shipped_runtime_sql_matches" ]]; then
    fail "shipped runtime code must not contain legacy/deleted scope topology SQL table dependencies"
    printf '%s\n' "$shipped_runtime_sql_matches" >&2
  fi

  local runtime_tail_matches
  runtime_tail_matches="$(legacy_runtime_tail_matches)"
  if [[ -n "$runtime_tail_matches" ]]; then
    fail "shipped runtime code must not reference channel_agent_entries or channel_share_cards"
    printf '%s\n' "$runtime_tail_matches" >&2
  fi

  local server_agent_receive_matches
  server_agent_receive_matches="$(server_agent_receive_runtime_matches)"
  if [[ -n "$server_agent_receive_matches" ]]; then
    fail "production server-side agent receive/cursor/waiter paths must stay removed"
    printf '%s\n' "$server_agent_receive_matches" >&2
  fi

  check_no_matches \
    "fresh TiDB bootstrap must not create or reference legacy/deleted scope tables" \
    "$legacy_inventory_table_pattern" \
    internal/tidb/bootstrap.sql

  local runtime_test_matches
  runtime_test_matches="$(legacy_runtime_test_matches)"
  if [[ -n "$runtime_test_matches" ]]; then
    fail "runtime tests and fixtures must not carry legacy/deleted scope table SQL dependencies"
    printf '%s\n' "$runtime_test_matches" >&2
  fi

  local docs_matches
  docs_matches="$(legacy_canonical_docs_matches)"
  if [[ -n "$docs_matches" ]]; then
    fail "canonical/final docs must not describe legacy/deleted scope storage tables"
    printf '%s\n' "$docs_matches" >&2
  fi

  check_no_matches \
    "scope activity migration must not create message-backed activity rows" \
    "activity_ref_type.*message|message_created" \
    internal/tidb/migrations/00116_scope_message_backfill.sql internal/store/message/scope_planner.go

  check_no_matches \
    "storage spec final-state text must use scope storage facts, not legacy delivery/storage facts" \
    "(?<![A-Za-z0-9_])(?:space_messages|targeted_delivery_events|space_message_deliveries)(?![A-Za-z0-9_])|policy epochs?|policy epoch" \
    specs/storage/storage.md
}

run_self_test() {
  local tmp
  tmp="$(mktemp -d)"
  trap 'rm -rf "$tmp"' RETURN
  mkdir -p "$tmp/internal/store/message" "$tmp/internal/api" "$tmp/specs/message" "$tmp/docs" "$tmp/internal/tidb/migrations"
  cat > "$tmp/internal/store/message/scope_api.go" <<'BAD'
package message

const badSearch = `SELECT * FROM scope_message_search_docs WHERE LOWER(content) LIKE '%needle%'`
BAD
  cat > "$tmp/internal/store/message/scope_specialties.go" <<'BAD'
package message

// missing FULLTEXT finalize contract
BAD
  cat > "$tmp/internal/api/bad_runtime.go" <<'BAD'
package api

const oldRuntimeTable = `SELECT * FROM targeted_delivery_events`
const oldTailTable = `SELECT * FROM channel_share_cards`
type AgentReceiveService struct{}
const removedReceiveRoute = "/internal/agent/:agentId/receive"
BAD
  cat > "$tmp/internal/store/message/bad_fixture_test.go" <<'BAD'
package message

const oldFixtureTable = `INSERT INTO channels (id) VALUES ('chn_bad')`
BAD
  mkdir -p "$tmp/internal/tidb"
  cat > "$tmp/internal/tidb/bootstrap.sql" <<'BAD'
CREATE TABLE channels (id varchar(255) PRIMARY KEY);
BAD
  cat > "$tmp/specs/message/bad.md" <<'BAD'
PrivateVisibilityMutations, agentteam-tidb-admin message-search-backfill, message routing annotations, durable delivery fact, agent ledger, and scope_runtime_events runtime evidence must fail.
BAD
  cat > "$tmp/internal/tidb/migrations/00115_scope_message_schema.sql" <<'BAD'
CREATE TABLE scope_message_routing_annotations (id varchar(255));
CREATE TABLE scope_runtime_events (id varchar(255));
BAD
  cat > "$tmp/docs/tech_design.md" <<'BAD'
Use (space_id, seq) scan.
BAD
  cat > "$tmp/internal/tidb/migrations/00116_scope_message_backfill.sql" <<'BAD'
INSERT INTO scope_activity_events (activity_ref_type, activity_type) VALUES ('message', 'message_created');
INSERT INTO scope_message_search_state SELECT state.`finalize_status`, state.`read_ready` FROM message_search_tidb_state state;
BAD
  mkdir -p "$tmp/specs/storage"
  cat > "$tmp/specs/storage/storage.md" <<'BAD'
Final-state storage uses targeted_delivery_events and policy epochs.
BAD
  if AGENTTEAM_MESSAGE_GUARDRAIL_ROOT="$tmp" "$0" --check-only >/tmp/agentteam-message-guardrail-self-test.out 2>&1; then
    printf 'negative fixture unexpectedly passed\n' >&2
    cat /tmp/agentteam-message-guardrail-self-test.out >&2 || true
    return 1
  fi
  rm -f /tmp/agentteam-message-guardrail-self-test.out

  rm -rf "$tmp"/*
  mkdir -p "$tmp/internal/store/message"
  cat > "$tmp/internal/store/message/scope_specialties.go" <<'GOOD'
package message

const descriptorName = "scope_search_workspace_messages_for_backfill"
const validScopeQuery = `SELECT * FROM scope_messages WHERE scope_id = $1`
GOOD
  AGENTTEAM_MESSAGE_GUARDRAIL_ROOT="$tmp" "$0" --check-only >/tmp/agentteam-message-guardrail-positive-self-test.out 2>&1 || {
    printf 'positive fixture unexpectedly failed\n' >&2
    cat /tmp/agentteam-message-guardrail-positive-self-test.out >&2 || true
    return 1
  }
  rm -f /tmp/agentteam-message-guardrail-positive-self-test.out

  printf 'message scope cutover guardrail self-test passed\n'
}

case "${1:-}" in
  --self-test)
    run_self_test
    exit 0
    ;;
  --inventory)
    print_legacy_inventory
    exit 0
    ;;
  --check-only|"")
    run_checks
    ;;
  *)
    printf 'Usage: %s [--self-test|--check-only|--inventory]\n' "$0" >&2
    exit 2
    ;;
esac

if (( failures > 0 )); then
  printf '\nmessage scope cutover guardrail failed with %d issue(s).\n' "$failures" >&2
  exit 1
fi

printf 'message scope cutover guardrail passed.\n'
