#!/usr/bin/env bash
# ops-marketing-provision — self-provision marketing channels for a project
#
# Subcommands:
#   provision-ga4         --project <key> [--domain <d>] [--account-id <acc>]
#                         [--display-name <name>] [--industry SHOPPING|TECHNOLOGY]
#   provision-gsc         --project <key> [--site sc-domain:<d>|https://<d>/]
#   provision-instagram   --project <key> [--force]
#   provision-google-ads  --project <key> [--skip-if-pending]
#   provision-all         --project <key> | --all-projects
#   status                --project <key> [--json]
#   --help
#
# Idempotent: GET-first, reuse existing matches before creating.
# Set OPS_MARKETING_DRY_RUN=1 to print planned API calls without executing.
# Set OPS_MARKETING_BACKGROUND=1 to suppress interactive prompts.
#
# No real account IDs, emails, or domains are hardcoded in this file.
# All config is read from preferences.json and environment variables.

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
# shellcheck disable=SC1091
OPS_PLUGIN_ROOT_FALLBACK="${PLUGIN_ROOT}" . "${PLUGIN_ROOT}/lib/registry-path.sh"
# shellcheck disable=SC1091
. "${PLUGIN_ROOT}/scripts/lib/ga4-resolve.sh"

PREFS="${OPS_DATA_DIR}/preferences.json"
DRY_RUN="${OPS_MARKETING_DRY_RUN:-0}"
# OPS_MARKETING_BACKGROUND=1 suppresses interactive prompts (used by callers / tests)
export OPS_MARKETING_BACKGROUND="${OPS_MARKETING_BACKGROUND:-0}"
DOPPLER_PROJECT="${OPS_MARKETING_DOPPLER_PROJECT:-claude-ops}"
DOPPLER_CONFIG="${OPS_MARKETING_DOPPLER_CONFIG:-prd}"

# ---------------------------------------------------------------------------
# Logging helpers
# ---------------------------------------------------------------------------
log()  { printf '[provision] %s\n' "$1" >&2; }
info() { printf '%s\n' "$1"; }
die()  { printf '[provision] ERROR: %s\n' "$1" >&2; exit 1; }

dry_notice() {
  if [ "$DRY_RUN" = "1" ]; then
    printf '[DRY-RUN] would execute: %s\n' "$1" >&2
    return 0
  fi
  return 1
}

# ---------------------------------------------------------------------------
# Prereq checks
# ---------------------------------------------------------------------------
need_cmd() {
  command -v "$1" >/dev/null 2>&1 || die "Required command not found: $1 — please install it first"
}

# ---------------------------------------------------------------------------
# curl helper — checks HTTP status, aborts on non-2xx
# Usage: http_call <METHOD> <URL> [--data <body>] [--header "K: V"] ...
# Returns response body. Exits non-zero on error.
# ---------------------------------------------------------------------------
http_call() {
  local method="$1"; shift
  local url="$1"; shift
  local extra_args=()
  while [ $# -gt 0 ]; do
    extra_args+=("$1")
    shift
  done

  local tmpfile
  tmpfile="$(mktemp)"
  # shellcheck disable=SC2064
  trap "rm -f '$tmpfile'" RETURN

  local response http_code body
  response=$(curl -sS -w "\n%{http_code}" -X "$method" "$url" "${extra_args[@]}" 2>&1)
  http_code="${response##*$'\n'}"
  body="${response%$'\n'*}"

  if [[ "$http_code" -lt 200 || "$http_code" -ge 300 ]]; then
    log "HTTP $http_code from $method $url"
    log "Response: $body"
    die "API call failed with HTTP $http_code"
  fi

  printf '%s' "$body"
}

# Same as http_call but returns 1 on non-2xx without exiting; does not print
# response body to stdout on failure (so "$(... || echo '{}')" fallbacks work).
http_call_allow_fail() {
  local method="$1"; shift
  local url="$1"; shift
  local extra_args=()
  while [ $# -gt 0 ]; do
    extra_args+=("$1")
    shift
  done

  local tmpfile
  tmpfile="$(mktemp)"
  # shellcheck disable=SC2064
  trap "rm -f '$tmpfile'" RETURN

  local response http_code body
  response=$(curl -sS -w "\n%{http_code}" -X "$method" "$url" "${extra_args[@]}" 2>&1)
  http_code="${response##*$'\n'}"
  body="${response%$'\n'*}"

  if [[ "$http_code" -lt 200 || "$http_code" -ge 300 ]]; then
    log "HTTP $http_code from $method $url"
    log "Response: $body"
    return 1
  fi

  printf '%s' "$body"
}

# ---------------------------------------------------------------------------
# ADC token — get current access token with required scopes
# ---------------------------------------------------------------------------
ga4_get_token() {
  local token
  token=$(gcloud auth application-default print-access-token 2>/dev/null || true)
  if [ -z "$token" ]; then
    log "No Application Default Credentials found."
    log "Run the OAuth bootstrap to authenticate:"
    log ""
    log "  gcloud auth application-default login \\"
    log "    --scopes=openid,email,profile,\\"
    log "https://www.googleapis.com/auth/cloud-platform,\\"
    log "https://www.googleapis.com/auth/analytics.edit,\\"
    log "https://www.googleapis.com/auth/analytics.manage.users,\\"
    log "https://www.googleapis.com/auth/webmasters"
    log ""
    log "Store your OAuth client JSON at: ~/.gcp/ops-oauth-client.json"
    log "Then re-run this command."
    die "Missing ADC credentials"
  fi
  printf '%s' "$token"
}

gsc_get_token() {
  ga4_get_token
}

# ---------------------------------------------------------------------------
# Preferences helpers
# ---------------------------------------------------------------------------
prefs_get_raw() {
  [ -f "$PREFS" ] || return 0
  jq -r "$1" "$PREFS" 2>/dev/null || true
}

prefs_get_project_field() {
  local proj="$1" channel="$2" field="$3"
  prefs_get_raw ".marketing.projects[\"$proj\"][\"$channel\"][\"$field\"] // empty"
}

prefs_get_domain() {
  local proj="$1"
  prefs_get_raw ".marketing.projects[\"$proj\"].domain // .projects[\"$proj\"].domain // empty"
}

# Write a value into preferences.json using jq in-place update
prefs_set_marketing() {
  local proj="$1" channel="$2" field="$3" value="$4"
  [ -f "$PREFS" ] || echo '{}' > "$PREFS"

  local tmp
  tmp="$(mktemp)"
  jq --arg p "$proj" --arg c "$channel" --arg f "$field" --arg v "$value" \
    '.marketing.projects[$p][$c][$f] = $v' "$PREFS" > "$tmp" && mv "$tmp" "$PREFS"
  log "prefs: marketing.projects.$proj.$channel.$field = $value"
}

# Apply multiple field updates atomically via a single jq pipeline.
# Args: <proj> <channel> <field1> <value1> [<field2> <value2> ...]
# Empty value strings are skipped (use prefs_unset_marketing to delete).
prefs_set_marketing_multi() {
  local proj="$1" channel="$2"; shift 2
  [ -f "$PREFS" ] || echo '{}' > "$PREFS"

  local tmp filter='.'
  local jq_args=(--arg p "$proj" --arg c "$channel")
  local i=0
  while [ $# -ge 2 ]; do
    local f="$1" v="$2"; shift 2
    [ -z "$v" ] && continue
    jq_args+=(--arg "f${i}" "$f" --arg "v${i}" "$v")
    filter+=" | .marketing.projects[\$p][\$c][\$f${i}] = \$v${i}"
    i=$((i+1))
  done
  tmp="$(mktemp)"
  jq "${jq_args[@]}" "$filter" "$PREFS" > "$tmp" && mv "$tmp" "$PREFS"
  log "prefs: marketing.projects.$proj.$channel updated ($i fields)"
}

prefs_set_marketing_status() {
  local proj="$1" channel="$2" status="$3"
  prefs_set_marketing "$proj" "$channel" "status" "$status"
}

# ---------------------------------------------------------------------------
# Doppler push helper
# ---------------------------------------------------------------------------
doppler_set() {
  local key="$1" value="$2"
  if dry_notice "doppler secrets upload {\"$key\": \"<secret>\"} --project $DOPPLER_PROJECT --config $DOPPLER_CONFIG"; then
    return 0
  fi
  need_cmd doppler
  if printf '%s' "{\"$key\": \"$value\"}" | \
    doppler secrets upload - --project "$DOPPLER_PROJECT" --config "$DOPPLER_CONFIG" >/dev/null 2>&1; then
    log "Doppler: set $key in $DOPPLER_PROJECT/$DOPPLER_CONFIG"
  else
    log "WARN: Doppler push failed for $key — set manually"
  fi
}

# ---------------------------------------------------------------------------
# GA4 provisioning
# ---------------------------------------------------------------------------
cmd_provision_ga4() {
  local project="" domain="" account_id="" display_name="" industry="TECHNOLOGY"

  while [ $# -gt 0 ]; do
    case "$1" in
      --project)       project="${2:-}";      shift ;;
      --domain)        domain="${2:-}";       shift ;;
      --account-id)    account_id="${2:-}";   shift ;;
      --display-name)  display_name="${2:-}"; shift ;;
      --industry)      industry="${2:-}";     shift ;;
      *) die "Unknown argument: $1" ;;
    esac
    shift
  done

  [ -n "$project" ] || die "--project is required"

  need_cmd gcloud
  need_cmd curl
  need_cmd jq

  # Resolve domain from prefs if not provided
  if [ -z "$domain" ]; then
    domain="$(prefs_get_domain "$project")"
  fi
  [ -n "$domain" ] || die "--domain is required (or set marketing.projects.$project.domain in preferences.json)"

  # Resolve display name
  if [ -z "$display_name" ]; then
    display_name="$(printf '%s' "$project" | awk '{ print toupper(substr($0, 1, 1)) substr($0, 2) }') Web"
  fi

  # Resolve GA4 account ID from prefs or env
  if [ -z "$account_id" ]; then
    account_id="${GA4_ACCOUNT_ID:-$(prefs_get_raw '.marketing.ga4_account_id // empty')}"
  fi
  [ -n "$account_id" ] || die "--account-id is required (or set GA4_ACCOUNT_ID env / marketing.ga4_account_id in prefs)"
  account_id="${account_id#accounts/}"

  local token
  if [ "$DRY_RUN" = "1" ]; then
    token="dry-run"
  else
    token="$(ga4_get_token)"
  fi

  local auth_header="Authorization: Bearer $token"
  local base_admin="https://analyticsadmin.googleapis.com"

  log "Provisioning GA4 for project=$project domain=$domain account=accounts/$account_id"

  # --- Step 1: Bind SA access (idempotent) ---
  local sa_email="${GA4_SERVICE_ACCOUNT_EMAIL:-}"
  if [ -n "$sa_email" ]; then
    log "Checking SA access binding for $sa_email..."
    if dry_notice "POST $base_admin/v1alpha/accounts/$account_id/accessBindings {user: $sa_email}"; then
      true
    else
      # Check existing
      local existing_bindings
      existing_bindings="$(http_call_allow_fail GET "$base_admin/v1alpha/accounts/$account_id/accessBindings" \
        -H "$auth_header" 2>/dev/null || echo '{}')"
      local already_bound
      already_bound="$(printf '%s' "$existing_bindings" | jq -r \
        --arg sa "$sa_email" '.accessBindings[]? | select(.user == $sa) | .name' 2>/dev/null || true)"

      if [ -z "$already_bound" ]; then
        http_call POST "$base_admin/v1alpha/accounts/$account_id/accessBindings" \
          -H "$auth_header" -H "Content-Type: application/json" \
          --data "{\"user\":\"$sa_email\",\"roles\":[\"predefinedRoles/admin\"]}" >/dev/null
        log "SA access binding created"
      else
        log "SA access binding already exists: $already_bound"
      fi
    fi
  fi

  # --- Step 2: Find or create GA4 property ---
  log "Checking for existing GA4 property '$display_name'..."
  local property_id=""
  if dry_notice "GET $base_admin/v1beta/properties?filter=parent:accounts/$account_id"; then
    property_id="000000000"
    log "[DRY-RUN] Using placeholder property_id=$property_id"
  else
    local props_resp
    props_resp="$(http_call GET "$base_admin/v1beta/properties?filter=parent:accounts%2F$account_id" \
      -H "$auth_header")"
    property_id="$(printf '%s' "$props_resp" | jq -r \
      --arg name "$display_name" \
      '.properties[]? | select(.displayName == $name) | .name | split("/")[1]' 2>/dev/null | head -1 || true)"

    if [ -z "$property_id" ]; then
      log "Creating GA4 property '$display_name'..."
      local create_resp
      create_resp="$(http_call POST "$base_admin/v1beta/properties" \
        -H "$auth_header" -H "Content-Type: application/json" \
        --data "{
          \"parent\": \"accounts/$account_id\",
          \"displayName\": \"$display_name\",
          \"industryCategory\": \"$industry\",
          \"timeZone\": \"Europe/Amsterdam\",
          \"currencyCode\": \"USD\"
        }")"
      property_id="$(printf '%s' "$create_resp" | jq -r '.name | split("/")[1]' 2>/dev/null)"
      log "Created GA4 property: $property_id"
    else
      log "Found existing GA4 property: $property_id"
    fi
  fi

  # --- Step 3: Find or create web data stream ---
  log "Checking for existing web data stream..."
  local stream_id="" measurement_id=""
  if dry_notice "GET $base_admin/v1beta/properties/$property_id/dataStreams"; then
    stream_id="0000000000"
    measurement_id="G-XXXXXXXXXX"
    log "[DRY-RUN] Using placeholder stream_id=$stream_id measurement_id=$measurement_id"
  else
    local streams_resp
    streams_resp="$(http_call GET "$base_admin/v1beta/properties/$property_id/dataStreams" \
      -H "$auth_header")"
    local domain_clean="${domain#https://}"; domain_clean="${domain_clean#http://}"; domain_clean="${domain_clean%/}"
    stream_id="$(printf '%s' "$streams_resp" | jq -r \
      --arg d "$domain_clean" \
      '.dataStreams[]? | select(.type == "WEB_DATA_STREAM") | select(.webStreamData.defaultUri | test($d; "i")) | .name | split("/")[3]' \
      2>/dev/null | head -1 || true)"

    if [ -z "$stream_id" ]; then
      log "Creating web data stream for https://$domain_clean..."
      local stream_resp
      stream_resp="$(http_call POST "$base_admin/v1beta/properties/$property_id/dataStreams" \
        -H "$auth_header" -H "Content-Type: application/json" \
        --data "{
          \"type\": \"WEB_DATA_STREAM\",
          \"displayName\": \"$display_name Web\",
          \"webStreamData\": {\"defaultUri\": \"https://$domain_clean\"}
        }")"
      stream_id="$(printf '%s' "$stream_resp" | jq -r '.name | split("/")[3]' 2>/dev/null)"
      measurement_id="$(printf '%s' "$stream_resp" | jq -r '.webStreamData.measurementId // empty' 2>/dev/null)"
      log "Created stream: $stream_id, measurement_id: $measurement_id"
    else
      measurement_id="$(printf '%s' "$streams_resp" | jq -r \
        --arg sid "$stream_id" \
        '.dataStreams[]? | select(.name | endswith($sid)) | .webStreamData.measurementId // empty' \
        2>/dev/null || true)"
      log "Found existing stream: $stream_id, measurement_id: $measurement_id"
    fi
  fi

  # --- Step 4: Find or create Measurement Protocol secret ---
  log "Checking for existing MP secret..."
  local api_secret=""
  if dry_notice "GET $base_admin/v1beta/properties/$property_id/dataStreams/$stream_id/measurementProtocolSecrets"; then
    api_secret="fake-mp-secret-for-dry-run"
    log "[DRY-RUN] Using placeholder api_secret"
  else
    local secrets_resp
    secrets_resp="$(http_call GET \
      "$base_admin/v1beta/properties/$property_id/dataStreams/$stream_id/measurementProtocolSecrets" \
      -H "$auth_header")"
    api_secret="$(printf '%s' "$secrets_resp" | jq -r \
      '.measurementProtocolSecrets[]? | select(.displayName == "server-events") | .secretValue // empty' \
      2>/dev/null | head -1 || true)"

    if [ -z "$api_secret" ]; then
      log "Creating MP secret 'server-events'..."
      local secret_resp
      secret_resp="$(http_call POST \
        "$base_admin/v1beta/properties/$property_id/dataStreams/$stream_id/measurementProtocolSecrets" \
        -H "$auth_header" -H "Content-Type: application/json" \
        --data '{"displayName":"server-events"}')"
      api_secret="$(printf '%s' "$secret_resp" | jq -r '.secretValue // empty' 2>/dev/null)"
      log "Created MP secret"
    else
      log "Found existing MP secret"
    fi
  fi

  # --- Step 5: Push secret to Doppler ---
  local doppler_key
  doppler_key="GA4_API_SECRET_$(printf '%s' "$project" | tr '[:lower:]-' '[:upper:]_')"
  doppler_set "$doppler_key" "$api_secret"

  # --- Step 6: Write to preferences.json ---
  if [ "$DRY_RUN" != "1" ]; then
    prefs_set_marketing "$project" "ga4" "property_id" "$property_id"
    prefs_set_marketing "$project" "ga4" "measurement_id" "$measurement_id"
    prefs_set_marketing "$project" "ga4" "stream_id" "$stream_id"
    prefs_set_marketing "$project" "ga4" "api_secret" "doppler:$DOPPLER_PROJECT/$DOPPLER_CONFIG/$doppler_key"
    prefs_set_marketing_status "$project" "ga4" "active"
  fi

  info ""
  info "GA4 provisioned for project: $project"
  info "  property_id:    $property_id"
  info "  measurement_id: $measurement_id"
  info "  stream_id:      $stream_id"
  info "  api_secret:     stored in Doppler as $doppler_key"
}

# ---------------------------------------------------------------------------
# GSC provisioning
# ---------------------------------------------------------------------------
cmd_provision_gsc() {
  local project="" site_url=""

  while [ $# -gt 0 ]; do
    case "$1" in
      --project) project="${2:-}"; shift ;;
      --site)    site_url="${2:-}"; shift ;;
      *) die "Unknown argument: $1" ;;
    esac
    shift
  done

  [ -n "$project" ] || die "--project is required"

  need_cmd gcloud
  need_cmd curl
  need_cmd jq

  # Resolve site URL from prefs if not provided
  if [ -z "$site_url" ]; then
    site_url="$(prefs_get_project_field "$project" "gsc" "site_url")"
  fi
  if [ -z "$site_url" ]; then
    local domain
    domain="$(prefs_get_domain "$project")"
    [ -n "$domain" ] && site_url="https://$domain/"
  fi
  [ -n "$site_url" ] || die "--site is required (e.g. sc-domain:example.com or https://example.com/)"

  local token
  if [ "$DRY_RUN" = "1" ]; then
    token="dry-run"
  else
    token="$(gsc_get_token)"
  fi
  local auth_header="Authorization: Bearer $token"
  local base_gsc="https://searchconsole.googleapis.com/webmasters/v3"

  log "Provisioning GSC for project=$project site=$site_url"

  # URL-encode the site URL
  local site_encoded
  site_encoded="$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=''))" "$site_url" 2>/dev/null \
    || printf '%s' "$site_url" | sed 's|:|%3A|g; s|/|%2F|g')"

  # --- Step 1: Check if site already added ---
  local already_added=0
  if dry_notice "GET $base_gsc/sites"; then
    log "[DRY-RUN] Skipping site list check"
  else
    local sites_resp
    sites_resp="$(http_call_allow_fail GET "$base_gsc/sites" -H "$auth_header" 2>/dev/null || echo '{}')"
    local found
    found="$(printf '%s' "$sites_resp" | jq -r \
      --arg s "$site_url" '.siteEntry[]? | select(.siteUrl == $s) | .siteUrl' 2>/dev/null || true)"
    [ -n "$found" ] && already_added=1
  fi

  if [ "$already_added" = "1" ]; then
    log "Site already added to Search Console: $site_url"
  else
    log "Adding site to Search Console: $site_url"
    if ! dry_notice "PUT $base_gsc/sites/$site_encoded"; then
      http_call_allow_fail PUT "$base_gsc/sites/$site_encoded" -H "$auth_header" >/dev/null 2>&1 \
        || log "WARN: PUT site returned non-2xx — site may need manual verification"
    fi
  fi

  # --- Step 2: Verification via DNS TXT ---
  log "Checking verification token..."
  local verify_token=""
  if ! dry_notice "GET $base_gsc/sites/$site_encoded/siteVerificationToken"; then
    local token_resp
    token_resp="$(http_call_allow_fail GET "$base_gsc/sites/$site_encoded" \
      -H "$auth_header" 2>/dev/null || echo '{}')"
    # The token is surfaced via webmasters API v1 (not v3); use that for verification info
    local verify_method
    verify_method="$(printf '%s' "$token_resp" | jq -r '.verificationState // "unverified"' 2>/dev/null || echo "unverified")"
    log "Verification state: $verify_method"

    # Try to get DNS TXT token via site verification API
    local svt_resp svt_type svt_identifier
    if [[ "$site_url" == sc-domain:* ]]; then
      svt_type="INET_DOMAIN"
      svt_identifier="${site_url#sc-domain:}"
    else
      svt_type="SITE"
      svt_identifier="$site_url"
    fi
    svt_resp="$(http_call_allow_fail POST \
      "https://www.googleapis.com/siteVerification/v1/token" \
      -H "$auth_header" -H "Content-Type: application/json" \
      --data "{\"site\":{\"type\":\"$svt_type\",\"identifier\":\"$svt_identifier\"},\"verificationMethod\":\"DNS_TXT\"}" \
      2>/dev/null || echo '{}')"
    verify_token="$(printf '%s' "$svt_resp" | jq -r '.token // empty' 2>/dev/null || true)"
  else
    verify_token="google-site-verification=DRY-RUN-PLACEHOLDER"
  fi

  # --- Step 3: Auto-add DNS TXT if Cloudflare available ---
  local cf_token="${CLOUDFLARE_API_TOKEN:-}"
  local cf_key="${CLOUDFLARE_API_KEY:-}"
  local cf_email="${CLOUDFLARE_EMAIL:-}"
  local dns_done=0

  if [ -n "$verify_token" ] && { [ -n "$cf_token" ] || { [ -n "$cf_key" ] && [ -n "$cf_email" ]; }; }; then
    local cf_auth_header=""
    if [ -n "$cf_token" ]; then
      cf_auth_header="Authorization: Bearer $cf_token"
    else
      cf_auth_header="X-Auth-Key: $cf_key"
    fi

    # Extract the root domain for zone lookup
    local site_domain="${site_url#sc-domain:}"
    site_domain="${site_domain#https://}"; site_domain="${site_domain#http://}"; site_domain="${site_domain%/}"
    # Get the root domain (last two parts)
    local root_domain
    root_domain="$(printf '%s' "$site_domain" | awk -F. '{print $(NF-1)"."$NF}')"

    log "Looking up Cloudflare zone for $root_domain..."
    if ! dry_notice "GET https://api.cloudflare.com/client/v4/zones?name=$root_domain"; then
      local zone_resp
      zone_resp="$(curl -sS -w "\n%{http_code}" \
        -H "$cf_auth_header" ${cf_email:+-H "X-Auth-Email: $cf_email"} \
        "https://api.cloudflare.com/client/v4/zones?name=$root_domain" 2>/dev/null || echo '{}'$'\n'"0")"
      local zone_code="${zone_resp##*$'\n'}"
      local zone_body="${zone_resp%$'\n'*}"

      if [ "$zone_code" = "200" ]; then
        local zone_id
        zone_id="$(printf '%s' "$zone_body" | jq -r '.result[0].id // empty' 2>/dev/null || true)"

        if [ -n "$zone_id" ]; then
          # Check for existing TXT record
          local existing_txt
          existing_txt="$(curl -sS \
            -H "$cf_auth_header" ${cf_email:+-H "X-Auth-Email: $cf_email"} \
            "https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records?type=TXT&name=$root_domain" \
            2>/dev/null | jq -r \
            --arg t "$verify_token" '.result[]? | select(.content == $t) | .id' 2>/dev/null || true)"

          if [ -z "$existing_txt" ]; then
            log "Adding DNS TXT verification record via Cloudflare..."
            if curl -sS -X POST \
              -H "$cf_auth_header" ${cf_email:+-H "X-Auth-Email: $cf_email"} \
              -H "Content-Type: application/json" \
              "https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records" \
              --data "{\"type\":\"TXT\",\"name\":\"$root_domain\",\"content\":\"$verify_token\",\"ttl\":120}" \
              >/dev/null 2>&1; then
              log "DNS TXT record added"
            else
              log "WARN: DNS TXT upsert failed — add manually"
            fi
          else
            log "DNS TXT record already exists"
          fi
          dns_done=1
        fi
      fi
    else
      dns_done=1
    fi
  fi

  if [ "$dns_done" = "0" ] && [ -n "$verify_token" ]; then
    info ""
    info "Add this DNS TXT record to verify site ownership:"
    info "  Type: TXT"
    info "  Name: @ (root of your domain)"
    info "  Value: $verify_token"
    info ""
    info "After adding, re-run: $0 provision-gsc --project $project --site $site_url"
  fi

  # --- Step 4: Write to preferences.json ---
  if [ "$DRY_RUN" != "1" ]; then
    prefs_set_marketing "$project" "gsc" "site_url" "$site_url"
    prefs_set_marketing_status "$project" "gsc" "active"
  fi

  info ""
  info "GSC provisioned for project: $project"
  info "  site_url: $site_url"
  [ -n "$verify_token" ] && info "  verify_token: $verify_token"
}

# ---------------------------------------------------------------------------
# Meta appsecret_proof — required when "Require App Secret" is on (default for
# all our system-user tokens). HMAC-SHA256(access_token, app_secret).
# Echoes the hex digest (empty on missing inputs).
# ---------------------------------------------------------------------------
meta_appsecret_proof() {
  local token="$1" secret="$2"
  { [ -z "$token" ] || [ -z "$secret" ]; } && return 0
  printf '%s' "$token" | openssl dgst -sha256 -hmac "$secret" -hex 2>/dev/null | awk '{print $NF}'
}

# ---------------------------------------------------------------------------
# provision-instagram — resolve IG Business Account ID from Meta token.
# Idempotent: skips API call if instagram.account_id already populated and
# smoke-tests it before declaring success. Requires meta.access_token in prefs.
# ---------------------------------------------------------------------------
cmd_provision_instagram() {
  local project="" force=0

  while [ $# -gt 0 ]; do
    case "$1" in
      --project) project="${2:-}"; shift ;;
      --force)   force=1 ;;
      *) die "Unknown argument to provision-instagram: $1" ;;
    esac
    shift
  done

  [ -n "$project" ] || die "--project is required"
  need_cmd jq
  need_cmd openssl
  need_cmd curl

  log "Provisioning Instagram for project: $project"

  local meta_token_ref meta_secret_ref page_id existing
  meta_token_ref="$(prefs_get_project_field "$project" "meta" "access_token")"
  meta_secret_ref="$(prefs_get_project_field "$project" "meta" "app_secret")"
  page_id="$(prefs_get_project_field "$project" "meta" "page_id")"
  existing="$(prefs_get_project_field "$project" "instagram" "account_id")"

  local meta_token meta_secret
  meta_token="$(resolve_cred "$meta_token_ref")"
  meta_secret="$(resolve_cred "$meta_secret_ref")"

  if [ -z "$meta_token" ]; then
    log "Meta access token not configured for $project — required before Instagram"
    log "Configure marketing.projects.$project.meta.access_token in preferences.json first"
    exit 2
  fi

  local proof
  proof="$(meta_appsecret_proof "$meta_token" "$meta_secret")"
  local proof_qs=""
  [ -n "$proof" ] && proof_qs="&appsecret_proof=$proof"

  # Idempotent short-circuit: if account_id already set and not --force, verify
  # it still works against the API and exit clean.
  if [ -n "$existing" ] && [ "$existing" != "null" ] && [ "$force" = "0" ]; then
    log "Already configured: instagram.account_id=$existing — running smoke test"
    if dry_notice "GET graph.facebook.com/v22.0/$existing?fields=username,followers_count"; then
      info "[Instagram] dry-run: existing id=$existing"
      return 0
    fi
    local probe
    probe=$(http_call_allow_fail GET \
      "https://graph.facebook.com/v22.0/$existing?fields=username,followers_count,media_count${proof_qs}" \
      -H "Authorization: Bearer $meta_token" 2>/dev/null || echo "{}")
    local username followers
    username=$(printf '%s' "$probe" | jq -r '.username // empty' 2>/dev/null)
    followers=$(printf '%s' "$probe" | jq -r '.followers_count // empty' 2>/dev/null)
    if [ -n "$username" ]; then
      info "[Instagram] ✓ verified @${username} (${followers:-0} followers) — id=$existing"
      return 0
    fi
    log "Existing id $existing failed verification — re-resolving"
  fi

  # Resolve via Page if page_id known, else /me/accounts.
  local ig_id=""
  if [ -n "$page_id" ] && [ "$page_id" != "null" ]; then
    if dry_notice "GET graph.facebook.com/v22.0/$page_id?fields=instagram_business_account,connected_instagram_account"; then
      info "[DRY-RUN] would resolve IG via page $page_id"
      return 0
    fi
    local page_resp
    page_resp=$(http_call_allow_fail GET \
      "https://graph.facebook.com/v22.0/$page_id?fields=instagram_business_account,connected_instagram_account,name${proof_qs}" \
      -H "Authorization: Bearer $meta_token" 2>/dev/null || echo "{}")
    ig_id=$(printf '%s' "$page_resp" | jq -r '.instagram_business_account.id // .connected_instagram_account.id // empty' 2>/dev/null)
  fi

  if [ -z "$ig_id" ]; then
    if dry_notice "GET graph.facebook.com/v22.0/me/accounts?fields=id,name,instagram_business_account"; then
      info "[DRY-RUN] would fall back to /me/accounts"
      return 0
    fi
    local me_resp
    me_resp=$(http_call_allow_fail GET \
      "https://graph.facebook.com/v22.0/me/accounts?fields=id,name,instagram_business_account${proof_qs}" \
      -H "Authorization: Bearer $meta_token" 2>/dev/null || echo "{}")
    ig_id=$(printf '%s' "$me_resp" | jq -r '.data[0].instagram_business_account.id // empty' 2>/dev/null)
  fi

  if [ -z "$ig_id" ]; then
    die "Could not resolve Instagram Business Account ID — ensure the Facebook Page is linked to an IG Business account in Meta Business Manager"
  fi

  # Smoke-test resolved ID
  local probe username followers
  probe=$(http_call GET \
    "https://graph.facebook.com/v22.0/$ig_id?fields=username,followers_count,media_count${proof_qs}" \
    -H "Authorization: Bearer $meta_token")
  username=$(printf '%s' "$probe" | jq -r '.username // empty')
  followers=$(printf '%s' "$probe" | jq -r '.followers_count // 0')

  # Atomic prefs write
  local now
  now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
  prefs_set_marketing_multi "$project" "instagram" \
    "account_id" "$ig_id" \
    "username"   "$username" \
    "resolved_at" "$now"

  info "[Instagram] ✓ resolved — @${username} (${followers} followers) — id=$ig_id"
}

# ---------------------------------------------------------------------------
# Helper: scan env / Doppler for an existing Google Ads credential.
# Args: <doppler_key> [env_var]
# Echoes value if found (env first, then doppler), empty otherwise.
# Always returns 0 — errexit-safe (callers check empty string, not exit code).
# ---------------------------------------------------------------------------
gads_scan_credential() {
  local doppler_key="$1"
  local env_var="${2:-$doppler_key}"
  local val="${!env_var:-}"
  if [ -n "$val" ]; then
    printf '%s' "$val"
    return 0
  fi
  if command -v doppler >/dev/null 2>&1; then
    # Suppress non-zero exit when key is missing (set -e safety).
    val="$(doppler secrets get "$doppler_key" --plain \
      --project "$DOPPLER_PROJECT" --config "$DOPPLER_CONFIG" 2>/dev/null || true)"
    printf '%s' "$val"
  fi
  return 0
}

# ---------------------------------------------------------------------------
# provision-google-ads — 4-step Google Ads setup flow.
#   Step A: developer token (manual approval pathway with pending-state)
#   Step B: OAuth2 client (Cloud Console — shared across projects)
#   Step C: refresh token via browser OAuth callback
#   Step D: customer ID resolution (auto-MCC detection)
# ---------------------------------------------------------------------------
cmd_provision_google_ads() {
  local project="" skip_if_pending=0

  while [ $# -gt 0 ]; do
    case "$1" in
      --project)         project="${2:-}"; shift ;;
      --skip-if-pending) skip_if_pending=1 ;;
      *) die "Unknown argument to provision-google-ads: $1" ;;
    esac
    shift
  done

  [ -n "$project" ] || die "--project is required"
  need_cmd jq
  need_cmd curl

  local pending_dir="${OPS_DATA_DIR}/state/marketing-provision"
  mkdir -p "$pending_dir"
  local pending_file="${pending_dir}/${project}-google-ads-pending.json"

  if [ "$skip_if_pending" = "1" ] && [ -f "$pending_file" ]; then
    log "Google Ads still pending for $project (dev token approval) — skipping per --skip-if-pending"
    return 0
  fi

  log "Provisioning Google Ads for project: $project"

  # ── Step A: Developer Token ───────────────────────────────────────────
  local dev_token
  dev_token=$(gads_scan_credential GOOGLE_ADS_DEVELOPER_TOKEN)

  if [ -z "$dev_token" ]; then
    if [ "$DRY_RUN" = "1" ]; then
      info "[DRY-RUN] dev token missing — would create pending state at $pending_file"
      printf '{"project":"%s","stage":"developer_token","instructions":"Apply at ads.google.com/aw/apicenter","dry_run":true,"created_at":"%s"}\n' \
        "$project" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$pending_file"
      info "[Google Ads] DRY-RUN — pending file written ($pending_file). Apply for dev token at https://ads.google.com/aw/apicenter."
      return 1
    fi
    log "GOOGLE_ADS_DEVELOPER_TOKEN not found in env or Doppler"
    log "Apply for a developer token: https://ads.google.com/aw/apicenter"
    log "Approval typically takes 24-48 hours. Re-run this command after approval."
    printf '{"project":"%s","stage":"developer_token","instructions":"Apply at https://ads.google.com/aw/apicenter — re-run provision-google-ads --project %s after approval","created_at":"%s"}\n' \
      "$project" "$project" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$pending_file"
    log "Pending state written: $pending_file"
    exit 1
  fi
  log "Developer token: present"

  # ── Step B: OAuth2 Client ─────────────────────────────────────────────
  local client_id client_secret
  client_id=$(gads_scan_credential GOOGLE_ADS_CLIENT_ID)
  client_secret=$(gads_scan_credential GOOGLE_ADS_CLIENT_SECRET)

  if [ -z "$client_id" ] || [ -z "$client_secret" ]; then
    if [ "$DRY_RUN" = "1" ]; then
      info "[DRY-RUN] OAuth client missing — would create pending state"
      printf '{"project":"%s","stage":"oauth_client","dry_run":true}\n' "$project" > "$pending_file"
      return 1
    fi
    log "OAuth2 client credentials not found"
    log "Create a Desktop OAuth client at: https://console.cloud.google.com/apis/credentials"
    log "  - Application type: Desktop app"
    log "  - Enabled API: Google Ads API"
    log "Store as Doppler secrets: GOOGLE_ADS_CLIENT_ID, GOOGLE_ADS_CLIENT_SECRET"
    printf '{"project":"%s","stage":"oauth_client","created_at":"%s"}\n' \
      "$project" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$pending_file"
    exit 1
  fi
  log "OAuth2 client: present"

  # Source OAuth helper lib
  # shellcheck disable=SC1091
  . "${PLUGIN_ROOT}/scripts/lib/google-ads-oauth.sh"

  # ── Step C: Refresh Token ─────────────────────────────────────────────
  local refresh_token
  refresh_token=$(gads_scan_credential "GOOGLE_ADS_${project^^}_REFRESH_TOKEN")
  [ -z "$refresh_token" ] && refresh_token=$(gads_scan_credential GOOGLE_ADS_REFRESH_TOKEN)

  if [ -z "$refresh_token" ]; then
    if [ "$DRY_RUN" = "1" ]; then
      info "[DRY-RUN] would launch localhost OAuth callback on :8080 (120s)"
      printf '{"project":"%s","stage":"refresh_token","dry_run":true}\n' "$project" > "$pending_file"
      return 1
    fi
    log "Starting OAuth browser flow on http://localhost:8080 (120s timeout)"
    local auth_url
    auth_url=$(gads_authorize_url "$client_id")
    log "Open: $auth_url"
    log "(opens automatically — Sam should authorize on the consent screen)"

    # Open + capture in parallel; capture writes the code to a temp file.
    local code_file
    code_file=$(mktemp)
    ( gads_localhost_capture 8080 120 > "$code_file" 2>/dev/null ) &
    local capture_pid=$!
    sleep 1
    # Best-effort open (silent if `open` missing or on SSH per Rule 7)
    if command -v open >/dev/null 2>&1 && [ -z "${SSH_CONNECTION:-}${SSH_CLIENT:-}${SSH_TTY:-}" ]; then
      open "$auth_url" >/dev/null 2>&1 || true
    fi
    wait "$capture_pid" || true
    local code
    code=$(cat "$code_file" 2>/dev/null)
    rm -f "$code_file"

    [ -n "$code" ] || die "OAuth flow timed out — no authorization code received"
    log "Authorization code received (${#code} chars)"

    local token_resp
    token_resp=$(gads_exchange_code "$code" "$client_id" "$client_secret")
    refresh_token=$(printf '%s' "$token_resp" | jq -r '.refresh_token // empty')
    [ -n "$refresh_token" ] || die "Token exchange failed: $(printf '%s' "$token_resp" | jq -r '.error_description // .error // "no refresh_token in response"')"

    log "Refresh token obtained — writing to Doppler"
    doppler_set "GOOGLE_ADS_${project^^}_REFRESH_TOKEN" "$refresh_token"
    prefs_set_marketing "$project" "google_ads" "refresh_token" \
      "doppler:${DOPPLER_PROJECT}/${DOPPLER_CONFIG}/GOOGLE_ADS_${project^^}_REFRESH_TOKEN"
  else
    log "Refresh token: present"
  fi

  # ── Step D: Customer ID ──────────────────────────────────────────────
  local existing_customer
  existing_customer="$(prefs_get_project_field "$project" "google_ads" "customer_id")"
  if [ -n "$existing_customer" ] && [ "$existing_customer" != "null" ]; then
    log "Customer ID already configured: $existing_customer"
    rm -f "$pending_file"
    info "[Google Ads] ✓ already configured — customer_id=$existing_customer"
    return 0
  fi

  if dry_notice "POST oauth2.googleapis.com/token (refresh); GET v24/customers:listAccessibleCustomers"; then
    info "[DRY-RUN] would resolve customer ID"
    return 0
  fi

  local access_token
  access_token=$(gads_refresh_access_token "$refresh_token" "$client_id" "$client_secret")
  [ -n "$access_token" ] || die "Access token refresh failed — check client_id/client_secret/refresh_token"

  local accessible
  accessible=$(gads_list_accessible_customers "$access_token" "$dev_token")
  local resource_names
  resource_names=$(printf '%s' "$accessible" | jq -r '.resourceNames[]? // empty' 2>/dev/null)
  [ -n "$resource_names" ] || die "No accessible customers — $(printf '%s' "$accessible" | jq -r '.error.message // "unknown error"')"

  # Single account: auto-select. Multiple: log all and pick first non-manager.
  local customer_count chosen_customer login_customer=""
  customer_count=$(printf '%s\n' "$resource_names" | wc -l | tr -d ' ')
  log "Accessible customers ($customer_count):"
  printf '%s\n' "$resource_names" | sed 's/^/  - /' >&2

  if [ "$customer_count" = "1" ]; then
    local rn_single cid_single info_resp_single is_manager_single
    rn_single=$(printf '%s\n' "$resource_names" | head -n1)
    cid_single="${rn_single##customers/}"
    info_resp_single=$(gads_get_customer_info "$cid_single" "$access_token" "$dev_token" 2>/dev/null || echo "{}")
    is_manager_single=$(printf '%s' "$info_resp_single" | jq -r '.results[0].customer.manager // false' 2>/dev/null)
    if [ "$is_manager_single" = "true" ]; then
      login_customer="$cid_single"
      log "  (sole accessible account is MCC manager: $cid_single)"
    fi
    chosen_customer="$cid_single"
  else
    # Walk accounts, find first non-manager. Manager accounts become login_customer_id.
    while IFS= read -r rn; do
      [ -z "$rn" ] && continue
      local cid info_resp is_manager
      cid="${rn##customers/}"
      info_resp=$(gads_get_customer_info "$cid" "$access_token" "$dev_token" 2>/dev/null || echo "{}")
      is_manager=$(printf '%s' "$info_resp" | jq -r '.results[0].customer.manager // false' 2>/dev/null)
      if [ "$is_manager" = "true" ] && [ -z "$login_customer" ]; then
        login_customer="$cid"
        log "  (MCC manager: $cid)"
      elif [ "$is_manager" = "false" ] && [ -z "$chosen_customer" ]; then
        chosen_customer="$cid"
      fi
    done <<< "$resource_names"
    [ -z "$chosen_customer" ] && chosen_customer=$(printf '%s\n' "$resource_names" | head -n1 | sed 's#^customers/##')
  fi

  log "Selected customer: $chosen_customer${login_customer:+ (login-customer-id: $login_customer)}"

  # Atomic prefs write
  prefs_set_marketing_multi "$project" "google_ads" \
    "customer_id"        "$chosen_customer" \
    "developer_token"    "doppler:${DOPPLER_PROJECT}/${DOPPLER_CONFIG}/GOOGLE_ADS_DEVELOPER_TOKEN" \
    "client_id"          "doppler:${DOPPLER_PROJECT}/${DOPPLER_CONFIG}/GOOGLE_ADS_CLIENT_ID" \
    "client_secret"      "doppler:${DOPPLER_PROJECT}/${DOPPLER_CONFIG}/GOOGLE_ADS_CLIENT_SECRET" \
    "login_customer_id"  "$login_customer" \
    "configured_at"      "$(date -u +%Y-%m-%dT%H:%M:%SZ)"

  rm -f "$pending_file"
  info "[Google Ads] ✓ configured — customer_id=$chosen_customer"
}

# ---------------------------------------------------------------------------
# provision-all
# ---------------------------------------------------------------------------
cmd_provision_all() {
  local project="" site_for_gsc="" all_projects=0 ga4_remaining=()

  while [ $# -gt 0 ]; do
    case "$1" in
      --project) project="${2:-}"; shift ;;
      --site) site_for_gsc="${2:-}"; shift ;;
      --all-projects) all_projects=1 ;;
      *) ga4_remaining+=("$1") ;;
    esac
    shift
  done

  if [ "$all_projects" = "1" ]; then
    [ -f "$PREFS" ] || die "preferences.json not found — cannot iterate projects"
    local project_list
    project_list=$(jq -r '.marketing.projects | keys[]' "$PREFS")
    [ -n "$project_list" ] || die "no projects in marketing.projects"
    local failed=0
    while IFS= read -r p; do
      info "════════════════════════════════════════════════════════════════"
      info "  $p"
      info "════════════════════════════════════════════════════════════════"
      cmd_provision_all --project "$p" || failed=$((failed+1))
      info ""
    done <<< "$project_list"
    [ "$failed" = "0" ] || log "$failed project(s) had failures — see logs above"
    return $failed
  fi

  [ -n "$project" ] || die "--project is required (or use --all-projects)"

  info "Provisioning all channels for project: $project"
  info ""

  local provision_failures=0
  # Each sub-call runs in a subshell so a downstream `die`/`exit` only kills
  # that subshell — the chain continues to the next channel. Failures are
  # logged but do not abort provision-all.
  info "--- GA4 ---"
  ( cmd_provision_ga4 --project "$project" "${ga4_remaining[@]+"${ga4_remaining[@]}"}" ) || {
    log "GA4 provisioning failed for $project (continuing with other channels)"
    provision_failures=$((provision_failures + 1))
  }
  info ""

  info "--- Google Search Console ---"
  if [ -n "$site_for_gsc" ]; then
    ( cmd_provision_gsc --project "$project" --site "$site_for_gsc" ) || {
      log "GSC provisioning failed for $project (continuing)"
      provision_failures=$((provision_failures + 1))
    }
  else
    ( cmd_provision_gsc --project "$project" ) || {
      log "GSC provisioning failed for $project (continuing)"
      provision_failures=$((provision_failures + 1))
    }
  fi
  info ""

  info "--- Instagram ---"
  # IG is best-effort: silent no-op if Meta token not configured.
  local meta_tok_ref
  meta_tok_ref="$(prefs_get_project_field "$project" "meta" "access_token")"
  if [ -n "$meta_tok_ref" ] && [ "$meta_tok_ref" != "null" ]; then
    ( cmd_provision_instagram --project "$project" ) || {
      log "Instagram provisioning failed for $project (continuing)"
      provision_failures=$((provision_failures + 1))
    }
  else
    log "Skipping Instagram — meta.access_token not configured"
  fi
  info ""

  info "--- Google Ads ---"
  # Skip if pending (dev token approval in progress).
  ( cmd_provision_google_ads --project "$project" --skip-if-pending ) || {
    log "Google Ads provisioning incomplete for $project (likely awaiting dev token approval)"
    provision_failures=$((provision_failures + 1))
  }
  info ""

  info "Done. Run: $0 status --project $project"
  return "$provision_failures"
}

# ---------------------------------------------------------------------------
# status
# ---------------------------------------------------------------------------
cmd_status() {
  local project="" json_mode=0

  while [ $# -gt 0 ]; do
    case "$1" in
      --project) project="${2:-}"; shift ;;
      --json)    json_mode=1 ;;
      *) die "Unknown argument: $1" ;;
    esac
    shift
  done

  [ -n "$project" ] || die "--project is required"

  if [ "$json_mode" = "1" ]; then
    marketing_channels_status "$project" --json
  else
    info "Marketing channel status for project: $project"
    info ""
    marketing_channels_status "$project"
  fi
}

# ---------------------------------------------------------------------------
# help
# ---------------------------------------------------------------------------
cmd_help() {
  cat <<'HELP'
ops-marketing-provision — self-provision marketing channels

Usage:
  ops-marketing-provision provision-ga4         --project <key> [options]
  ops-marketing-provision provision-gsc         --project <key> [--site <url>]
  ops-marketing-provision provision-instagram   --project <key> [--force]
  ops-marketing-provision provision-google-ads  --project <key> [--skip-if-pending]
  ops-marketing-provision provision-all         (--project <key> | --all-projects)
  ops-marketing-provision status                --project <key> [--json]
  ops-marketing-provision --help

provision-ga4 options:
  --project <key>          Project key (matches preferences.json marketing.projects.<key>)
  --domain <domain>        Domain for the web stream (e.g. acme.example)
  --account-id <acc>       GA4 account ID (or set GA4_ACCOUNT_ID env)
  --display-name <name>    GA4 property display name (default: "<project> Web")
  --industry <cat>         SHOPPING or TECHNOLOGY (default: TECHNOLOGY)

provision-gsc options:
  --project <key>          Project key
  --site <url>             Site URL: sc-domain:example.com or https://example.com/

provision-instagram options:
  --project <key>          Project key
  --force                  Re-resolve even if instagram.account_id is already set
  Requires marketing.projects.<key>.meta.access_token (token + optional app_secret
  for appsecret_proof signing). Idempotent — smoke-tests existing IDs before re-API.

provision-google-ads options:
  --project <key>          Project key
  --skip-if-pending        Exit 0 if dev-token approval is pending (for provision-all chains)

  Four-step flow (each step is no-op if cred already present):
    A. GOOGLE_ADS_DEVELOPER_TOKEN — scan env+Doppler. If missing, writes a pending
       state file at $OPS_DATA_DIR/state/marketing-provision/<key>-google-ads-pending.json
       and exits 1 with instructions to apply at https://ads.google.com/aw/apicenter
       (24-48h approval).
    B. GOOGLE_ADS_CLIENT_ID/CLIENT_SECRET — scan env+Doppler. If missing, prints
       Cloud Console URL to create a Desktop OAuth client.
    C. Refresh token — launches a localhost OAuth callback on :8080 (120s timeout),
       opens the consent URL, captures the auth code, exchanges for refresh_token,
       writes to Doppler GOOGLE_ADS_<KEY_UPPER>_REFRESH_TOKEN.
    D. Customer ID — calls v24/customers:listAccessibleCustomers, auto-detects MCC
       manager accounts (sets login_customer_id), writes customer_id to prefs.

Environment variables:
  OPS_MARKETING_DRY_RUN=1          Print planned actions, no network calls
  OPS_MARKETING_BACKGROUND=1       Suppress interactive prompts
  OPS_MARKETING_DOPPLER_PROJECT     Doppler project for secrets (default: claude-ops)
  OPS_MARKETING_DOPPLER_CONFIG      Doppler config for secrets (default: prd)
  GA4_ACCOUNT_ID                    GA4 account ID
  GA4_SERVICE_ACCOUNT_EMAIL         SA email to bind account-level admin access
  CLOUDFLARE_API_TOKEN              Cloudflare Bearer token for DNS auto-upsert
  CLOUDFLARE_API_KEY + CLOUDFLARE_EMAIL  Alternative Cloudflare auth

OAuth bootstrap (required before first run):
  gcloud auth application-default login \
    --scopes=openid,email,profile,\
https://www.googleapis.com/auth/cloud-platform,\
https://www.googleapis.com/auth/analytics.edit,\
https://www.googleapis.com/auth/analytics.manage.users,\
https://www.googleapis.com/auth/webmasters

  Store OAuth client JSON at: ~/.gcp/ops-oauth-client.json (never committed)

Worked example:
  ops-marketing-provision provision-ga4 \
    --project myapp \
    --domain myapp.example \
    --account-id <YOUR_GA4_ACCOUNT_ID> \
    --industry TECHNOLOGY

  ops-marketing-provision status --project myapp --json
HELP
}

# ---------------------------------------------------------------------------
# Dispatch
# ---------------------------------------------------------------------------
SUBCMD="${1:-}"
[ -n "$SUBCMD" ] || { cmd_help; exit 0; }
shift

case "$SUBCMD" in
  provision-ga4)         cmd_provision_ga4 "$@" ;;
  provision-gsc)         cmd_provision_gsc "$@" ;;
  provision-instagram)   cmd_provision_instagram "$@" ;;
  provision-google-ads)  cmd_provision_google_ads "$@" ;;
  provision-all)         cmd_provision_all "$@" ;;
  status)                cmd_status "$@" ;;
  --help|-h|help)        cmd_help ;;
  *) die "Unknown subcommand: $SUBCMD — run '$0 --help' for usage" ;;
esac
