#!/usr/bin/env bash
# check-brand-cache.sh — reconcile the bytes the public hostname returns
# against the bytes on disk (Task 2019).
#
# The failure this exists for emits no event at either end. The installer
# writes the new brand bytes and logs a successful upgrade; the origin serves
# them correctly to anyone asking it directly; Cloudflare's edge serves the
# pre-upgrade ones and reports `HIT` to nobody looking. There is no action to
# log and re-running the upgrade does not surface it, so only a reconciliation
# of served bytes against on-disk bytes reveals the state. That is the
# condition behind the 2026-07-26 report, where the regenerated Real Agent logo
# was correct on disk and invisible through the hostname for four hours.
#
#   $ check-brand-cache.sh --install-dir <dir> --config-dir <dir>
#
# stdout, exit 0:  [brand-cache] clean host=<h> files=<n>
# stdout, exit 0:  [brand-cache] skipped host=none reason=no-tunnel-ingress
# stderr, exit 1:  [brand-cache] STALE host=<h> path=<p> edge=<sha7> disk=<sha7> cf-cache-status=<s> age=<n>
# stderr, exit 1:  [brand-cache] UNREACHABLE host=<h> path=<p> http=<code>
#
# No ingress hostname and no alias domain means no edge and therefore no
# possible divergence, so that is a skip, not a pass — a caller must never read
# the two as the same.
#
# Scope: the files under server/public/brand/ only. See the PATHS block below.
#
# BRAND_CACHE_CURL overrides the fetch command (tests stub it).
set -uo pipefail

INSTALL_DIR=""
CONFIG_DIR=""
while [ $# -gt 0 ]; do
  case "$1" in
    --install-dir) INSTALL_DIR="$2"; shift 2 ;;
    --config-dir) CONFIG_DIR="$2"; shift 2 ;;
    *) echo "[brand-cache] unknown argument: $1" >&2; exit 2 ;;
  esac
done
if [ -z "$INSTALL_DIR" ] || [ -z "$CONFIG_DIR" ]; then
  echo "[brand-cache] usage: check-brand-cache.sh --install-dir <dir> --config-dir <dir>" >&2
  exit 2
fi

CURL="${BRAND_CACHE_CURL:-curl}"

# sha256sum is GNU; darwin ships shasum. Both print "<digest>  <path>".
sha256() {
  if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{print $1}'
  else shasum -a 256 "$1" | awk '{print $1}'; fi
}

# The hostname to probe, read from the same two sources and with the same
# exclusions as platform/plugins/admin/mcp/src/lib/public-hostname.ts:
# cloudflared/config.yml first, then alias-domains.json. First non-excluded
# wins within each source.
#
# No apex tiebreak, and no attempt to pick the *public* host over the admin
# one: only /brand/ assets are fetched, and the admin gate exempts /brand/ on
# every host class (server/index.ts:1311-1322), so any ingress host serves them
# identically. The alias source is read because an install whose edge is an
# alias domain with no ingress row does have an edge that can diverge, and
# reporting `skipped` there would read as nothing-to-check.
resolve_host() {
  local cfg="$CONFIG_DIR/cloudflared/config.yml" alias="$CONFIG_DIR/alias-domains.json" h
  if [ -f "$cfg" ]; then
    h="$(sed -n 's/^[[:space:]]*-[[:space:]]*hostname:[[:space:]]*\([^[:space:]]*\).*/\1/p' "$cfg" \
      | grep -v '^localhost$' | grep -v '\.local$' | head -1)"
    if [ -n "$h" ]; then echo "$h"; return 0; fi
  fi
  [ -f "$alias" ] || return 0
  tr ',' '\n' < "$alias" | sed -n 's/.*"\([^"]*\)".*/\1/p' \
    | grep -v '^localhost$' | grep -v '\.local$' | head -1
}

HOST="$(resolve_host)"
if [ -z "$HOST" ]; then
  echo "[brand-cache] skipped host=none reason=no-tunnel-ingress"
  exit 0
fi

# /brand/ only. /brand-defaults.css is NOT exempt from the admin gate
# (server/index.ts:1311-1322 lists /favicon.ico, /assets/, /brand/,
# /api/calendar/free-busy and the PWA paths, and nothing else), so through an
# admin-gated or operator host it answers with a login page at status 200
# (index.ts:1379). Hashing that against the stylesheet on disk would be a
# permanent STALE with no cache involved, which would both bury a real
# divergence and make the clean line unreachable. Covering it, and the page
# shells, needs a resolved public hostname — Task 2026.
PUBLIC_DIR="$INSTALL_DIR/server/public"
PATHS=()
if [ -d "$PUBLIC_DIR/brand" ]; then
  while IFS= read -r f; do PATHS+=("/brand/$(basename "$f")"); done \
    < <(find "$PUBLIC_DIR/brand" -maxdepth 1 -type f | sort)
fi

# Nothing to compare is a skip with its own reason, not a clean run. Emitting
# `files=0` here would read as a pass on an install whose payload never
# deployed, and that is the dead signal the file count is pinned against.
# Also the guard for bash 3.2 (darwin), where "${PATHS[@]}" on an empty array
# is an unbound-variable error under `set -u`.
if [ "${#PATHS[@]}" -eq 0 ]; then
  echo "[brand-cache] skipped host=$HOST reason=no-brand-assets"
  exit 0
fi

TMP="$(mktemp -d "${TMPDIR:-/tmp}/brand-cache.XXXXXX")"
trap 'rm -rf "$TMP"' EXIT

FAILED=0
CHECKED=0
for p in "${PATHS[@]}"; do
  body="$TMP/body"; hdr="$TMP/hdr"
  "$CURL" -sS -L -D "$hdr" -o "$body" "https://$HOST$p" >/dev/null 2>&1
  code="$(awk 'tolower($0) ~ /^http\// {c=$2} END{print c+0}' "$hdr" 2>/dev/null)"
  if [ "$code" != "200" ]; then
    echo "[brand-cache] UNREACHABLE host=$HOST path=$p http=$code" >&2
    FAILED=1
    continue
  fi
  edge="$(sha256 "$body")"
  disk="$(sha256 "$PUBLIC_DIR$p")"
  if [ "$edge" != "$disk" ]; then
    # tail -1, not head -1: -L means $hdr can hold several header blocks, and
    # the cache state that matters is the one on the response the bytes came
    # from. head -1 would name a redirect hop's cache state instead, which is
    # the field an operator reads to tell this fault from an origin fault.
    status="$(awk 'tolower($1) == "cf-cache-status:" {print $2}' "$hdr" | tr -d '\r' | tail -1)"
    age="$(awk 'tolower($1) == "age:" {print $2}' "$hdr" | tr -d '\r' | tail -1)"
    echo "[brand-cache] STALE host=$HOST path=$p edge=${edge:0:7} disk=${disk:0:7} cf-cache-status=${status:-none} age=${age:-0}" >&2
    FAILED=1
    continue
  fi
  CHECKED=$((CHECKED + 1))
done

[ "$FAILED" -eq 1 ] && exit 1
echo "[brand-cache] clean host=$HOST files=$CHECKED"
exit 0
