#!/usr/bin/env bash
#
# ticrypt-snapshot.sh - collect raw tiCrypt REST API responses.
#
# Fetches every documented read route and writes the unmodified JSON, one file
# per route. It does NOT reproduce the 15 management-interface exports: that
# needs the derived columns (quota percentages, per-team and per-project
# rollups, rank derivation) which live in ticrypt-snapshot.py.
#
# Requires curl. jq is optional and used only to pretty-print and validate.
#
#   export TICRYPT_BASE_URL=https://ticrypt.yourinstitution.edu
#   export TICRYPT_TOKEN=KEY
#   ./ticrypt-snapshot.sh
#
# Output files are named the way ticrypt-snapshot.py --replay expects, so the
# two compose: collect here, normalize anywhere.
#
#   ./ticrypt-snapshot.sh                                    # restricted host
#   python3 ticrypt-snapshot.py --replay ticrypt-snapshot-raw-<date>
#
# Create the API key in the tiCrypt front end under Management -> Miscellaneous
# -> API Keys, and mark it read-only: this script only reads.
#
# As of backend 3.14.1 a key carries several types at once. Select every type
# (Drive, VM, Team, Project, User) and one key covers every route called here.
# On earlier versions a key carries exactly one type, so pass five.
#
# Reference: https://ticrypt.com/docs/admin-guide/operations/rest-api

set -euo pipefail

VERSION="1.0.0"
PROG="$(basename "$0")"

# Tokens are read from the environment in preference to the command line so
# they stay out of shell history and the process table.
BASE_URL="${TICRYPT_BASE_URL:-}"
TOKEN_ALL="${TICRYPT_TOKEN:-}"
TOKEN_USER="${TICRYPT_TOKEN_USER:-}"
TOKEN_TEAM="${TICRYPT_TOKEN_TEAM:-}"
TOKEN_VM="${TICRYPT_TOKEN_VM:-}"
TOKEN_DRIVE="${TICRYPT_TOKEN_DRIVE:-}"
TOKEN_PROJECT="${TICRYPT_TOKEN_PROJECT:-}"

REALMS="libvirt"
OUT_DIR=""
TIMEOUT=30

usage() {
    cat <<EOF
$PROG $VERSION - collect raw tiCrypt REST API responses

Usage: $PROG [options]

  --base-url URL       backend domain, e.g. https://ticrypt.yourinstitution.edu
  --token KEY          API key with every type selected (needs backend 3.14.1+)
  --token-user KEY     API key of type User
  --token-team KEY     API key of type Team
  --token-vm KEY       API key of type VM
  --token-drive KEY    API key of type Drive
  --token-project KEY  API key of type Project
  --realms LIST        comma-separated realm IDs (default: libvirt)
  --out-dir DIR        output directory (default: ./ticrypt-snapshot-raw-<date>)
  --timeout SECONDS    per-request timeout (default: 30)
  --version            print version and exit
  -h, --help           print this help and exit

Environment: TICRYPT_BASE_URL, TICRYPT_TOKEN, TICRYPT_TOKEN_<TYPE>.
These are read in preference to the matching flags.

TLS verification is always on and cannot be disabled.

This collects raw API responses. To reproduce the management-interface
exports, use ticrypt-snapshot.py.
EOF
}

while [ $# -gt 0 ]; do
    case "$1" in
        --base-url)      BASE_URL="${BASE_URL:-$2}"; shift 2 ;;
        --token)         TOKEN_ALL="${TOKEN_ALL:-$2}"; shift 2 ;;
        --token-user)    TOKEN_USER="${TOKEN_USER:-$2}"; shift 2 ;;
        --token-team)    TOKEN_TEAM="${TOKEN_TEAM:-$2}"; shift 2 ;;
        --token-vm)      TOKEN_VM="${TOKEN_VM:-$2}"; shift 2 ;;
        --token-drive)   TOKEN_DRIVE="${TOKEN_DRIVE:-$2}"; shift 2 ;;
        --token-project) TOKEN_PROJECT="${TOKEN_PROJECT:-$2}"; shift 2 ;;
        --realms)        REALMS="$2"; shift 2 ;;
        --out-dir)       OUT_DIR="$2"; shift 2 ;;
        --timeout)       TIMEOUT="$2"; shift 2 ;;
        --version)       echo "$PROG $VERSION"; exit 0 ;;
        -h|--help)       usage; exit 0 ;;
        *) echo "error: unknown option $1" >&2; echo "try $PROG --help" >&2; exit 2 ;;
    esac
done

command -v curl >/dev/null 2>&1 || {
    echo "error: curl is required but not installed." >&2
    exit 2
}

HAVE_JQ=0
if command -v jq >/dev/null 2>&1; then
    HAVE_JQ=1
fi

if [ -z "$BASE_URL" ]; then
    echo "error: --base-url is required (or set TICRYPT_BASE_URL)" >&2
    exit 2
fi
BASE_URL="${BASE_URL%/}"

if [ -z "$TOKEN_ALL$TOKEN_USER$TOKEN_TEAM$TOKEN_VM$TOKEN_DRIVE$TOKEN_PROJECT" ]; then
    cat >&2 <<'EOF'
error: no API key given. Pass --token with a key carrying every type
       (backend 3.14.1+), or one --token-<type> per scope. Keys are
       created in the tiCrypt front end under Management ->
       Miscellaneous -> API Keys.
EOF
    exit 2
fi

# A single all-types key is preferred; otherwise fall back per scope.
token_for() {
    if [ -n "$TOKEN_ALL" ]; then printf '%s' "$TOKEN_ALL"; return 0; fi
    case "$1" in
        user)    printf '%s' "$TOKEN_USER" ;;
        team)    printf '%s' "$TOKEN_TEAM" ;;
        vm)      printf '%s' "$TOKEN_VM" ;;
        drive)   printf '%s' "$TOKEN_DRIVE" ;;
        project) printf '%s' "$TOKEN_PROJECT" ;;
    esac
}

started="$(date +%Y-%m-%d)"
[ -n "$OUT_DIR" ] || OUT_DIR="ticrypt-snapshot-raw-${started}"
mkdir -p "$OUT_DIR"

# Match ticrypt-snapshot.py's replay naming: strip the leading slash, replace
# "/" with "-", append sorted query values, then anything not alphanumeric or
# -_ becomes _.  /api/vms/images + realm=libvirt -> api-vms-images-libvirt.json
slug() {
    local path="$1" extra="${2:-}" stem
    stem="${path#/}"
    stem="${stem%/}"
    stem="${stem//\//-}"
    [ -n "$extra" ] && stem="${stem}-${extra}"
    printf '%s.json' "$(printf '%s' "$stem" | sed 's/[^A-Za-z0-9_-]/_/g')"
}

MANIFEST_ROWS=()
FETCHED=0
SKIPPED=0

json_escape() {
    printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'
}

# fetch <path> <scope> [query] [optional]
# "optional" marks the routes added in 3.14.1, which 404 until the backend
# ships them. Those are recorded and skipped rather than being fatal.
fetch() {
    local path="$1" scope="$2" query="${3:-}" optional="${4:-0}"
    local token url out status body note

    token="$(token_for "$scope")"
    if [ -z "$token" ]; then
        echo "  ! skip $path - no token for $scope routes" >&2
        MANIFEST_ROWS+=("$(printf '{"route":"%s","scope":"%s","status":0,"bytes":0,"skipped":"no token for %s routes"}' \
            "$(json_escape "$path")" "$scope" "$scope")")
        SKIPPED=$((SKIPPED + 1))
        return 0
    fi

    url="${BASE_URL}${path}?"
    [ -n "$query" ] && url="${url}${query}&"
    url="${url}token=${token}"

    out="${OUT_DIR}/$(slug "$path" "${query#*=}")"
    body="$(mktemp)"

    # curl prints 000 through --write-out when it cannot connect, and exits
    # non-zero; || true keeps that from aborting under set -e without
    # appending a second status.
    status="$(curl --silent --show-error --location \
        --max-time "$TIMEOUT" \
        --header 'Accept: application/json' \
        --output "$body" \
        --write-out '%{http_code}' \
        "$url" 2>/dev/null || true)"
    [ -n "$status" ] || status="000"

    case "$status" in
        200)
            if [ "$HAVE_JQ" -eq 1 ]; then
                if jq '.' "$body" > "$out" 2>/dev/null; then
                    :
                else
                    echo "  ! $path returned 200 but not valid JSON" >&2
                    mv "$body" "$out"
                fi
            else
                mv "$body" "$out"
            fi
            note="$(wc -c < "$out" | tr -d ' ')"
            echo "  wrote $(basename "$out") (${note} bytes)" >&2
            MANIFEST_ROWS+=("$(printf '{"route":"%s","scope":"%s","status":200,"bytes":%s,"file":"%s"}' \
                "$(json_escape "$path")" "$scope" "$note" "$(json_escape "$(basename "$out")")")")
            FETCHED=$((FETCHED + 1))
            ;;
        404)
            if [ "$optional" -eq 1 ]; then
                echo "  - skip $path - not served by this backend (added in 3.14.1)" >&2
                note="route added in 3.14.1; this backend does not serve it yet"
            else
                echo "  ! skip $path - 404" >&2
                note="404"
            fi
            MANIFEST_ROWS+=("$(printf '{"route":"%s","scope":"%s","status":404,"bytes":0,"skipped":"%s"}' \
                "$(json_escape "$path")" "$scope" "$(json_escape "$note")")")
            SKIPPED=$((SKIPPED + 1))
            ;;
        401|403)
            echo "  ! skip $path - $status. The key is missing, expired, or not of a type" >&2
            echo "    that grants $scope routes. Check the key type in Management ->" >&2
            echo "    Miscellaneous -> API Keys." >&2
            MANIFEST_ROWS+=("$(printf '{"route":"%s","scope":"%s","status":%s,"bytes":0,"skipped":"key does not grant %s routes"}' \
                "$(json_escape "$path")" "$scope" "$status" "$scope")")
            SKIPPED=$((SKIPPED + 1))
            ;;
        000)
            echo "  ! skip $path - cannot reach ${BASE_URL}. Check the backend domain" >&2
            echo "    and that its TLS certificate is trusted by this host." >&2
            MANIFEST_ROWS+=("$(printf '{"route":"%s","scope":"%s","status":0,"bytes":0,"skipped":"connection failed"}' \
                "$(json_escape "$path")" "$scope")")
            SKIPPED=$((SKIPPED + 1))
            ;;
        *)
            echo "  ! skip $path - HTTP $status" >&2
            MANIFEST_ROWS+=("$(printf '{"route":"%s","scope":"%s","status":%s,"bytes":0,"skipped":"HTTP %s"}' \
                "$(json_escape "$path")" "$scope" "$status" "$status")")
            SKIPPED=$((SKIPPED + 1))
            ;;
    esac

    rm -f "$body"
}

echo "tiCrypt raw collection $VERSION" >&2
[ "$HAVE_JQ" -eq 0 ] && echo "  (jq not found - writing responses unformatted)" >&2

# --- Routes available today -------------------------------------------------
fetch /api/users    user
fetch /api/teams    team
fetch /api/drives   drive
fetch /api/projects project

IFS=',' read -ra REALM_LIST <<< "$REALMS"
for realm in "${REALM_LIST[@]}"; do
    realm="$(printf '%s' "$realm" | tr -d '[:space:]')"
    [ -n "$realm" ] || continue
    fetch "/api/vms/${realm}/active"  vm
    fetch "/api/vms/${realm}/configs" vm
    fetch /api/vms/images             vm "realm=${realm}"
done

# --- Routes added in 3.14.1 -------------------------------------------------
# These 404 until the backend ships them. Recorded, never fatal.
fetch /api/realms                 vm      "" 1
fetch /api/vms/bricks             vm      "" 1
fetch /api/external-servers       vm      "" 1
fetch /api/teams/usage            team    "" 1
fetch /api/projects/memberships   project "" 1
fetch /api/projects/usage         project "" 1
fetch /api/security-requirements  project "" 1
fetch /api/security-levels        project "" 1
fetch /api/users/certifications   user    "" 1
fetch /api/users/managed-objects  user    "" 1

# --- Manifest ---------------------------------------------------------------
if [ -n "$TOKEN_ALL" ]; then
    AUTH_MODE="single key, all types"
else
    AUTH_MODE="one key per type"
fi

{
    printf '{\n'
    printf '  "tool": "ticrypt-snapshot.sh",\n'
    printf '  "version": "%s",\n' "$VERSION"
    printf '  "captured": "%s",\n' "$(date +%Y-%m-%dT%H:%M:%S)"
    printf '  "baseUrl": "%s",\n' "$(json_escape "$BASE_URL")"
    printf '  "realms": "%s",\n' "$(json_escape "$REALMS")"
    printf '  "authMode": "%s",\n' "$AUTH_MODE"
    printf '  "fetched": %s,\n' "$FETCHED"
    printf '  "skipped": %s,\n' "$SKIPPED"
    printf '  "routes": [\n'
    for i in "${!MANIFEST_ROWS[@]}"; do
        printf '    %s' "${MANIFEST_ROWS[$i]}"
        [ "$i" -lt $(( ${#MANIFEST_ROWS[@]} - 1 )) ] && printf ','
        printf '\n'
    done
    printf '  ]\n'
    printf '}\n'
} > "${OUT_DIR}/_collection-manifest.json"

echo "" >&2
echo "Collected $FETCHED route(s) into $(cd "$OUT_DIR" && pwd)" >&2
[ "$SKIPPED" -gt 0 ] && echo "$SKIPPED route(s) skipped - see _collection-manifest.json" >&2

cat >&2 <<EOF

These are raw API responses, not the management-interface exports. To build
those, pass this directory to the Python script:

  python3 ticrypt-snapshot.py --replay $OUT_DIR
EOF
