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

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

require_contains() {
  local file="$1"
  local pattern="$2"
  local message="$3"
  if [[ ! -f "$file" ]]; then
    fail "$message (missing $file)"
    return
  fi
  if ! rg -q -P -- "$pattern" "$file"; then
    fail "$message"
  fi
}

require_not_contains() {
  local file="$1"
  local pattern="$2"
  local message="$3"
  if [[ ! -f "$file" ]]; then
    fail "$message (missing $file)"
    return
  fi
  if rg -q -P -- "$pattern" "$file"; then
    fail "$message"
  fi
}

require_match_count_at_least() {
  local file="$1"
  local pattern="$2"
  local minimum="$3"
  local message="$4"
  local matches count
  if [[ ! -f "$file" ]]; then
    fail "$message (missing $file)"
    return
  fi
  matches="$(rg -n -P -- "$pattern" "$file" 2>/dev/null || true)"
  if [[ -z "$matches" ]]; then
    count=0
  else
    count="$(printf '%s\n' "$matches" | wc -l | tr -d ' ')"
  fi
  if (( count < minimum )); then
    fail "$message (found $count, want at least $minimum)"
  fi
}

check_pattern_matches_examples() {
  local pattern="$1"
  local description="$2"
  shift 2

  local example
  for example in "$@"; do
    if ! printf '%s\n' "$example" | rg -q -P -- "$pattern"; then
      fail "$description regex must match common spelling: $example"
    fi
  done
}

check_web_trigger_metadata_fallback_absent() {
  local matches pattern
  pattern="readMessageTriggerMetadata|metadata\\s*\\??\\s*(?:\\.\\s*trigger|\\[\\s*[\"'\\x60]trigger[\"'\\x60]\\s*\\])|\\[\\s*[\"'\\x60]trigger[\"'\\x60]\\s*\\]"
  check_pattern_matches_examples "$pattern" "web trigger metadata fallback denylist" \
    "readMessageTriggerMetadata(message)" \
    "metadata.trigger" \
    "metadata?.trigger" \
    "metadata['trigger']" \
    'metadata["trigger"]' \
    "metadata?.['trigger']" \
    'metadata?.["trigger"]' \
    "record['trigger']" \
    'record["trigger"]'
  matches="$(rg -n -P \
    --glob '*.ts' \
    --glob '*.tsx' \
    --glob '!**/*.test.ts' \
    --glob '!**/*.test.tsx' \
    --glob '!**/*.spec.ts' \
    --glob '!**/*.spec.tsx' \
    -- "$pattern" web/src 2>/dev/null || true)"
  if [[ -n "$matches" ]]; then
    fail "web production must not use metadata.trigger as trigger source/presentation fallback"
    printf '%s\n' "$matches" >&2
  fi
}

check_trigger_metadata_compat_boundary() {
  local matches
  matches="$(rg -n -P \
    --glob '*.go' \
    --glob '!**/*_test.go' \
    -- 'triggerMetadata\(|metadata\.trigger|\.Metadata\["trigger"\]|\["trigger"\]' internal cmd desktop scripts 2>/dev/null || true)"
  if [[ -n "$matches" ]]; then
    fail "production trigger source reads/writes must use typed ContributionSource/clientMessageID; metadata.trigger must not be a production source fallback"
    printf '%s\n' "$matches" >&2
  fi
}

require_contains \
  internal/store/message/contribution_source.go \
  'type ContributionSource struct' \
  "visible contributions must expose a typed source/run explanation"

require_contains \
  internal/store/message/contribution_source.go \
  'func DefaultContributionSourceForAppMessage\(' \
  "ordinary supported authors must have canonical default contribution sources"

require_not_contains \
  internal/store/message/contribution_source.go \
  'ContributionSourceKindAppCallback|app_callback' \
  "removed generic App callbacks must not remain a contribution-source kind or author fallback"

require_not_contains \
  internal/store/taskboard/source_provenance.go \
  'ContributionSourceKindAppCallback|app_callback' \
  "task projections must not invent the removed generic App callback source"

require_not_contains \
  internal/store/taskboard/task_timeline_read_service.go \
  'ContributionSourceKindAppCallback|app_callback' \
  "task timeline projections must not invent the removed generic App callback source"

require_contains \
  internal/store/message/source_resolver.go \
  'func AttachContributionSourcesForAppMessages\(' \
  "message read paths must attach canonical contribution sources through the central resolver"

require_contains \
  internal/store/message/source_resolver.go \
  'func AttachContributionSourcesForHistoryMessages\(' \
  "agent history/search read paths must attach canonical contribution sources through the central resolver"

require_contains \
  internal/store/message/contribution_source.go \
  'func \(source ContributionSource\) ValidateForWrite\(' \
  "write-time ContributionSource values must use an explicit allowlist"

require_contains \
  internal/store/message/contribution_source.go \
  'func \(source ContributionSource\) NormalizeForWrite\(' \
  "message write boundaries must normalize and validate ContributionSource values"

require_contains \
  internal/store/message/contribution_source_test.go \
  'TestContributionSourceValidateForWrite' \
  "ContributionSource write validation must have focused coverage"

require_contains \
  internal/store/message/api_types.go \
  'Source\s+ContributionSource' \
  "store AppMessage read model must carry ContributionSource"

require_contains \
  internal/api/dto/agent_messages.go \
  'type ContributionSource struct' \
  "API DTO must expose ContributionSource"

require_contains \
  internal/api/dto/agent_messages.go \
  'Source\s+\*ContributionSource\s+`json:"source,omitempty"`' \
  "API AppMessage DTO must include the canonical source field"

require_contains \
  internal/api/dto/converter/message.go \
  'Source:\s+ToContributionSource\(message\.Source\)' \
  "daemon receive-message conversion must expose canonical source"

require_match_count_at_least \
  internal/api/dto/converter/message.go \
  'Source:\s+ToContributionSource\(message\.Source\)' \
  3 \
  "daemon receive, agent history/search, and browser message DTO conversions must all expose canonical source"

require_contains \
  internal/api/dto/converter/message.go \
  'func ToOldDaemonReceiveMessage\(' \
  "old-daemon source normalization must live in the DTO converter owner"

require_contains \
  internal/api/daemon/protocol.go \
  'converter\.ToOldDaemonReceiveMessage\(' \
  "daemon start commands must use the DTO-owned old-daemon source projection"

require_contains \
  internal/api/daemon/typed_sender.go \
  'converter\.ToOldDaemonReceiveMessage\(' \
  "daemon deliver commands must use the DTO-owned old-daemon source projection"

if [[ -e internal/api/sourceprojection ]]; then
  fail "retired sourceprojection facade package must stay removed"
fi

require_contains \
  web/src/types/conversation.ts \
  'export type MessageContributionSource' \
  "web message type must include the canonical source shape"

require_contains \
  web/src/types/conversation.ts \
  'source\?: MessageContributionSource' \
  "web Message must expose the canonical source field"

require_contains \
  web/src/agentteam/api/messages.ts \
  'messageContributionSourceSchema' \
  "web API normalization must validate the canonical source shape"

require_contains \
  web/src/agentteam/pages/workspace-shell/socketPayloadGuards.ts \
  'record\.source' \
  "web realtime guards must validate optional message source shape"

require_contains \
  web/src/agentteam/pages/workspace-shell/socketPayloadGuards.test.ts \
  'rejects scalar contribution source' \
  "web realtime source guard must have regression coverage"

require_contains \
  web/src/agentteam/workspace/scheduled-messages/messageSourcePresentation.ts \
  'readMessageSourcePresentation' \
  "source UI presentation must read canonical message.source only"

require_contains \
  web/src/agentteam/workspace/conversation/components/MessageRow.tsx \
  'readMessageSourcePresentation\(message\)' \
  "message row source badges must read canonical source, not metadata.trigger fallback"

require_contains \
  web/src/agentteam/workspace/scheduled-messages/messageSourcePresentation.test.ts \
  'ignores old metadata-only schedule hints' \
  "web source presentation must reject metadata.trigger fallback"


require_contains \
  internal/store/app/app_messages.go \
  'AttachContributionSourcesForAppMessages' \
  "human-facing message reads must attach contribution sources"

require_contains \
  internal/store/message/search.go \
  'AttachContributionSourcesForAppMessages' \
  "message search read model must attach contribution sources"

require_contains \
  internal/store/message/scope_api.go \
  'AttachContributionSourcesForAppMessages' \
  "scope API message read model must attach contribution sources"

require_contains \
  internal/store/message/scope_api.go \
  'AttachContributionSourcesForHistoryMessages' \
  "scope API agent history/search/read-check model must attach contribution sources"

require_contains \
  internal/schedule/service.go \
  'Source\s+message\.ContributionSource' \
  "schedule effect request must carry typed ContributionSource"

require_contains \
  internal/schedule/service.go \
  'request\.Source\.ValidateForWrite\(\)' \
  "schedule effect boundary must validate ContributionSource before message creation"

require_contains \
  internal/api/im/fanout/schedule_effect_writer.go \
  'schedule\.ValidateEffectRequest\(request\)' \
  "schedule message writer boundary must validate ContributionSource before human message sends"

require_contains \
  internal/store/app/message_append.go \
  'options\.Source\.NormalizeForWrite\(\)' \
  "canonical app message write path must validate ContributionSource"

require_contains \
  internal/store/app/agent_messages.go \
  'params\.Source\.NormalizeForWrite\(\)' \
  "agent-authored message write path must validate ContributionSource"

require_contains \
  desktop/internal/types/session.go \
  'Source\s+ContributionSource' \
  "desktop inbox messages must preserve ContributionSource"

require_contains \
  desktop/internal/wire/types.go \
  'Source\s+types\.ContributionSource' \
  "desktop AgentTeam delivery protocol must preserve ContributionSource"

require_contains \
  internal/schedule/service_test.go \
  'ContributionSourceKindScheduleRun' \
  "scheduled delivery must have source coverage"

require_contains \
  internal/api/im/fanout/schedule_effect_writer_test.go \
  'humanWriter\.options\.Source' \
  "schedule writer source propagation must have coverage"

if rg -q -n -- 'metadata\.trigger|trigger metadata|trigger provenance.*metadata' specs/trigger/trigger.md desktop/prompts/channel/agentteam/reference.md specs/message/scope-model.md 2>/dev/null; then
  fail "greenfield trigger/scope/prompt specs must not describe trigger provenance through metadata.trigger or trigger metadata"
  rg -n -- 'metadata\.trigger|trigger metadata|trigger provenance.*metadata' specs/trigger/trigger.md desktop/prompts/channel/agentteam/reference.md specs/message/scope-model.md >&2 || true
fi

if rg -q -n -- 'ContributionSourceKindWorkflowCommand|ContributionSourceKindBot' internal cmd desktop web 2>/dev/null; then
  fail "ContributionSource must not register the retired workflow_command or a future bot kind; Workflow Run uses the native workflow_run source kind"
  rg -n -- 'ContributionSourceKindWorkflowCommand|ContributionSourceKindBot' internal cmd desktop web >&2 || true
fi

check_trigger_metadata_compat_boundary
check_web_trigger_metadata_fallback_absent

if (( failures > 0 )); then
  exit 1
fi

printf 'message contribution source guardrails OK\n'
