#!/usr/bin/env bash
# 00-download.sh — run on the CONNECTED builder only.
#
# Downloads everything the air-gapped RHEL 9.6 environment needs to run
# OWASP ZAP as the DAST stage of the Drone pipeline:
#   images/    ZAP 2.17.0 container image (pinned by digest, docker-archive)
#   downloads/ ZAP 2.17.0 Linux package, Temurin 21 JRE, add-on catalogue
#   downloads/addons/  every add-on published for ZAP 2.17 (SHA-256 verified)
#   metadata/  provenance: digests, catalogue, versions
#
# Every artefact is checked against a published SHA-256 before it is kept.
# Re-running is safe: verified files are not downloaded again.
set -euo pipefail

ROOT="$(cd "$(dirname "$0")/.." && pwd)"
source "$ROOT/config/versions.env"

log() { printf '[%s] %s\n' "$(date -u +%H:%M:%SZ)" "$*"; }
die() { printf 'ERROR: %s\n' "$*" >&2; exit 1; }

for tool in curl sha256sum python3 skopeo; do
  command -v "$tool" >/dev/null || die "$tool is required on the connected builder"
done

mkdir -p "$ROOT/images" "$ROOT/downloads/addons" "$ROOT/metadata"

# fetch URL DEST SHA256 — download to a temp name, verify, then move into place.
fetch() {
  local url="$1" dest="$2" sha="$3"
  if [[ -f "$dest" ]] && echo "$sha  $dest" | sha256sum --check --status; then
    log "ok (cached) $(basename "$dest")"; return 0
  fi
  log "downloading $(basename "$dest")"
  curl -fL --retry 5 --retry-delay 3 -o "$dest.part" "$url"
  echo "$sha  $dest.part" | sha256sum --check --status \
    || { rm -f "$dest.part"; die "checksum mismatch for $url"; }
  mv "$dest.part" "$dest"
}

# ---------------------------------------------------------------------------
# 1. Container image, pinned to the amd64 manifest digest.
# ---------------------------------------------------------------------------
IMAGE_TAR="$ROOT/images/zaproxy-${ZAP_VERSION}-amd64.tar"
log "resolving $ZAP_IMAGE_REPO@$ZAP_IMAGE_INDEX_DIGEST (linux/amd64)"
amd64_digest="$(skopeo inspect --raw "docker://$ZAP_IMAGE_REPO@$ZAP_IMAGE_INDEX_DIGEST" \
  | python3 -c 'import json,sys
d=json.load(sys.stdin)
print(next(m["digest"] for m in d["manifests"] if m["platform"]=={"architecture":"amd64","os":"linux"}))')"
[[ "$amd64_digest" == "$ZAP_IMAGE_AMD64_DIGEST" ]] \
  || die "upstream index now points amd64 at $amd64_digest (expected $ZAP_IMAGE_AMD64_DIGEST) — review before updating config/versions.env"

if [[ -s "$IMAGE_TAR" && -s "$ROOT/metadata/zap-image.sha256" ]] \
   && sha256sum --check --status "$ROOT/metadata/zap-image.sha256" 2>/dev/null; then
  log "ok (cached) $(basename "$IMAGE_TAR")"
else
  rm -f "$IMAGE_TAR"
  log "copying image to $(basename "$IMAGE_TAR") (about 2.4 GB)"
  skopeo copy --override-arch amd64 --override-os linux \
    "docker://$ZAP_IMAGE_REPO@$ZAP_IMAGE_AMD64_DIGEST" \
    "docker-archive:$IMAGE_TAR:$ZAP_IMAGE_LOCAL"
  (cd "$ROOT" && sha256sum "images/$(basename "$IMAGE_TAR")") > "$ROOT/metadata/zap-image.sha256"
fi
skopeo inspect "docker://$ZAP_IMAGE_REPO@$ZAP_IMAGE_AMD64_DIGEST" > "$ROOT/metadata/zap-image-inspect.json"
skopeo inspect --config "docker://$ZAP_IMAGE_REPO@$ZAP_IMAGE_AMD64_DIGEST" > "$ROOT/metadata/zap-image-config.json"

# ---------------------------------------------------------------------------
# 2. ZAP Linux package (for hosts that cannot run containers) + JRE.
# ---------------------------------------------------------------------------
fetch "https://github.com/zaproxy/zaproxy/releases/download/v${ZAP_VERSION}/ZAP_${ZAP_VERSION}_Linux.tar.gz" \
      "$ROOT/downloads/ZAP_${ZAP_VERSION}_Linux.tar.gz" "$ZAP_LINUX_SHA256"
fetch "$JRE_URL" "$ROOT/downloads/$JRE_FILE" "$JRE_SHA256"

# ---------------------------------------------------------------------------
# 3. Add-on catalogue for this ZAP line and every add-on in it.
#    ZapVersions-<major.minor>.xml carries the SHA-256 for each add-on.
# ---------------------------------------------------------------------------
catalogue="$ROOT/downloads/ZapVersions-${ZAP_VERSION%.*}.xml"
log "downloading add-on catalogue $(basename "$catalogue")"
curl -fsSL --retry 5 -o "$catalogue.part" \
  "https://raw.githubusercontent.com/zaproxy/zap-admin/master/ZapVersions-${ZAP_VERSION%.*}.xml"
mv "$catalogue.part" "$catalogue"

python3 - "$catalogue" "$ROOT/downloads/addons" "$ROOT/metadata/addons.tsv" <<'PY'
import hashlib, os, sys, time, urllib.request
import xml.etree.ElementTree as ET

catalogue, outdir, manifest = sys.argv[1:4]
rows = []
for el in ET.parse(catalogue).getroot():
    if not el.tag.startswith("addon_"):
        continue
    h = el.findtext("hash")
    algo, _, digest = h.partition(":")
    # The catalogue publishes SHA-256 for almost every add-on; a small number of
    # older, rarely-updated add-ons still carry a SHA1 hash. Verify each against
    # whichever algorithm upstream declares for it.
    algo = algo.upper().replace("-", "").lower()   # "SHA-256"->"sha256", "SHA1"->"sha1"
    assert algo in ("sha256", "sha1"), h
    rows.append((el.tag[6:], el.findtext("status"), el.findtext("version"),
                 el.findtext("file"), el.findtext("url"), algo, digest.lower()))

def digest_of(path, algo):
    s = hashlib.new(algo)
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(1 << 20), b""):
            s.update(chunk)
    return s.hexdigest()

wanted = {r[3] for r in rows}
for addon_id, status, version, fname, url, algo, digest in rows:
    dest = os.path.join(outdir, fname)
    if os.path.exists(dest) and digest_of(dest, algo) == digest:
        continue
    for attempt in range(5):
        try:
            with urllib.request.urlopen(url, timeout=120) as r, open(dest + ".part", "wb") as f:
                while chunk := r.read(1 << 20):
                    f.write(chunk)
            break
        except OSError as e:
            print(f"  retry {fname}: {e}", file=sys.stderr)
            time.sleep(3)
    else:
        sys.exit(f"failed to download {url}")
    if digest_of(dest + ".part", algo) != digest:
        os.remove(dest + ".part")
        sys.exit(f"checksum mismatch for {fname}")
    os.replace(dest + ".part", dest)
    print(f"  ok {fname}")

# Remove add-on versions no longer in the catalogue so the bundle stays exact.
for f in os.listdir(outdir):
    if f.endswith(".zap") and f not in wanted:
        os.remove(os.path.join(outdir, f))
        print(f"  removed stale {f}")

with open(manifest, "w") as m:
    m.write("id\tstatus\tversion\tfile\talgo\tdigest\n")
    for addon_id, status, version, fname, url, algo, digest in sorted(rows):
        m.write(f"{addon_id}\t{status}\t{version}\t{fname}\t{algo}\t{digest}\n")
print(f"{len(rows)} add-ons verified")
PY

# ---------------------------------------------------------------------------
# 4. Provenance record.
# ---------------------------------------------------------------------------
cat > "$ROOT/metadata/download-provenance.txt" <<EOF
downloaded_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)
builder=$(hostname)
zap_version=$ZAP_VERSION
zap_image=$ZAP_IMAGE_REPO@$ZAP_IMAGE_INDEX_DIGEST
zap_image_amd64=$ZAP_IMAGE_REPO@$ZAP_IMAGE_AMD64_DIGEST
zap_image_local_tag=$ZAP_IMAGE_LOCAL
zap_linux_sha256=$ZAP_LINUX_SHA256
jre=$JRE_FILE sha256=$JRE_SHA256
addon_catalogue_sha256=$(sha256sum "$catalogue" | cut -d' ' -f1)
addons=$(($(wc -l < "$ROOT/metadata/addons.tsv") - 1))
EOF
log "download complete — next: scripts/99-package.sh"
