#!/bin/bash
# Run INSIDE taler-hacktivism-exchange-ansible.
# Writes /var/www/exchange-landing/stats.json
#
# Data lives in Postgres DB taler-exchange, schema exchange.*
# (reserves, reserves_in, known_coins, withdraw, denominations, …)
#
# IMPORTANT: never use  psql -F$'\t' -v ON_ERROR_STOP=1
# If the tab arg is lost, -F eats -v and ON_ERROR_STOP=1 becomes the
# *username* → peer auth fails → silent empty counts (all zeros).
# Never overwrite stats.json on failure — write stats-run.json instead.
set -euo pipefail
export TZ="${TZ:-Europe/Zurich}"
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin${PATH:+:$PATH}"
LANDING_DIR="${LANDING_DIR:-/var/www/exchange-landing}"
OUT="$LANDING_DIR/stats.json"
RUN="$LANDING_DIR/stats-run.json"
TMP="${OUT}.tmp.$$"
DB="${EXCHANGE_DB:-taler-exchange}"
BASE_URL="${EXCHANGE_BASE_URL:-https://exchange.hacktivism.ch}"
ERRLOG="${LANDING_STATS_ERRLOG:-/var/log/landing-stats-exchange.err}"
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_human() { date +"%Y-%m-%d %H:%M %Z"; }
json_str() {
  printf '"%s"' "$(printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr '\n\r\t' '   ')"
}

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
}

abort() {
  write_run false "$*"
  printf '%s\n' "abort exchange-stats: $*" | tee -a "$ERRLOG" >&2
  rm -f "$TMP" 2>/dev/null || true
  exit 1
}

# Prefer runuser; fall back to su. Always absolute-ish via PATH.
as_postgres() {
  if command -v runuser >/dev/null 2>&1; then
    runuser -u postgres -- "$@"
  elif command -v su >/dev/null 2>&1; then
    su -s /bin/bash postgres -c "$*"
  else
    return 127
  fi
}

# Single value or one row with '|' separators (no -F, no -v flags).
psqlq() {
  local sql="$1" out ec
  set +e
  out=$(as_postgres psql -d "$DB" -At -c "$sql" 2>>"$ERRLOG")
  ec=$?
  set -e
  if [ "$ec" -ne 0 ]; then
    abort "psql failed (ec=$ec): ${sql:0:120}"
  fi
  printf '%s' "$out"
}

fmt_goa() {
  local v="${1:-0}" f="${2:-0}"
  v=${v//[^0-9-]/}; f=${f//[^0-9-]/}; v=${v:-0}; f=${f:-0}
  if [ "$f" = "0" ] || [ -z "$f" ]; then
    printf 'GOA:%s' "$v"
    return
  fi
  awk -v v="$v" -v f="$f" 'BEGIN{
    frac = sprintf("%08d", f+0); sub(/0+$/, "", frac)
    if (frac == "") printf "GOA:%d", v+0
    else printf "GOA:%d.%s", v+0, frac
  }'
}

q_count() {
  local n
  n=$(psqlq "SELECT count(*)::text FROM $1;")
  # strip whitespace/newlines
  n=$(printf '%s' "$n" | tr -d '[:space:]')
  [[ "$n" =~ ^[0-9]+$ ]] || abort "bad count for $1: '$n'"
  printf '%s' "$n"
}

# --- preflight ---
command -v psql >/dev/null 2>&1 || abort "psql not in PATH ($PATH)"
command -v runuser >/dev/null 2>&1 || command -v su >/dev/null 2>&1 || abort "neither runuser nor su in PATH"
PROBE=$(psqlq "SELECT 1;")
[ "$PROBE" = "1" ] || abort "postgres not reachable (SELECT 1 → '$PROBE')"

# --- coin lifecycle ---
KNOWN_COINS=$(q_count "exchange.known_coins")
COINS_LIVE=$(psqlq "SELECT count(*)::text FROM exchange.known_coins WHERE (remaining).val > 0 OR (remaining).frac > 0;")
COINS_LIVE=$(printf '%s' "${COINS_LIVE:-0}" | tr -d '[:space:]')
COINS_LIVE=${COINS_LIVE:-0}
COINS_SPENT=$(( KNOWN_COINS - COINS_LIVE ))

REM_ROW=$(psqlq "SELECT coalesce(sum((remaining).val),0)::text || '|' || coalesce(sum((remaining).frac),0)::text FROM exchange.known_coins;")
IFS='|' read -r REM_VAL REM_FRAC <<<"${REM_ROW:-0|0}"
REMAINING_AMT=$(fmt_goa "${REM_VAL:-0}" "${REM_FRAC:-0}")

DENOMS_TOTAL=$(q_count "exchange.denominations")
DENOM_VALUES=$(psqlq "SELECT count(DISTINCT ((coin).val, (coin).frac))::text FROM exchange.denominations;")
DENOM_VALUES=$(printf '%s' "${DENOM_VALUES:-0}" | tr -d '[:space:]')

NOW_US=$(date +%s)000000
DENOMS_WITHDRAWABLE=$(psqlq "SELECT count(*)::text FROM exchange.denominations WHERE valid_from <= ${NOW_US} AND expire_withdraw > ${NOW_US};")
DENOMS_WITHDRAWABLE=$(printf '%s' "${DENOMS_WITHDRAWABLE:-0}" | tr -d '[:space:]')

RESERVES=$(q_count "exchange.reserves")
RESERVES_IN=$(q_count "exchange.reserves_in")
WIN_ROW=$(psqlq "SELECT coalesce(sum((credit).val),0)::text || '|' || coalesce(sum((credit).frac),0)::text FROM exchange.reserves_in;")
IFS='|' read -r WIN_VAL WIN_FRAC <<<"${WIN_ROW:-0|0}"
WIRE_IN_AMT=$(fmt_goa "${WIN_VAL:-0}" "${WIN_FRAC:-0}")

WITHDRAW_OPS=$(q_count "exchange.withdraw")
WOUT_ROW=$(psqlq "SELECT coalesce(sum((amount_with_fee).val),0)::text || '|' || coalesce(sum((amount_with_fee).frac),0)::text FROM exchange.withdraw;")
IFS='|' read -r WO_VAL WO_FRAC <<<"${WOUT_ROW:-0|0}"
WITHDRAW_AMT=$(fmt_goa "${WO_VAL:-0}" "${WO_FRAC:-0}")

REFRESH_OPS=$(q_count "exchange.refresh")
RECOUP=$(q_count "exchange.recoup")
REFUNDS=$(q_count "exchange.refunds")
COIN_DEPOSITS=$(q_count "exchange.coin_deposits")
BATCH_DEPOSITS=$(q_count "exchange.batch_deposits")
WIRE_OUT=$(q_count "exchange.wire_out")
COIN_HISTORY=$(q_count "exchange.coin_history")
WIRE_ACCTS=$(q_count "exchange.wire_accounts")

# known coins by denom value (pipe-separated)
BY_DENOM_TSV=$(psqlq "
SELECT (d.coin).val::text || '|' || (d.coin).frac::text || '|' || count(*)::text || '|' ||
       count(*) FILTER (WHERE (k.remaining).val > 0 OR (k.remaining).frac > 0)::text
FROM exchange.known_coins k
JOIN exchange.denominations d ON d.denominations_serial = k.denominations_serial
GROUP BY (d.coin).val, (d.coin).frac
ORDER BY (d.coin).val, (d.coin).frac;
")

BY_DENOM_JSON="["
bf=1
while IFS='|' read -r dv df cnt live; do
  [ -z "${dv:-}" ] && continue
  amt=$(fmt_goa "$dv" "$df")
  if [ "$bf" = 1 ]; then bf=0; else BY_DENOM_JSON="${BY_DENOM_JSON},"; fi
  BY_DENOM_JSON="${BY_DENOM_JSON}
    {\"value\": $(json_str "$amt"), \"coins\": ${cnt:-0}, \"live\": ${live:-0}}"
done <<<"$BY_DENOM_TSV"
BY_DENOM_JSON="${BY_DENOM_JSON}
  ]"

LADDER_TSV=$(psqlq "
SELECT (coin).val::text || '|' || (coin).frac::text || '|' || count(*)::text
FROM exchange.denominations
GROUP BY (coin).val, (coin).frac
ORDER BY (coin).val, (coin).frac;
")
LADDER_JSON="["
lf=1
while IFS='|' read -r dv df nkeys; do
  [ -z "${dv:-}" ] && continue
  amt=$(fmt_goa "$dv" "$df")
  if [ "$lf" = 1 ]; then lf=0; else LADDER_JSON="${LADDER_JSON},"; fi
  LADDER_JSON="${LADDER_JSON}
    {\"value\": $(json_str "$amt"), \"keys\": ${nkeys:-0}}"
done <<<"$LADDER_TSV"
LADDER_JSON="${LADDER_JSON}
  ]"

# recent wire-in / withdraw activity (no personal names — reserve_pub hex truncated)
RECENT_JSON="["
rf=1
while IFS='|' read -r ts val frac; do
  [ -z "${ts:-}" ] && continue
  sec=$(awk -v t="$ts" 'BEGIN{printf "%d", int(t/1000000)}')
  human=$(date -d "@${sec}" +"%Y-%m-%d %H:%M %Z" 2>/dev/null || echo "$sec")
  amt=$(fmt_goa "${val:-0}" "${frac:-0}")
  if [ "$rf" = 1 ]; then rf=0; else RECENT_JSON="${RECENT_JSON},"; fi
  RECENT_JSON="${RECENT_JSON}
    {\"kind\": \"wire_in\", \"amount\": $(json_str "$amt"), \"ts_human\": $(json_str "$human"), \"ts_us\": ${ts:-0}}"
done < <(psqlq "
SELECT execution_date::text || '|' || (credit).val::text || '|' || (credit).frac::text
FROM exchange.reserves_in
ORDER BY execution_date DESC
LIMIT 8;
")
while IFS='|' read -r ts val frac; do
  [ -z "${ts:-}" ] && continue
  sec=$(awk -v t="$ts" 'BEGIN{printf "%d", int(t/1000000)}')
  human=$(date -d "@${sec}" +"%Y-%m-%d %H:%M %Z" 2>/dev/null || echo "$sec")
  amt=$(fmt_goa "${val:-0}" "${frac:-0}")
  if [ "$rf" = 1 ]; then rf=0; else RECENT_JSON="${RECENT_JSON},"; fi
  RECENT_JSON="${RECENT_JSON}
    {\"kind\": \"withdraw\", \"amount\": $(json_str "$amt"), \"ts_human\": $(json_str "$human"), \"ts_us\": ${ts:-0}}"
done < <(psqlq "
SELECT execution_date::text || '|' || (amount_with_fee).val::text || '|' || (amount_with_fee).frac::text
FROM exchange.withdraw
ORDER BY execution_date DESC
LIMIT 6;
")
RECENT_JSON="${RECENT_JSON}
  ]"

# live performance
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{printf "%d", (t+0)*1000}'
}
KEYS_MS=$(measure_ms "${BASE_URL}/keys")
CONFIG_MS=$(measure_ms "${BASE_URL}/config")
KEYS_HTTP=$(curl -sS -o /dev/null -m 8 -w '%{http_code}' "${BASE_URL}/keys" 2>/dev/null || echo "000")
CONFIG_HTTP=$(curl -sS -o /dev/null -m 8 -w '%{http_code}' "${BASE_URL}/config" 2>/dev/null || echo "000")
LOADAVG=""
[ -r /proc/loadavg ] && LOADAVG=$(awk '{print $1","$2","$3}' /proc/loadavg)

WW="false"
pgrep -f taler-exchange-wirewatch >/dev/null 2>&1 && WW="true"

STAT_EVENTS=$(q_count "exchange.exchange_statistic_counter_event")

# Fail closed: zero denoms usually means broken query path (table always has keys)
if [ "${DENOMS_TOTAL:-0}" = "0" ]; then
  abort "denominations count is 0 — refusing to publish (check peer auth / PATH)"
fi

GEN=$(now_iso)
HUMAN=$(now_human)
num_or_null() { case "${1:-}" in ''|null) echo null ;; *) echo "$1" ;; esac; }

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,
  "source": "exchange-db",
  "schema": "exchange.* @ taler-exchange",
  "focus": "coins",
  "timezone": $(json_str "$TZ"),
  "generated_at": $(json_str "$GEN"),
  "generated_at_human": $(json_str "$HUMAN"),
  "reserves": ${RESERVES:-0},
  "wire_in_count": ${RESERVES_IN:-0},
  "wire_in_amount": $(json_str "$WIRE_IN_AMT"),
  "wire_out": ${WIRE_OUT:-0},
  "wire_accounts": ${WIRE_ACCTS:-0},
  "wirewatch_running": $WW,
  "known_coins": ${KNOWN_COINS:-0},
  "coins_live": ${COINS_LIVE:-0},
  "coins_spent": ${COINS_SPENT:-0},
  "coins_remaining_amount": $(json_str "$REMAINING_AMT"),
  "withdraw_ops": ${WITHDRAW_OPS:-0},
  "withdraw_amount": $(json_str "$WITHDRAW_AMT"),
  "refresh_ops": ${REFRESH_OPS:-0},
  "recoup": ${RECOUP:-0},
  "refunds": ${REFUNDS:-0},
  "coin_deposits": ${COIN_DEPOSITS:-0},
  "batch_deposits": ${BATCH_DEPOSITS:-0},
  "coin_history_events": ${COIN_HISTORY:-0},
  "denominations": ${DENOMS_TOTAL:-0},
  "denom_values": ${DENOM_VALUES:-0},
  "denoms_withdrawable": ${DENOMS_WITHDRAWABLE:-0},
  "by_denom": $BY_DENOM_JSON,
  "denom_ladder": $LADDER_JSON,
  "recent_activity": $RECENT_JSON,
  "performance": {
    "keys_http": $(json_str "$KEYS_HTTP"),
    "keys_ms": $(num_or_null "$KEYS_MS"),
    "config_http": $(json_str "$CONFIG_HTTP"),
    "config_ms": $(num_or_null "$CONFIG_MS"),
    "loadavg": $(json_str "${LOADAVG:-}"),
    "statistic_counter_events": ${STAT_EVENTS:-0},
    "memory": {
${MEM_JSON}
    }
  }
}
EOF
grep -q '"ok": true' "$TMP" || abort "tmp json missing ok:true"
mv -f "$TMP" "$OUT"
write_run true
echo "ok exchange reserves=$RESERVES wire_in=$WIRE_IN_AMT coins=$KNOWN_COINS withdraw=$WITHDRAW_AMT denoms=$DENOMS_TOTAL keys_ms=$KEYS_MS -> $OUT"
