#!/usr/bin/env bash
# Area versions-### — Taler packages vs deb.taler.net (trixie) + repo availability.
#
# Checks:
#   1) deb.taler.net apt endpoints reachable (InRelease / Packages / sample .deb)
#   2) containers can reach deb.taler.net (install path from inside)
#   3) installed taler*/libeufin*/libtaler*/libdonau* versions vs suite index
#
# Env:
#   TALER_APT_SUITE=trixie
#   TALER_APT_BASE=https://deb.taler.net/apt/debian
#   TALER_APT_INDEX=…/dists/trixie/main/binary-amd64/Packages
#   TALER_APT_TESTING_INDEX=…/dists/trixie-testing/…
#   TALER_PKG_BEHIND=warn|error   (default: core packages ERROR if behind, else warn)
#   SKIP_SSH=1                    skip container install checks
set -euo pipefail
ROOT=$(cd "$(dirname "$0")" && pwd)
# shellcheck source=lib.sh
source "$ROOT/lib.sh"

set_area versions

SUITE="${TALER_APT_SUITE:-trixie}"
APT_BASE="${TALER_APT_BASE:-https://deb.taler.net/apt/debian}"
APT_BASE="${APT_BASE%/}"
INDEX="${TALER_APT_INDEX:-${APT_BASE}/dists/${SUITE}/main/binary-amd64/Packages}"
INRELEASE="${TALER_APT_INRELEASE:-${APT_BASE}/dists/${SUITE}/InRelease}"
TESTING_INDEX="${TALER_APT_TESTING_INDEX:-${APT_BASE}/dists/${SUITE}-testing/main/binary-amd64/Packages}"
BEHIND_MODE="${TALER_PKG_BEHIND:-warn}"

tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT

http_get() {
  # usage: http_get URL [outfile]  → prints http_code; body to outfile or /dev/null
  local url="$1" out="${2:-/dev/null}"
  curl -sS --max-time 30 -L -o "$out" -w '%{http_code}' "$url" 2>/dev/null || echo 000
}

# ---------------------------------------------------------------------------
# 1) OUTSIDE — deb.taler.net install source (runner / laptop, no SSH)
# ---------------------------------------------------------------------------
section "versions · outside · deb.taler.net (${SUITE})"

# DNS
HOST_APT="${APT_BASE#https://}"; HOST_APT="${HOST_APT#http://}"; HOST_APT="${HOST_APT%%/*}"
if getent hosts "$HOST_APT" >/dev/null 2>&1 \
  || python3 -c "import socket; socket.getaddrinfo('${HOST_APT}', 443)" >/dev/null 2>&1; then
  ip=$(python3 -c "import socket; print(socket.getaddrinfo('${HOST_APT}',443)[0][4][0])" 2>/dev/null || true)
  ok "outside DNS ${HOST_APT}" "${ip:-resolved}"
else
  fail "outside DNS ${HOST_APT}" "unresolvable — cannot use as apt source"
fi

# HTTPS portal / apt root
code=$(http_get "https://${HOST_APT}/")
[ "$code" = "200" ] && ok "outside https://${HOST_APT}/" "HTTP $code" \
  || warn "outside https://${HOST_APT}/" "HTTP $code"

code=$(http_get "${APT_BASE}/")
case "$code" in
  200|301|302) ok "outside apt base" "${APT_BASE}/ HTTP $code" ;;
  *) fail "outside apt base" "${APT_BASE}/ HTTP $code" ;;
esac

# Suite metadata (what apt update needs)
code=$(http_get "$INRELEASE" "$tmp/InRelease")
if [ "$code" = "200" ] && [ -s "$tmp/InRelease" ]; then
  ok "outside InRelease" "${SUITE} HTTP 200 · $(wc -c <"$tmp/InRelease" | tr -d ' ') bytes"
else
  fail "outside InRelease" "HTTP $code — $INRELEASE (apt update will fail)"
fi

RELEASE_URL="${APT_BASE}/dists/${SUITE}/Release"
code=$(http_get "$RELEASE_URL" "$tmp/Release")
[ "$code" = "200" ] && ok "outside Release" "${SUITE} HTTP 200" \
  || warn "outside Release" "HTTP $code — $RELEASE_URL"

# Packages (plain) — required for version compare
code=$(http_get "$INDEX" "$tmp/Packages")
if [ "$code" = "200" ] && [ -s "$tmp/Packages" ] && grep -q '^Package: ' "$tmp/Packages"; then
  n_pkg=$(grep -c '^Package: ' "$tmp/Packages" || true)
  ok "outside Packages" "${SUITE} HTTP 200 · ${n_pkg} packages"
else
  fail "outside Packages" "HTTP $code — $INDEX"
  # still try more probes, but cannot compare versions without index
fi

# Packages.gz (apt often prefers this)
PKGZ_URL="${APT_BASE}/dists/${SUITE}/main/binary-amd64/Packages.gz"
code=$(http_get "$PKGZ_URL" "$tmp/Packages.gz")
if [ "$code" = "200" ] && [ -s "$tmp/Packages.gz" ]; then
  ok "outside Packages.gz" "${SUITE} HTTP 200 · $(wc -c <"$tmp/Packages.gz" | tr -d ' ') bytes"
else
  warn "outside Packages.gz" "HTTP $code — apt may still use plain Packages"
fi

# Sample pool .deb downloadable (proves packages are installable, not just listed)
if [ -s "$tmp/Packages" ]; then
  DEB_PATH=$(awk '
    /^Package: taler-exchange$/ { p=1 }
    p && /^Filename: / { sub(/^Filename: /,""); print; exit }
    p && /^$/ { p=0 }
  ' "$tmp/Packages")
  [ -z "$DEB_PATH" ] && DEB_PATH=$(awk '
    /^Package: taler-merchant$/ { p=1 }
    p && /^Filename: / { sub(/^Filename: /,""); print; exit }
    p && /^$/ { p=0 }
  ' "$tmp/Packages")
  if [ -n "$DEB_PATH" ]; then
    DEB_URL="${APT_BASE}/${DEB_PATH}"
    code=$(curl -sS --max-time 30 -o /dev/null -w '%{http_code}' -r 0-128 "$DEB_URL" 2>/dev/null || echo 000)
    case "$code" in
      200|206)
        ok "outside pool .deb" "$(basename "$DEB_PATH") HTTP $code"
        ;;
      *)
        fail "outside pool .deb" "HTTP $code — $DEB_URL (index ok but debs not fetchable)"
        ;;
    esac
  else
    warn "outside pool .deb" "no Filename for taler-exchange/merchant in index"
  fi

  for pkg in taler-exchange taler-merchant libeufin-bank; do
    ver=$(awk -v p="$pkg" '
      $0=="Package: "p { hit=1; next }
      hit && /^Version: / { sub(/^Version: /,""); print; exit }
      hit && /^$/ { hit=0 }
    ' "$tmp/Packages")
    if [ -n "$ver" ]; then
      ok "outside suite offers ${pkg}" "$ver"
    else
      fail "outside suite offers ${pkg}" "missing from ${SUITE} Packages"
    fi
  done
fi

HAVE_TESTING=0
code=$(http_get "$TESTING_INDEX" "$tmp/Packages-testing")
if [ "$code" = "200" ] && [ -s "$tmp/Packages-testing" ] && grep -q '^Package: ' "$tmp/Packages-testing"; then
  HAVE_TESTING=1
  ok "outside ${SUITE}-testing Packages" "HTTP 200 (optional compare)"
else
  info "outside ${SUITE}-testing Packages" "HTTP ${code:-000} (optional)"
fi

# TLS: certificate verify (curl default) already used above; explicit openssl probe if available
if command -v openssl >/dev/null 2>&1; then
  if echo | openssl s_client -servername "$HOST_APT" -connect "${HOST_APT}:443" 2>/dev/null \
    | grep -q 'Verify return code: 0'; then
    ok "outside TLS ${HOST_APT}" "verify ok"
  else
    # curl succeeded with default CA — soft warn only
    warn "outside TLS ${HOST_APT}" "openssl verify not clean (curl may still work)"
  fi
fi

# Need Packages index for later compare
if [ ! -s "$tmp/Packages" ] || ! grep -q '^Package: ' "$tmp/Packages"; then
  fail "outside Packages usable" "cannot continue version compare without suite index"
  summary
  exit 1
fi

# ---------------------------------------------------------------------------
# 2) INSIDE — containers can reach deb.taler.net (pasta / install path)
# ---------------------------------------------------------------------------
section "versions · inside · containers → deb.taler.net"

if [ "${SKIP_SSH}" = "1" ]; then
  warn "ssh" "SKIP_SSH=1 — skip container reachability + installed package compare"
  info "outside-only" "deb.taler.net public checks completed above"
  summary
  exit 0
fi
if ! koopa_ssh_ok; then
  err "ssh" "cannot reach ${KOOPA_SSH} — set SKIP_SSH=1 to skip container checks"
  info "outside-only" "deb.taler.net public checks completed above"
  summary
  exit 1
fi
ok "ssh ${KOOPA_SSH}"

INRELEASE_URL="${APT_BASE}/dists/${SUITE}/InRelease"
# Write remote script to a file (avoids bash 3.2 parse bugs with case/;; inside $(…)<<heredoc).
{
  printf 'INRELEASE_URL=%q\n' "$INRELEASE_URL"
  cat <<'REMOTE'
set +e
resolve_ctr() {
  local want="$1" c
  c=$(podman ps --format '{{.Names}}' 2>/dev/null | grep -Fx "$want" | head -1)
  if [ -n "$c" ]; then echo "$c"; return; fi
  case "$want" in
    *exchange*) podman ps --format '{{.Names}}' 2>/dev/null | grep -i exchange | head -1 ;;
    *bank*)     podman ps --format '{{.Names}}' 2>/dev/null | grep -iE 'hacktivism-bank|taler-bank' | head -1 ;;
    *)
      c=$(podman ps --format '{{.Names}}' 2>/dev/null | grep -E '^taler-hacktivism$' | head -1)
      if [ -n "$c" ]; then echo "$c"; return; fi
      podman ps --format '{{.Names}}' 2>/dev/null | grep -iE 'merchant|hacktivism' | grep -viE 'bank|exchange' | head -1
      ;;
  esac
}
for want in taler-hacktivism-exchange-ansible taler-hacktivism-bank taler-hacktivism; do
  c=$(resolve_ctr "$want")
  [ -n "$c" ] || continue
  role=other
  case "$c" in
    *exchange*) role=exchange ;;
    *bank*)     role=bank ;;
    *)          role=merchant ;;
  esac
  code=$(podman exec "$c" curl -sS -m 12 -o /dev/null -w '%{http_code}' "$INRELEASE_URL" 2>/dev/null || echo 000)
  echo "R|${role}|${c}|${code}"
  if podman exec "$c" bash -lc 'grep -Rqs deb.taler.net /etc/apt/sources.list /etc/apt/sources.list.d 2>/dev/null'; then
    echo "S|${role}|${c}|yes"
  else
    echo "S|${role}|${c}|no"
  fi
  podman exec "$c" dpkg-query -W -f '${Package}\t${Version}\n' 2>/dev/null \
    | awk -v role="$role" -v ctr="$c" '
        $1 ~ /^(taler-|libeufin-|libtaler|libdonau)/ {
          printf "P|%s|%s|%s|%s\n", role, ctr, $1, $2
        }'
done
REMOTE
} >"$tmp/remote-versions.sh"

REMOTE_OUT=$(koopa_ssh_bash 60 <"$tmp/remote-versions.sh" || true)

printf '%s\n' "$REMOTE_OUT" | grep -E '^R\|' >"$tmp/reach.tsv" || true
printf '%s\n' "$REMOTE_OUT" | grep -E '^S\|' >"$tmp/sources.tsv" || true
printf '%s\n' "$REMOTE_OUT" | grep -E '^P\|' >"$tmp/installed.tsv" || true

if [ -s "$tmp/reach.tsv" ]; then
  while IFS='|' read -r _ role ctr code; do
    if [ "$code" = "200" ]; then
      ok "container ${role} → deb.taler.net" "${ctr} InRelease HTTP $code"
    else
      fail "container ${role} → deb.taler.net" "${ctr} InRelease HTTP $code — apt install/update will fail"
    fi
  done <"$tmp/reach.tsv"
else
  warn "container → deb.taler.net" "no reachability rows"
fi

if [ -s "$tmp/sources.tsv" ]; then
  while IFS='|' read -r _ role ctr has; do
    if [ "$has" = "yes" ]; then
      ok "container ${role} apt source" "${ctr} lists deb.taler.net"
    else
      warn "container ${role} apt source" "${ctr} no deb.taler.net in sources.list*"
    fi
  done <"$tmp/sources.tsv"
fi

if [ ! -s "$tmp/installed.tsv" ]; then
  fail "installed packages" "none found in taler containers"
  summary
  exit 1
fi
n_inst=$(wc -l <"$tmp/installed.tsv" | tr -d ' ')
ok "collected installed packages" "${n_inst} rows"

# ---------------------------------------------------------------------------
# 3) compare installed vs trixie (and note testing)
# ---------------------------------------------------------------------------
section "versions · compare installed vs ${SUITE}"

export BEHIND_MODE HAVE_TESTING
export TALER_APT_SUITE="$SUITE"
CORE_PKGS="taler-exchange taler-exchange-database taler-merchant taler-merchant-webui libeufin-bank libeufin-common libtalerexchange libtalermerchant taler-terms-generator"
export CORE_PKGS

python3 - "$tmp/Packages" "$tmp/Packages-testing" "$tmp/installed.tsv" <<'PY' >"$tmp/py.out"
import sys, os
from collections import defaultdict

main_idx, test_idx, inst_path = sys.argv[1:4]
have_testing = os.environ.get("HAVE_TESTING", "0") == "1"
core = set(os.environ.get("CORE_PKGS", "").split())
suite = os.environ.get("TALER_APT_SUITE", "trixie")

def _ord_ch(c):
    if c == "~":
        return -1
    if c.isdigit():
        return 0
    if c.isalpha():
        return ord(c)
    return ord(c) + 256

def _split_ver(v):
    epoch = 0
    if ":" in v:
        e, v = v.split(":", 1)
        try:
            epoch = int(e)
        except ValueError:
            epoch = 0
    if "-" in v:
        upstream, deb = v.rsplit("-", 1)
    else:
        upstream, deb = v, ""
    return epoch, upstream, deb

def _cmp_part(a, b):
    ia = ib = 0
    while ia < len(a) or ib < len(b):
        while True:
            ca = a[ia] if ia < len(a) and not a[ia].isdigit() else ""
            cb = b[ib] if ib < len(b) and not b[ib].isdigit() else ""
            if ca == "" and cb == "":
                break
            if ca == "" and cb:
                return -1 if _ord_ch(cb) > 0 else (1 if cb == "~" else -1)
            if cb == "" and ca:
                return 1 if _ord_ch(ca) > 0 else (-1 if ca == "~" else 1)
            oa, ob = _ord_ch(ca), _ord_ch(cb)
            ia += 1
            ib += 1
            if oa != ob:
                return (oa > ob) - (oa < ob)
        sa = sb = ""
        while ia < len(a) and a[ia].isdigit():
            sa += a[ia]; ia += 1
        while ib < len(b) and b[ib].isdigit():
            sb += b[ib]; ib += 1
        na = int(sa) if sa else 0
        nb = int(sb) if sb else 0
        if na != nb:
            return (na > nb) - (na < nb)
    return 0

def deb_cmp(a, b):
    if a == b:
        return 0
    ea, ua, da = _split_ver(a)
    eb, ub, db = _split_ver(b)
    if ea != eb:
        return (ea > eb) - (ea < eb)
    c = _cmp_part(ua, ub)
    if c:
        return c
    return _cmp_part(da, db)

def parse_packages(path):
    pkgs = {}
    if not path or not os.path.isfile(path) or os.path.getsize(path) == 0:
        return pkgs
    cur = ver = None
    with open(path, encoding="utf-8", errors="replace") as f:
        for line in f:
            line = line.rstrip("\n")
            if line.startswith("Package: "):
                if cur and ver and (cur not in pkgs or deb_cmp(ver, pkgs[cur]) > 0):
                    pkgs[cur] = ver
                cur = line[9:].strip()
                ver = None
            elif line.startswith("Version: ") and cur:
                ver = line[9:].strip()
            elif line == "" and cur:
                if ver and (cur not in pkgs or deb_cmp(ver, pkgs[cur]) > 0):
                    pkgs[cur] = ver
                cur = ver = None
        if cur and ver and (cur not in pkgs or deb_cmp(ver, pkgs[cur]) > 0):
            pkgs[cur] = ver
    return pkgs

main = parse_packages(main_idx)
testing = parse_packages(test_idx) if have_testing else {}

by_pkg = defaultdict(list)
with open(inst_path, encoding="utf-8") as f:
    for line in f:
        line = line.strip()
        if not line.startswith("P|"):
            continue
        parts = line.split("|")
        if len(parts) < 5:
            continue
        _, role, ctr, pkg, ver = parts[:5]
        by_pkg[pkg].append((role, ctr, ver))

for pkg in sorted(by_pkg.keys()):
    versions = sorted({v for _, _, v in by_pkg[pkg]}, key=lambda v: v)
    # pick "highest" via deb_cmp
    inst = versions[0]
    for v in versions[1:]:
        if deb_cmp(v, inst) > 0:
            inst = v
    roles = ",".join(sorted({r for r, _, _ in by_pkg[pkg]}))
    multi = len(set(versions)) > 1
    trixie = main.get(pkg)
    testv = testing.get(pkg)
    status = "ok"
    detail = ""
    if multi:
        status = "warn"
        detail = "multiple installed: " + ", ".join(sorted(set(versions)))
    if trixie is None:
        if status == "ok":
            status = "info"
        detail = (detail + "; " if detail else "") + f"not in {suite} main index"
    else:
        c = deb_cmp(inst, trixie)
        if c == 0:
            detail = (detail + "; " if detail else "") + f"= {suite} {trixie}"
        elif c < 0:
            status = "behind"
            detail = f"installed {inst} < {suite} {trixie}"
        else:
            status = "ahead"
            detail = f"installed {inst} > {suite} {trixie}"
            if testv and deb_cmp(inst, testv) == 0:
                detail += f" (={suite}-testing)"
            elif testv:
                detail += f" (testing has {testv})"
    core_flag = "1" if pkg in core else "0"
    print(f"{status}|{pkg}|{inst}|{trixie or '-'}|{roles}|{core_flag}|{detail}")
PY

ok_n=0; ahead_n=0; behind_n=0
while IFS='|' read -r status pkg inst trixie roles core detail; do
  [ -n "${status:-}" ] || continue
  label="pkg ${pkg} (${roles})"
  case "$status" in
    ok)
      ok "$label" "${inst}"
      ok_n=$((ok_n + 1))
      ;;
    ahead)
      info "$label" "$detail"
      ahead_n=$((ahead_n + 1))
      ;;
    behind)
      behind_n=$((behind_n + 1))
      if [ "$core" = "1" ] || [ "$BEHIND_MODE" = "error" ]; then
        fail "$label" "$detail"
      else
        warn "$label" "$detail"
      fi
      ;;
    warn)
      warn "$label" "$detail"
      ;;
    info)
      info "$label" "installed ${inst}${detail:+ — $detail}"
      ;;
    *)
      info "$label" "$status $detail"
      ;;
  esac
done <"$tmp/py.out"

# Core packages must exist somewhere
for need in taler-exchange libeufin-bank taler-merchant; do
  if grep -qE "^P\|[^|]+\|[^|]+\|${need}\|" "$tmp/installed.tsv"; then
    ok "core installed ${need}"
  else
    fail "core installed ${need}" "not present in any taler container"
  fi
done

info "suite" "deb.taler.net ${SUITE} (testing_index=${HAVE_TESTING})"
info "tally" "match=${ok_n} ahead=${ahead_n} behind=${behind_n}"

summary
