#!/bin/bash
# Generate public landing stats JSON for bank.hacktivism.ch intro page.
#
# *** Run INSIDE the bank container *** (taler-hacktivism-bank).
# Writes: $LANDING_DIR/stats.json  →  https://bank.hacktivism.ch/intro/stats.json
#
# Pure bash + curl + awk (no python). Times in Europe/Zurich (CET/CEST).
#
# Documented: configs/bank-landing/README.md
set -euo pipefail

LANDING_DIR="${LANDING_DIR:-/var/www/bank-landing}"
# Demo funding account (for recent withdraw list / flow)
BANK_USER="${BANK_USER:-explorer}"
# Admin lists all accounts + can read other accounts' txs
ADMIN_USER="${ADMIN_USER:-admin}"
# Per-account transaction window (libeufin delta). Was -100 → systematically
# undercounted credits/withdraws on active accounts (broken public stats).
TX_DELTA="${TX_DELTA:--50000}"
# Max accounts to list + scan (was 80 → missed later accounts; bank has 100+).
MAX_SCAN_ACCOUNTS="${MAX_SCAN_ACCOUNTS:-500}"
# Account-list page size for GET /accounts?delta=… (must cover all users)
ACCOUNTS_DELTA="${ACCOUNTS_DELTA:--500}"
# curl timeout per account (deeper history needs more headroom)
TX_CURL_TIMEOUT="${TX_CURL_TIMEOUT:-25}"
export TZ="${TZ:-Europe/Zurich}"

PASS="${BANK_PASS:-}"
ADMIN_PASS="${BANK_ADMIN_PASS:-}"

detect_bank() {
  if [ -n "${BANK_URL:-}" ]; then
    echo "${BANK_URL%/}"
    return
  fi
  local port="" conf u code
  for conf in /etc/libeufin/bank-overrides.conf /etc/libeufin/libeufin-bank.conf; do
    if [ -f "$conf" ]; then
      port=$(grep -E '^\s*PORT\s*=' "$conf" 2>/dev/null | tail -1 | awk -F= '{gsub(/ /,"",$2); print $2}')
      [ -n "$port" ] && break
    fi
  done
  port="${port:-9012}"
  for u in "http://127.0.0.1:${port}" "http://127.0.0.1:9012" "http://127.0.0.1:8080"; do
    code=$(curl -sS -m 2 -o /dev/null -w '%{http_code}' "$u/config" 2>/dev/null || echo 000)
    if [ "$code" = "200" ]; then
      echo "$u"
      return
    fi
  done
  echo "http://127.0.0.1:${port}"
}

BANK="$(detect_bank)"
BANK="${BANK%/}"

read_pass_file() {
  local f
  for f in "$@"; do
    if [ -f "$f" ] && [ -r "$f" ]; then
      tr -d '\n' <"$f"
      return 0
    fi
  done
  return 1
}

if [ -z "$PASS" ]; then
  PASS=$(read_pass_file \
    "/root/bank-${BANK_USER}-password.txt" \
    /root/bank-explorer-password.txt \
    "/etc/libeufin/secrets/bank-${BANK_USER}-password.txt" || true)
fi
if [ -z "$ADMIN_PASS" ]; then
  ADMIN_PASS=$(read_pass_file \
    /root/bank-admin-password.txt \
    /etc/libeufin/secrets/bank-admin-password.txt || true)
fi

WORKDIR=$(mktemp -d)
trap 'rm -rf "$WORKDIR"' EXIT
OUT="$LANDING_DIR/stats.json"
RUN="$LANDING_DIR/stats-run.json"
TMP="${OUT}.tmp.$$"
mkdir -p "$LANDING_DIR"

now_iso() { date +%Y-%m-%dT%H:%M%z | sed -E 's/([+-][0-9]{2})([0-9]{2})$/\1:\2/'; }
now_unix() { date +%s; }
now_human() { date +"%Y-%m-%d %H:%M %Z"; }
iso_from_unix() {
  local u="$1"
  date -d "@${u}" +%Y-%m-%dT%H:%M%z 2>/dev/null | sed -E 's/([+-][0-9]{2})([0-9]{2})$/\1:\2/' \
    || date -r "${u}" +%Y-%m-%dT%H:%M%z 2>/dev/null | sed -E 's/([+-][0-9]{2})([0-9]{2})$/\1:\2/' \
    || echo ""
}
# Human CEST/CET label for display: "2026-07-09 20:49 CEST"
human_from_unix() {
  local u="$1"
  date -d "@${u}" +"%Y-%m-%d %H:%M %Z" 2>/dev/null \
    || date -r "${u}" +"%Y-%m-%d %H:%M %Z" 2>/dev/null \
    || echo ""
}

json_str() {
  printf '"%s"' "$(printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/	/\\t/g' | tr '\n' ' ')"
}

# Public run status for intro page. Never wipe stats.json on failure.
write_run() {
  local ok_json="$1" msg="${2:-}"
  cat >"$RUN" <<EOF
{
  "ok": ${ok_json},
  "at": $(json_str "$(now_iso)"),
  "at_human": $(json_str "$(now_human)"),
  "error": $( [ -n "$msg" ] && json_str "$msg" || echo null )
}
EOF
}

write_err() {
  local msg="$1"
  write_run false "$msg"
  rm -f "$TMP" 2>/dev/null || true
  echo "error: $msg (stats.json left unchanged; stats-run.json updated)" >&2
}

token_for() {
  local user="$1" pass="$2" out="$3"
  echo '{"scope":"readonly"}' >"$WORKDIR/tok-req.json"
  curl -sS -m 12 -u "${user}:${pass}" \
    -H 'Content-Type: application/json' \
    -d @"$WORKDIR/tok-req.json" \
    "${BANK}/accounts/${user}/token" >"$out" || true
  awk -F'"' '/access_token/ {
    for (i=1;i<=NF;i++) if ($i=="access_token") { print $(i+2); exit }
  }' "$out" 2>/dev/null || true
}

extract_field() {
  # extract "key":"value" or "key": number from a JSON blob (first hit)
  local key="$1" file="$2"
  awk -v key="$key" '
  function between(s, k,   p, rest, q2, r) {
    p = index(s, "\"" k "\"")
    if (p == 0) return ""
    rest = substr(s, p + length(k) + 2)
    while (rest ~ /^[[:space:]:]/) rest = substr(rest, 2)
    if (substr(rest, 1, 1) == "\"") {
      rest = substr(rest, 2)
      q2 = index(rest, "\"")
      if (q2 == 0) return ""
      return substr(rest, 1, q2 - 1)
    }
    r = ""
    while (rest ~ /^[0-9]/) {
      r = r substr(rest, 1, 1)
      rest = substr(rest, 2)
    }
    return r
  }
  { print between($0, key); exit }
  ' "$file" 2>/dev/null
}

# --- tokens ---
if [ -z "$PASS" ] && [ -z "$ADMIN_PASS" ]; then
  write_err "no bank password (explorer or admin) for stats"
  exit 1
fi

ADMIN_TOKEN=""
if [ -n "$ADMIN_PASS" ]; then
  ADMIN_TOKEN=$(token_for "$ADMIN_USER" "$ADMIN_PASS" "$WORKDIR/admin-tok.json")
fi
USER_TOKEN=""
if [ -n "$PASS" ]; then
  USER_TOKEN=$(token_for "$BANK_USER" "$PASS" "$WORKDIR/user-tok.json")
fi
# Prefer admin for everything when available
AUTH_TOKEN="${ADMIN_TOKEN:-$USER_TOKEN}"
if [ -z "$AUTH_TOKEN" ]; then
  write_err "token failed (admin/explorer)"
  exit 1
fi

# --- account list ---
ACCOUNTS_N=0
ACCOUNTS_USERS=0
: >"$WORKDIR/usernames.txt"
if [ -n "$ADMIN_TOKEN" ]; then
  # Use a large negative delta so we list *all* accounts (delta=-80 truncated
  # the roster and undercounted bank_accounts + flow).
  curl -sS -m 30 -H "Authorization: Bearer ${ADMIN_TOKEN}" \
    "${BANK}/accounts?delta=${ACCOUNTS_DELTA}" >"$WORKDIR/accounts.json" || true
  # usernames from "username":"..."
  tr -d '\n' <"$WORKDIR/accounts.json" \
    | sed 's/},{/}\n{/g' \
    | while IFS= read -r line; do
        u=$(printf '%s' "$line" | sed -n 's/.*"username"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
        [ -n "$u" ] && echo "$u"
      done >"$WORKDIR/usernames.txt" || true
  ACCOUNTS_N=$(grep -cve '^\s*$' "$WORKDIR/usernames.txt" 2>/dev/null || echo 0)
  ACCOUNTS_USERS=$(grep -Eve '^(admin|exchange)$' "$WORKDIR/usernames.txt" 2>/dev/null | grep -cve '^\s*$' || echo 0)
else
  echo "$BANK_USER" >"$WORKDIR/usernames.txt"
  ACCOUNTS_N=1
  ACCOUNTS_USERS=1
fi

# --- scan txs: distinguish incoming (credit) vs withdraw (Taler debit) ---
# all-wd.tsv:  amount \t t_s \t subject \t username \t reserve
# all-in.tsv:  amount \t t_s \t subject \t username
# flow.tsv:    kind \t amount_num   kind = incoming | withdraw | other_out
: >"$WORKDIR/all-wd.tsv"
: >"$WORKDIR/all-in.tsv"
: >"$WORKDIR/flow.tsv"
SCAN_OK=0
SCAN_EMPTY=0
SCAN_FAIL=0
# Scan customer accounts only (not exchange/admin — exchange credits would double-count)
{
  echo "$BANK_USER"
  grep -Eve "^(admin|exchange|${BANK_USER})$" "$WORKDIR/usernames.txt" 2>/dev/null || true
} | awk 'NF && !seen[$0]++' | head -n "$MAX_SCAN_ACCOUNTS" >"$WORKDIR/scan-users.txt"

while IFS= read -r uname; do
  [ -n "$uname" ] || continue
  case "$uname" in admin|exchange) continue ;; esac
  # Safe filename (usernames are mostly [A-Za-z0-9_-])
  safe=$(printf '%s' "$uname" | tr -c 'A-Za-z0-9._-' '_')
  code=$(curl -sS -m "${TX_CURL_TIMEOUT}" -o "$WORKDIR/tx-${safe}.json" -w '%{http_code}' \
    -H "Authorization: Bearer ${AUTH_TOKEN}" \
    "${BANK}/accounts/${uname}/transactions?delta=${TX_DELTA}" 2>/dev/null || echo 000)
  # 204 / empty body: account with no transactions — normal, not an error
  if [ "$code" = "204" ] || [ ! -s "$WORKDIR/tx-${safe}.json" ]; then
    SCAN_EMPTY=$((SCAN_EMPTY + 1))
    continue
  fi
  if [ "$code" != "200" ]; then
    SCAN_FAIL=$((SCAN_FAIL + 1))
    continue
  fi
  SCAN_OK=$((SCAN_OK + 1))
  tr -d '\n' <"$WORKDIR/tx-${safe}.json" \
    | sed 's/},{/}\n{/g' \
    >"$WORKDIR/tx-lines.txt" 2>/dev/null || continue
  awk -v uname="$uname" -v flowf="$WORKDIR/flow.tsv" -v inf="$WORKDIR/all-in.tsv" '
  function between(s, key,   p, rest, q2, r) {
    p = index(s, "\"" key "\"")
    if (p == 0) return ""
    rest = substr(s, p + length(key) + 2)
    while (rest ~ /^[[:space:]:]/) rest = substr(rest, 2)
    if (substr(rest, 1, 1) == "\"") {
      rest = substr(rest, 2)
      q2 = index(rest, "\"")
      if (q2 == 0) return ""
      return substr(rest, 1, q2 - 1)
    }
    r = ""
    while (rest ~ /^[0-9]/) {
      r = r substr(rest, 1, 1)
      rest = substr(rest, 2)
    }
    return r
  }
  function amt_n(a,   p) {
    p = index(a, ":")
    if (p == 0) return 0
    return substr(a, p + 1) + 0
  }
  {
    line = $0
    dir = between(line, "direction")
    amt = between(line, "amount")
    ts  = between(line, "t_s")
    subj = between(line, "subject")
    low = tolower(subj)
    if (amt == "") next
    n = amt_n(amt)
    if (dir == "credit") {
      # Incoming: bank credits into customer accounts (admin top-up, transfers in)
      print "incoming\t" n >> flowf
      printf "%s\t%s\t%s\t%s\n", amt, ts, subj, uname >> inf
    } else if (dir == "debit" && index(low, "withdraw") > 0) {
      # Withdraw: Taler withdrawal debit → exchange → wallet coins
      print "withdraw\t" n >> flowf
      res = subj
      sub(/^.*[Ww]ithdrawal[ ]+/, "", res)
      gsub(/[^A-Za-z0-9]/, "", res)
      printf "%s\t%s\t%s\t%s\t%s\n", amt, ts, subj, uname, res
    } else if (dir == "debit") {
      # Other debits (non-withdraw transfers)
      print "other_out\t" n >> flowf
    }
  }
  ' "$WORKDIR/tx-lines.txt" >>"$WORKDIR/all-wd.tsv" 2>/dev/null || true
done <"$WORKDIR/scan-users.txt"

# Sum by kind
TOTAL_IN_N=$(awk -F'\t' '$1=="incoming"{s+=$2} END{printf "%.8f", s+0}' "$WORKDIR/flow.tsv" 2>/dev/null || echo 0)
TOTAL_WD_FLOW_N=$(awk -F'\t' '$1=="withdraw"{s+=$2} END{printf "%.8f", s+0}' "$WORKDIR/flow.tsv" 2>/dev/null || echo 0)
TOTAL_OTHER_OUT_N=$(awk -F'\t' '$1=="other_out"{s+=$2} END{printf "%.8f", s+0}' "$WORKDIR/flow.tsv" 2>/dev/null || echo 0)
N_INCOMING=$(awk -F'\t' '$1=="incoming"{c++} END{print c+0}' "$WORKDIR/flow.tsv" 2>/dev/null || echo 0)
# total out = withdraw + other debits
TOTAL_OUT_N=$(awk -v a="${TOTAL_WD_FLOW_N:-0}" -v b="${TOTAL_OTHER_OUT_N:-0}" 'BEGIN{printf "%.8f", a+b}')

# Sort by t_s descending
sort -t$'\t' -k2,2nr "$WORKDIR/all-wd.tsv" -o "$WORKDIR/all-wd-sorted.tsv" 2>/dev/null \
  || cp "$WORKDIR/all-wd.tsv" "$WORKDIR/all-wd-sorted.tsv"
sort -t$'\t' -k2,2nr "$WORKDIR/all-in.tsv" -o "$WORKDIR/all-in-sorted.tsv" 2>/dev/null \
  || cp "$WORKDIR/all-in.tsv" "$WORKDIR/all-in-sorted.tsv"

# Unique reserves = individual wallet withdraws (each wallet reserve_pub)
WALLETS_N=$(awk -F'\t' '$5!=""{print $5}' "$WORKDIR/all-wd-sorted.tsv" | sort -u | grep -cve '^\s*$' || echo 0)
# Accounts that funded at least one withdraw
ACCOUNTS_WITH_WD=$(awk -F'\t' '$4!=""{print $4}' "$WORKDIR/all-wd-sorted.tsv" | sort -u | grep -cve '^\s*$' || echo 0)

# Aggregates
NOW=$(now_unix)
GEN_ISO=$(now_iso)
GEN_HUMAN=$(date +"%Y-%m-%d %H:%M %Z")

# Build recent withdraws JSON array (up to 10) with CEST times — landing shows all 10
RECENT_WD_N="${RECENT_WD_N:-10}"
: >"$WORKDIR/recent.jsonl"
N_WD=0
TOTAL_WD=0
W24=0; N24=0; W7=0; N7=0
DAY=$((NOW - 86400))
WEEK=$((NOW - 7 * 86400))
LAST_AMT=""; LAST_TS=""; LAST_SUBJ=""

while IFS=$'\t' read -r amt ts subj uname res; do
  [ -n "$amt" ] || continue
  n=$(printf '%s' "$amt" | awk -F: '{print $2+0}')
  N_WD=$((N_WD + 1))
  TOTAL_WD=$(awk -v a="$TOTAL_WD" -v b="$n" 'BEGIN{printf "%.8f", a+b}')
  ts_n=${ts:-0}
  if [ "$ts_n" -ge "$DAY" ] 2>/dev/null; then
    W24=$(awk -v a="$W24" -v b="$n" 'BEGIN{printf "%.8f", a+b}')
    N24=$((N24 + 1))
  fi
  if [ "$ts_n" -ge "$WEEK" ] 2>/dev/null; then
    W7=$(awk -v a="$W7" -v b="$n" 'BEGIN{printf "%.8f", a+b}')
    N7=$((N7 + 1))
  fi
  if [ -z "$LAST_AMT" ]; then
    LAST_AMT=$amt
    LAST_TS=$ts_n
    LAST_SUBJ=$subj
  fi
  if [ "$N_WD" -le "$RECENT_WD_N" ]; then
    at_h=""; at_iso=""
    if [ -n "$ts_n" ] && [ "$ts_n" != "0" ]; then
      at_h=$(human_from_unix "$ts_n")
      at_iso=$(iso_from_unix "$ts_n")
    fi
    printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$amt" "$ts_n" "$at_h" "$at_iso" "$uname" "$res" >>"$WORKDIR/recent.jsonl"
  fi
done <"$WORKDIR/all-wd-sorted.tsv"

fmt_goa() {
  awk -v n="$1" 'BEGIN{
    if (n+0 == int(n+0)) printf "GOA:%d", int(n+0);
    else printf "GOA:%.8g", n+0;
  }'
}

TOTAL_AMT=$(fmt_goa "$TOTAL_WD")
TOTAL_IN_AMT=$(fmt_goa "${TOTAL_IN_N:-0}")
TOTAL_OUT_AMT=$(fmt_goa "${TOTAL_OUT_N:-0}")
TOTAL_WD_FLOW_AMT=$(fmt_goa "${TOTAL_WD_FLOW_N:-0}")
TOTAL_OTHER_OUT_AMT=$(fmt_goa "${TOTAL_OTHER_OUT_N:-0}")
W24_AMT=$(fmt_goa "$W24")
W7_AMT=$(fmt_goa "$W7")

LAST_AT_H=""; LAST_AT_ISO=""
if [ -n "${LAST_TS:-}" ] && [ "$LAST_TS" != "0" ]; then
  LAST_AT_H=$(human_from_unix "$LAST_TS")
  LAST_AT_ISO=$(iso_from_unix "$LAST_TS")
fi

# recent_withdraws JSON
RECENT_JSON="["
first=1
while IFS=$'\t' read -r amt ts_n at_h at_iso uname res; do
  [ -n "$amt" ] || continue
  if [ "$first" = 1 ]; then first=0; else RECENT_JSON="${RECENT_JSON},"; fi
  RECENT_JSON="${RECENT_JSON}
    {
      \"kind\": \"withdraw\",
      \"amount\": $(json_str "$amt"),
      \"at\": $(json_str "$at_h"),
      \"at_iso\": $(json_str "$at_iso"),
      \"at_unix\": ${ts_n:-null},
      \"account\": $(json_str "$uname"),
      \"reserve\": $(json_str "$res")
    }"
done <"$WORKDIR/recent.jsonl"
RECENT_JSON="${RECENT_JSON}
  ]"

# recent incoming (up to 5)
: >"$WORKDIR/recent-in.jsonl"
N_IN_LIST=0
while IFS=$'\t' read -r amt ts subj uname; do
  [ -n "$amt" ] || continue
  N_IN_LIST=$((N_IN_LIST + 1))
  [ "$N_IN_LIST" -le 5 ] || break
  ts_n=${ts:-0}
  at_h=""; at_iso=""
  if [ -n "$ts_n" ] && [ "$ts_n" != "0" ]; then
    at_h=$(human_from_unix "$ts_n")
    at_iso=$(iso_from_unix "$ts_n")
  fi
  printf '%s\t%s\t%s\t%s\t%s\n' "$amt" "$ts_n" "$at_h" "$at_iso" "$uname" >>"$WORKDIR/recent-in.jsonl"
done <"$WORKDIR/all-in-sorted.tsv"

RECENT_IN_JSON="["
first=1
while IFS=$'\t' read -r amt ts_n at_h at_iso uname; do
  [ -n "$amt" ] || continue
  if [ "$first" = 1 ]; then first=0; else RECENT_IN_JSON="${RECENT_IN_JSON},"; fi
  RECENT_IN_JSON="${RECENT_IN_JSON}
    {
      \"kind\": \"incoming\",
      \"amount\": $(json_str "$amt"),
      \"at\": $(json_str "$at_h"),
      \"at_iso\": $(json_str "$at_iso"),
      \"at_unix\": ${ts_n:-null},
      \"account\": $(json_str "$uname")
    }"
done <"$WORKDIR/recent-in.jsonl"
RECENT_IN_JSON="${RECENT_IN_JSON}
  ]"

# Balance of explorer (optional display not required in footer)
BALANCE="GOA:0"
if [ -n "${USER_TOKEN:-$AUTH_TOKEN}" ]; then
  curl -sS -m 10 -H "Authorization: Bearer ${USER_TOKEN:-$AUTH_TOKEN}" \
    "${BANK}/accounts/${BANK_USER}" >"$WORKDIR/acct.json" || true
  BALANCE=$(awk -F'"' '/"amount"/ {
    for (i=1;i<=NF;i++) if ($i=="amount") { print $(i+2); exit }
  }' "$WORKDIR/acct.json" 2>/dev/null || echo "GOA:0")
fi

# Demo block kept for page QR logic only (not shown in footer)
DEMO_URI=""; DEMO_AMT=""; DEMO_CREATED=""; DEMO_WID=""; DEMO_STATUS=""
[ -f "$LANDING_DIR/withdraw.uri" ] && DEMO_URI=$(tr -d '\n' <"$LANDING_DIR/withdraw.uri")
[ -f "$LANDING_DIR/withdraw.amount" ] && DEMO_AMT=$(tr -d '\n' <"$LANDING_DIR/withdraw.amount")
[ -f "$LANDING_DIR/withdraw.created" ] && DEMO_CREATED=$(tr -d '\n' <"$LANDING_DIR/withdraw.created")
if [ -n "$DEMO_URI" ]; then
  DEMO_WID=$(basename "$DEMO_URI")
  curl -sS -m 8 \
    "${BANK}/taler-integration/withdrawal-operation/${DEMO_WID}" \
    >"$WORKDIR/demo-wd.json" 2>/dev/null || true
  DEMO_STATUS=$(awk -F'"' '/"status"/ {
    for (i=1;i<=NF;i++) if ($i=="status") { print $(i+2); exit }
  }' "$WORKDIR/demo-wd.json" 2>/dev/null || true)
fi
DEMO_READY=false
case "$DEMO_STATUS" in
  pending|selected) DEMO_READY=true ;;
  "") [ -n "$DEMO_URI" ] && DEMO_READY=true ;;
esac
[ "$DEMO_STATUS" = "confirmed" ] && DEMO_READY=false
[ "$DEMO_STATUS" = "aborted" ] && DEMO_READY=false

# Sanity: never publish an empty-looking success if admin scan should have accounts
if [ -n "$ADMIN_TOKEN" ] && [ "${ACCOUNTS_N:-0}" = "0" ]; then
  write_err "accounts list empty after admin scan"
  exit 1
fi

# Live performance probes (same idea as exchange landing-stats)
# Milliseconds as integer, rounded (not truncated) so sub-ms loopback does not
# always show 0 when we fall back to in-container URLs.
measure_ms() {
  local url="$1" t
  t=$(curl -sS -o /dev/null -m 8 -w '%{time_total}' "$url" 2>/dev/null || echo "")
  [ -z "$t" ] && { echo "null"; return; }
  awk -v t="$t" 'BEGIN{
    ms = (t+0)*1000
    if (ms > 0 && ms < 1) ms = 1
    printf "%d", int(ms + 0.5)
  }'
}
num_or_null() { case "${1:-}" in ''|null) echo null ;; *) echo "$1" ;; esac; }
# Prefer public URL for real client latency (Caddy → bank); fall back to loopback.
# Integration used to probe only $BANK (127.0.0.1:9012) → ~0–1 ms and not comparable
# to /config which already used the public host.
BANK_PUBLIC_BASE="${BANK_PUBLIC_URL:-https://bank.hacktivism.ch}"
CONFIG_MS=$(measure_ms "${BANK_PUBLIC_BASE}/config")
CONFIG_HTTP=$(curl -sS -o /dev/null -m 8 -w '%{http_code}' "${BANK_PUBLIC_BASE}/config" 2>/dev/null || echo "000")
if [ "$CONFIG_HTTP" != "200" ]; then
  CONFIG_MS=$(measure_ms "${BANK}/config")
  CONFIG_HTTP=$(curl -sS -o /dev/null -m 8 -w '%{http_code}' "${BANK}/config" 2>/dev/null || echo "000")
fi
INT_MS=$(measure_ms "${BANK_PUBLIC_BASE}/taler-integration/config")
INT_HTTP=$(curl -sS -o /dev/null -m 8 -w '%{http_code}' "${BANK_PUBLIC_BASE}/taler-integration/config" 2>/dev/null || echo "000")
if [ "$INT_HTTP" != "200" ]; then
  INT_MS=$(measure_ms "${BANK}/taler-integration/config")
  INT_HTTP=$(curl -sS -o /dev/null -m 8 -w '%{http_code}' "${BANK}/taler-integration/config" 2>/dev/null || echo "000")
fi
WEBUI_MS=$(measure_ms "${BANK_PUBLIC_BASE}/webui/")
WEBUI_HTTP=$(curl -sS -o /dev/null -m 8 -w '%{http_code}' "${BANK_PUBLIC_BASE}/webui/" 2>/dev/null || echo "000")
LOADAVG=""
[ -r /proc/loadavg ] && LOADAVG=$(awk '{print $1","$2","$3}' /proc/loadavg)

MEM_JSON='"container_rss_human": "—"'
MEM_HELPER="${MEM_HELPER:-/usr/local/lib/landing-mem-snapshot.sh}"
if [ -f "$MEM_HELPER" ]; then
  # shellcheck disable=SC1090
  . "$MEM_HELPER"
  mem_snapshot_json || true
fi

cat >"$TMP" <<EOF
{
  "ok": true,
  "currency": "GOA",
  "timezone": $(json_str "$TZ"),
  "generated_at": $(json_str "$GEN_ISO"),
  "generated_at_human": $(json_str "$GEN_HUMAN"),
  "generated_at_unix": $NOW,
  "source": "in-container landing-stats.sh",
  "bank_url": $(json_str "$BANK"),
  "scan": {
    "tx_delta": $(json_str "$TX_DELTA"),
    "accounts_delta": $(json_str "$ACCOUNTS_DELTA"),
    "max_scan_accounts": ${MAX_SCAN_ACCOUNTS:-0},
    "accounts_listed": ${ACCOUNTS_N:-0},
    "accounts_scanned_ok": ${SCAN_OK:-0},
    "accounts_empty_tx": ${SCAN_EMPTY:-0},
    "accounts_scan_fail": ${SCAN_FAIL:-0},
    "note": "empty_tx includes HTTP 204 (no ledger rows) — normal for new auto-accounts"
  },
  "bank_accounts": {
    "total": ${ACCOUNTS_N:-0},
    "users": ${ACCOUNTS_USERS:-0},
    "with_withdraws": ${ACCOUNTS_WITH_WD:-0}
  },
  "wallets": {
    "unique_reserves": ${WALLETS_N:-0},
    "note": "unique reserve pubs from Taler withdrawals (one per wallet withdraw)"
  },
  "balance_explorer": $(json_str "$BALANCE"),
  "flow": {
    "incoming": {
      "label": "Incoming bank credits",
      "count": ${N_INCOMING:-0},
      "amount": $(json_str "$TOTAL_IN_AMT"),
      "value": ${TOTAL_IN_N:-0}
    },
    "withdraw": {
      "label": "Taler withdrawals to wallets",
      "count": ${N_WD:-0},
      "amount": $(json_str "$TOTAL_WD_FLOW_AMT"),
      "value": ${TOTAL_WD_FLOW_N:-0}
    },
    "other_out": {
      "label": "Other debits (non-withdraw)",
      "amount": $(json_str "$TOTAL_OTHER_OUT_AMT"),
      "value": ${TOTAL_OTHER_OUT_N:-0}
    },
    "total_in": $(json_str "$TOTAL_IN_AMT"),
    "total_in_value": ${TOTAL_IN_N:-0},
    "total_out": $(json_str "$TOTAL_OUT_AMT"),
    "total_out_value": ${TOTAL_OUT_N:-0},
    "note": "incoming=credits; withdraw=Taler withdrawal debits; excl. admin+exchange accounts"
  },
  "withdraws": {
    "count": ${N_WD:-0},
    "total_amount": $(json_str "$TOTAL_AMT"),
    "total_value": ${TOTAL_WD:-0},
    "last_amount": $( [ -n "$LAST_AMT" ] && json_str "$LAST_AMT" || echo null ),
    "last_at": $( [ -n "$LAST_AT_H" ] && json_str "$LAST_AT_H" || echo null ),
    "last_at_iso": $( [ -n "$LAST_AT_ISO" ] && json_str "$LAST_AT_ISO" || echo null ),
    "last_at_unix": ${LAST_TS:-null},
    "last_subject": $( [ -n "$LAST_SUBJ" ] && json_str "$LAST_SUBJ" || echo null ),
    "last_24h": { "count": ${N24:-0}, "amount": $(json_str "$W24_AMT"), "value": ${W24:-0} },
    "last_7d": { "count": ${N7:-0}, "amount": $(json_str "$W7_AMT"), "value": ${W7:-0} }
  },
  "recent_withdraws": $RECENT_JSON,
  "recent_incoming": $RECENT_IN_JSON,
  "demo": {
    "uri": $( [ -n "$DEMO_URI" ] && json_str "$DEMO_URI" || echo null ),
    "amount": $( [ -n "$DEMO_AMT" ] && json_str "$DEMO_AMT" || echo null ),
    "created": $( [ -n "$DEMO_CREATED" ] && json_str "$DEMO_CREATED" || echo null ),
    "withdrawal_id": $( [ -n "$DEMO_WID" ] && json_str "$DEMO_WID" || echo null ),
    "status": $( [ -n "$DEMO_STATUS" ] && json_str "$DEMO_STATUS" || echo null ),
    "ready": $DEMO_READY
  },
  "performance": {
    "config_http": $(json_str "$CONFIG_HTTP"),
    "config_ms": $(num_or_null "$CONFIG_MS"),
    "integration_http": $(json_str "$INT_HTTP"),
    "integration_ms": $(num_or_null "$INT_MS"),
    "webui_http": $(json_str "$WEBUI_HTTP"),
    "webui_ms": $(num_or_null "$WEBUI_MS"),
    "loadavg": $(json_str "${LOADAVG:-}"),
    "memory": {
${MEM_JSON}
    }
  }
}
EOF
grep -q '"ok": true' "$TMP" || { write_err "tmp json missing ok:true"; exit 1; }
mv -f "$TMP" "$OUT"
write_run true
echo "ok accounts=${ACCOUNTS_N} wallets=${WALLETS_N} withdraws=${N_WD} total=${TOTAL_AMT} tz=${TZ} -> $OUT"
