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

# Author serviceKind contract guardrail.
#
# The backend decides which AuthorProfile.serviceKind values may reach the wire
# via supportedAuthorServiceKind() in internal/store/message/api_types.go (its
# case list is the authoritative allow-list). Every frontend surface that
# re-declares that allow-list must stay in lockstep — when they drift, a backend
# kind the frontend rejects makes every API page containing one such message
# fail to parse ("Invalid messages response") and realtime payloads get dropped
# (see the workflow/schedule drift fixed on 2026-08-16). Neither side's unit
# tests catch drift because each side only tests its own list, so this
# guardrail extracts both sets and compares them.
#
# Covered frontend surfaces:
#   - web/src/agentteam/api/messages.ts            (zod refine allow-list)
#   - web/src/agentteam/api/conversations.ts       (zod enum)
#   - web/src/agentteam/api/tasks/collaboration.ts (zod enum)
#   - web/src/agentteam/api/tasks/related.ts       (zod enum)
#   - web/src/agentteam/pages/workspace-shell/socketPayloadGuards.ts (realtime guard)
#   - web/src/agentteam/workspace/shared/messageAuthorProfile.ts     (display normalizer)
#   - web/src/types/conversation.ts           (type union)
#   - shared/src/types.ts                     (type union)
#
# Extraction is anchored on each surface's own syntax and tolerates equivalent
# formatting (wrapped arrays/enums/unions). author-service-kind-contract-test.sh
# locks that in with per-surface mutations and formatting rewrites.

ROOT="${AGENTTEAM_MESSAGE_SERVICE_KIND_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))
}

collect_quoted() {
  # Emit every double-quoted literal on stdin, one per line.
  awk '
    {
      line = $0
      while (match(line, /"[^"]*"/)) {
        print substr(line, RSTART + 1, RLENGTH - 2)
        line = substr(line, RSTART + RLENGTH)
      }
    }
  '
}

# Backend wire allow-list: the case tokens of supportedAuthorServiceKind(), with
# AuthorServiceKind* identifiers resolved through their constant declarations.
# Emits UNRESOLVED:<token> for tokens it cannot map so the caller fails loudly.
backend_service_kinds() {
  awk '
    FNR == NR {
      if (match($0, /AuthorServiceKind[A-Za-z0-9_]+[[:space:]]*=[[:space:]]*"[^"]+"/)) {
        decl = substr($0, RSTART, RLENGTH)
        eq = index(decl, "=")
        name = substr(decl, 1, eq - 1)
        gsub(/[[:space:]]/, "", name)
        val = substr(decl, eq + 1)
        gsub(/[[:space:]]|"/, "", val)
        consts[name] = val
      }
      next
    }
    function emit(line,   n, t, i, toks) {
      n = split(line, toks, /[[:space:]]*,[[:space:]]*/)
      for (i = 1; i <= n; i++) {
        t = toks[i]
        gsub(/^[[:space:]]+|[[:space:]]+$/, "", t)
        if (t == "") continue
        if (t ~ /^".*"$/) { gsub(/^"|"$/, "", t); print t }
        else if (t in consts) print consts[t]
        else printf "UNRESOLVED:%s\n", t
      }
    }
    /^func supportedAuthorServiceKind\(/ { in_func = 1 }
    in_func && /^}/ { in_func = 0 }
    in_func && in_case && /:[[:space:]]*$/ { in_case = 0; sub(/:[[:space:]]*$/, "", $0); emit($0); next }
    in_func && in_case { emit($0); next }
    in_func && /case[[:space:]]/ {
      in_case = 1
      line = $0
      sub(/^[[:space:]]*case[[:space:]]+/, "", line)
      if (line ~ /:[[:space:]]*$/) { sub(/:[[:space:]]*$/, "", line); in_case = 0 }
      emit(line)
    }
  ' internal/store/message/api_types.go internal/store/message/api_types.go |
    { printf '\n'; cat; } | sort -u
}

# web/src/agentteam/api/messages.ts: array feeding .includes() inside
# supportedAuthorServiceKindSchema, inline or wrapped.
web_messages_service_kinds() {
  awk '
    /const[[:space:]]+supportedAuthorServiceKindSchema/ { in_schema = 1 }
    in_schema {
      line = $0
      while (match(line, /"[^"]*"/)) {
        print substr(line, RSTART + 1, RLENGTH - 2)
        line = substr(line, RSTART + RLENGTH)
      }
      if ($0 ~ /\.includes\(/) in_schema = 0
    }
  ' web/src/agentteam/api/messages.ts | sort -u
}

# `serviceKind: z.enum([...])` validator, single-line or wrapped.
web_enum_service_kinds() {
  local file="$1"
  awk '
    !in_enum && /serviceKind:/ && ($0 ~ /z\.enum\(\[/ || $0 ~ /serviceKind:[[:space:]]*z[[:space:]]*$/) {
      in_enum = 1
    }
    in_enum {
      line = $0
      while (match(line, /"[^"]*"/)) {
        print substr(line, RSTART + 1, RLENGTH - 2)
        line = substr(line, RSTART + RLENGTH)
      }
      if ($0 ~ /\]\)/) in_enum = 0
    }
  ' "$file" | sort -u
}

# Realtime guard: quoted literals inside isSupportedAuthorServiceKind().
socket_guard_service_kinds() {
  awk '
    /function isSupportedAuthorServiceKind/ { in_fn = 1 }
    in_fn {
      line = $0
      while (match(line, /"[^"]*"/)) {
        print substr(line, RSTART + 1, RLENGTH - 2)
        line = substr(line, RSTART + RLENGTH)
      }
      if ($0 ~ /^[[:space:]]*\)[[:space:]]*$/) exit
    }
  ' web/src/agentteam/pages/workspace-shell/socketPayloadGuards.ts | sort -u
}

# Display normalizer: quoted literals inside normalizeSupportedServiceKind().
# This surface normalizes unknowns to "" and never lists "" itself.
author_profile_normalizer_kinds() {
  awk '
    /function normalizeSupportedServiceKind/ { in_fn = 1 }
    in_fn {
      line = $0
      while (match(line, /"[^"]*"/)) {
        print substr(line, RSTART + 1, RLENGTH - 2)
        line = substr(line, RSTART + RLENGTH)
      }
      if ($0 ~ /\?[[:space:]]*normalized/) exit
    }
  ' web/src/agentteam/workspace/shared/messageAuthorProfile.ts | sort -u
}

# `serviceKind?: "" | "app" | ...` type union, single-line or wrapped. Wrapped
# unions either lead each line with `|` (prettier) or end each line with `|`;
# the style is detected from the first continuation line.
union_service_kinds() {
  local file="$1"
  awk '
    function collect(line) {
      while (match(line, /"[^"]*"/)) {
        print substr(line, RSTART + 1, RLENGTH - 2)
        line = substr(line, RSTART + RLENGTH)
      }
    }
    !in_union && /serviceKind\?:/ {
      in_union = 1
      style = 0
      body = $0
      sub(/^[^:]*:[[:space:]]*/, "", body)
      collect(body)
      if (body ~ /\|[[:space:]]*$/) { style = 2; next }
      if (body == "") next
      in_union = 0
      next
    }
    in_union {
      if (style == 0) {
        style = ($0 ~ /^[[:space:]]*\|/) ? 1 : 2
      }
      if (style == 1) {
        if ($0 !~ /^[[:space:]]*\|/) { in_union = 0; next }
        collect($0)
      } else {
        collect($0)
        if ($0 !~ /\|[[:space:]]*$/) { in_union = 0 }
      }
    }
  ' "$file" | sort -u
}

compare_sets() {
  local label="$1"
  local expected="$2"
  local actual="$3"
  if [[ "$expected" != "$actual" ]]; then
    fail "$label mismatch (backend vs frontend)"
    printf 'backend : %s\nfrontend: %s\n' \
      "$(printf '%s' "$expected" | tr '\n' ' ')" \
      "$(printf '%s' "$actual" | tr '\n' ' ')" >&2
  fi
}

backend_all="$(backend_service_kinds)"
if printf '%s\n' "$backend_all" | grep -q '^UNRESOLVED:'; then
  fail "unsupported tokens in supportedAuthorServiceKind case list: $(printf '%s' "$backend_all" | grep '^UNRESOLVED:' | tr '\n' ' ')"
  backend_all="$(printf '%s\n' "$backend_all" | grep -v '^UNRESOLVED:')"
fi
backend_nonempty="$(printf '%s\n' "$backend_all" | grep -v '^$' || true)"
if [[ -z "$backend_nonempty" ]]; then
  fail "unable to extract the supportedAuthorServiceKind case list from internal/store/message/api_types.go"
fi

for validator_file in \
  web/src/agentteam/api/messages.ts \
  web/src/agentteam/api/conversations.ts \
  web/src/agentteam/api/tasks/collaboration.ts \
  web/src/agentteam/api/tasks/related.ts; do
  if [[ ! -f "$validator_file" ]]; then
    fail "missing frontend serviceKind validator $validator_file"
    continue
  fi
done

if [[ -f web/src/agentteam/api/messages.ts ]]; then
  compare_sets \
    "web/src/agentteam/api/messages.ts runtime validator" \
    "$backend_all" \
    "$(web_messages_service_kinds)"
fi

for validator_file in \
  web/src/agentteam/api/conversations.ts \
  web/src/agentteam/api/tasks/collaboration.ts \
  web/src/agentteam/api/tasks/related.ts; do
  compare_sets \
    "$validator_file runtime validator" \
    "$backend_all" \
    "$(web_enum_service_kinds "$validator_file")"
done

compare_sets \
  "web/src/agentteam/pages/workspace-shell/socketPayloadGuards.ts realtime guard" \
  "$backend_all" \
  "$(socket_guard_service_kinds)"

compare_sets \
  "web/src/agentteam/workspace/shared/messageAuthorProfile.ts normalizer" \
  "$backend_nonempty" \
  "$(author_profile_normalizer_kinds)"

compare_sets \
  "web/src/types/conversation.ts type union" \
  "$backend_all" \
  "$(union_service_kinds web/src/types/conversation.ts)"

compare_sets \
  "shared/src/types.ts type union" \
  "$backend_all" \
  "$(union_service_kinds shared/src/types.ts)"

if (( failures > 0 )); then
  printf 'Author serviceKind contract guardrail failed. Add the new kind to every frontend allow-list in the same PR that extends supportedAuthorServiceKind: web/src/agentteam/api/messages.ts, web/src/agentteam/api/conversations.ts, web/src/agentteam/api/tasks/collaboration.ts, web/src/agentteam/api/tasks/related.ts, web/src/agentteam/pages/workspace-shell/socketPayloadGuards.ts, web/src/agentteam/workspace/shared/messageAuthorProfile.ts, web/src/types/conversation.ts, shared/src/types.ts.\n' >&2
  exit 1
fi

printf 'Author serviceKind contract guardrail passed.\n'
