#!/usr/bin/env bash
# =============================================================================
#  graviton-check.sh — AWS Graviton (arm64) migration readiness check   v1.0
#
#  Will this x86 EC2 server (m5/m6i/m6a/…) run on Graviton (m7g/m8g/c7g/r7g)?
#
#  SAFE TO RUN ON PRODUCTION:
#   - Read-only. Installs nothing, changes nothing, restarts nothing.
#   - Sends no data anywhere. The only network calls are the local EC2
#     metadata endpoint and, with --online, public PyPI / Docker registry
#     lookups for package names (no data from your server is uploaded).
#   - Writes only a report folder in /tmp.
#
#  Usage:
#    curl -fsSL https://www.bluecurrentlabs.com/scripts/graviton-check.sh | sudo bash
#    curl -fsSL https://www.bluecurrentlabs.com/scripts/graviton-check.sh | sudo bash -s -- --online
#    sudo bash graviton-check.sh [--online] [--scan-dir /path] ...
#
#  Runs without sudo too, but can't see other users' files then.
#  Need help with the migration itself? cost@bluecurrentlabs.com
#  Verdicts: [OK] fine on arm64 | [WARN] needs a manual step | [BLOCK] must fix first
# =============================================================================
set -uo pipefail

ONLINE=0
SCAN_DIRS=(/opt /usr/local /srv /var/www /home /root)
while [[ $# -gt 0 ]]; do
  case "$1" in
    --online)   ONLINE=1 ;;
    --scan-dir) SCAN_DIRS+=("${2:?--scan-dir needs a path}"); shift ;;
    -h|--help)  sed -n '2,22p' "$0" 2>/dev/null || echo "See header of graviton-check.sh"; exit 0 ;;
    *) echo "Unknown option: $1 (try --help)"; exit 1 ;;
  esac; shift
done

OUT="/tmp/graviton-check_$(hostname -s)_$(date +%Y%m%d_%H%M%S)"
mkdir -p "$OUT" && chmod 700 "$OUT"
REPORT="$OUT/report.txt"; TMP="$OUT/.tmp"; mkdir -p "$TMP"
BLOCK=0; WARN=0
log()   { echo "$*" | tee -a "$REPORT"; }
ok()    { log "  [OK]    $*"; }
warn()  { log "  [WARN]  $*"; WARN=$((WARN+1)); }
block() { log "  [BLOCK] $*"; BLOCK=$((BLOCK+1)); }
hdr()   { log ""; log "== $* =="; }
have()  { command -v "$1" >/dev/null 2>&1; }
emit()  { while IFS='|' read -r lvl msg; do [[ -n "$lvl" ]] && "$lvl" "$msg"; done; }

log "Graviton readiness check v1.0 — $(date '+%Y-%m-%d %H:%M %z')"
[[ $EUID -ne 0 ]] && log "Note: running without root — results for other users' files may be incomplete."

# ----------------------------------------------------------------------------- 1. host
hdr "1. Host"
ARCH=$(uname -m)
TOKEN=$(curl -s -m 2 -X PUT http://169.254.169.254/latest/api/token -H "X-aws-ec2-metadata-token-ttl-seconds: 60" 2>/dev/null || true)
ITYPE=$(curl -s -m 2 -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-type 2>/dev/null || true)
[[ "$ITYPE" =~ ^[a-z0-9-]+\.[a-z0-9]+$ ]] || ITYPE="unknown (not EC2, or metadata blocked)"
. /etc/os-release
log "  Instance: $ITYPE | Arch: $ARCH | OS: $PRETTY_NAME | Kernel: $(uname -r)"
if [[ "$ARCH" == "aarch64" ]]; then
  ok "Already arm64 (Graviton). Moving between Graviton families (e.g. m6g -> m7g) needs no rebuild:"
  log "          stop instance -> change instance type -> start."
  rm -rf "$TMP"; exit 0
fi
case "$ID:${VERSION_ID%%.*}" in
  ubuntu:1[89]|ubuntu:2*|debian:1[0-9]|amzn:2|amzn:2023|rhel:[89]|rhel:1*|rocky:[89]|almalinux:[89])
     ok "$PRETTY_NAME has official arm64 AMIs." ;;
  centos:*|ubuntu:1[0-7]|debian:[0-9])
     block "$PRETTY_NAME is EOL or poorly supported on arm64 — plan an OS upgrade with the move." ;;
  *) warn "Confirm an arm64 AMI exists for $PRETTY_NAME." ;;
esac

# ----------------------------------------------------------------------------- 2. OS packages
hdr "2. OS packages"
if have dpkg-query; then
  PM=apt
  log "  $(dpkg-query -W -f='${Package}\n' | wc -l) packages installed, $(apt-mark showmanual 2>/dev/null | wc -l) installed manually (reinstalled on arm64 from repos)."
  dpkg --print-foreign-architectures | grep -q . && \
    warn "Foreign dpkg architectures enabled ($(dpkg --print-foreign-architectures | xargs)) — 32-bit x86 libraries don't exist on arm64."
  if ! ls /var/lib/apt/lists/*Release >/dev/null 2>&1; then
    warn "apt package lists are empty — run 'sudo apt-get update' and re-run for repository checks."
  else
    apt list --installed 2>/dev/null | grep -E '\[installed,local\]' | cut -d/ -f1 > "$TMP/local_deb"
    n=$(wc -l < "$TMP/local_deb")
    if   [[ $n -eq 0 ]];  then ok "All packages come from apt repositories."
    elif [[ $n -le 25 ]]; then while read -r p; do warn "'$p' was installed from a downloaded .deb — get its arm64 .deb."; done < "$TMP/local_deb"
    else warn "$n packages were installed from downloaded .debs (list in $OUT/local_debs.txt)."; cp "$TMP/local_deb" "$OUT/local_debs.txt"; fi
    for rel in /var/lib/apt/lists/*Release; do
      host=$(basename "$rel" | cut -d_ -f1)
      case "$host" in *ubuntu.com|*debian.org|*amazonaws.com|*ec2.archive*|*launchpadcontent.net|*ppa.launchpad.net) continue ;; esac
      archs=$(grep -m1 '^Architectures:' "$rel" | cut -d: -f2)
      if   [[ -z "$archs" ]]; then echo "warn|Repo $host: can't tell if it publishes arm64 — check manually."
      elif grep -qw arm64 <<<"$archs"; then echo "ok|Repo $host publishes arm64."
      else echo "block|Repo $host has NO arm64 packages — find another source for what you install from it."; fi
    done | sort -u | emit
  fi
elif have rpm; then
  PM=rpm
  log "  $(rpm -qa | wc -l) packages installed."
  warn "RPM-based system: confirm each enabled repo in /etc/yum.repos.d offers aarch64."
else
  PM=none; warn "No dpkg/rpm found — package checks skipped."
fi

# ----------------------------------------------------------------------------- 3. x86 binaries
hdr "3. x86-only programs outside the package manager"
log "  Scanning: ${SCAN_DIRS[*]}"
if have python3; then
python3 - "$TMP/x86.tsv" "${SCAN_DIRS[@]}" <<'PY'
import os, sys
out, dirs = sys.argv[1], sys.argv[2:]
SKIP = {'proc', 'sys', 'dev', '.git', '.cache', 'snap'}
rows = set()
for d in dirs:
    if not os.path.isdir(d): continue
    for root, subdirs, files in os.walk(d):
        subdirs[:] = [s for s in subdirs if s not in SKIP]
        for f in files:
            p = os.path.join(root, f)
            try:
                if os.path.islink(p) or os.path.getsize(p) < 64: continue
                with open(p, 'rb') as fh: h = fh.read(20)
            except Exception: continue
            if h[:4] != b'\x7fELF' or int.from_bytes(h[18:20], 'little') not in (0x3e, 0x03): continue
            if '/site-packages/' in p or '/dist-packages/' in p or '/venv/' in p or '/.venv/' in p: k = 'python'
            elif '/node_modules/' in p: k = 'node'
            elif '/vendor/' in p: k = 'php'
            else: k = 'standalone'
            rows.add((k, p))
with open(out, 'w') as fh:
    for k, p in sorted(rows): fh.write(f"{k}\t{p}\n")
PY
  touch "$TMP/x86.tsv"
  cnt() { grep -c "^$1	" "$TMP/x86.tsv" || true; }
  n=$(cnt python);     [[ $n -gt 0 ]] && warn "$n compiled Python files -> reinstall packages with pip on arm64 (section 4)."
  n=$(cnt node);       [[ $n -gt 0 ]] && warn "$n native Node.js modules -> run 'npm ci' on arm64."
  n=$(cnt php);        [[ $n -gt 0 ]] && warn "$n native files in PHP vendor/ -> run 'composer install' on arm64."
  n=$(cnt standalone)
  if [[ $n -gt 0 ]]; then
    block "$n x86 program files — each product below needs an arm64 version:"
    grep "^standalone	" "$TMP/x86.tsv" | cut -f2 | cut -d/ -f1-3 | sort | uniq -c | sort -rn | head -40 |
      while read -r c d; do log "            $(printf '%5s' "$c") files  $d"; done
    grep "^standalone	" "$TMP/x86.tsv" | cut -f2 > "$OUT/x86_programs.txt"
    log "            Full list: $OUT/x86_programs.txt"
  fi
  [[ -s "$TMP/x86.tsv" ]] || ok "No x86 program files found."
else
  warn "python3 not found — binary scan skipped."
fi
if have php && [[ "$PM" == apt ]]; then
  extdir=$(php -r 'echo ini_get("extension_dir");' 2>/dev/null)
  for so in "$extdir"/*.so; do
    [[ -f "$so" ]] || continue
    dpkg -S "$so" >/dev/null 2>&1 || warn "PHP extension $(basename "$so") was compiled/PECL-installed — reinstall it on arm64."
  done
fi

# ----------------------------------------------------------------------------- 4. runtimes
hdr "4. Language runtimes"
declare -A SEEN=()
: > "$TMP/pip_all"
for py in $( { ls /usr/bin/python3 /usr/local/bin/python3 2>/dev/null; find "${SCAN_DIRS[@]}" -maxdepth 6 -path '*/bin/python3' 2>/dev/null; } ); do
  [[ -x "$py" ]] || continue
  env=$(dirname "$(dirname "$py")"); [[ -n "${SEEN[$env]:-}" ]] && continue; SEEN[$env]=1
  ver=$("$py" -c 'import sys;print("%d.%d"%sys.version_info[:2])' 2>/dev/null) || continue
  if [[ "$env" != /usr && "$env" != /usr/local && ! -f "$env/pyvenv.cfg" ]]; then
    warn "Python bundled inside $env (not a venv) — install that product's arm64 build."; continue
  fi
  n=$("$py" -m pip freeze 2>/dev/null | tee -a "$TMP/pip_all" | wc -l)
  [[ $n -gt 0 ]] && log "  Python $ver at $env: $n packages"
done
if [[ $ONLINE -eq 1 && -s "$TMP/pip_all" ]]; then
  log "  Checking PyPI for arm64 builds…"
  grep '==' "$TMP/pip_all" | sort -u | python3 -c '
import sys, json, urllib.request
for line in sys.stdin:
    name, ver = line.strip().split("==", 1)
    try: d = json.load(urllib.request.urlopen(f"https://pypi.org/pypi/{name}/{ver}/json", timeout=10))
    except Exception: print(f"warn|{name}=={ver}: not found on PyPI (private package?) — check manually"); continue
    files = [u["filename"] for u in d.get("urls", [])]
    if any(f.endswith(".whl") and ("none-any" in f or "aarch64" in f) for f in files): continue
    if any(f.endswith((".tar.gz", ".zip")) for f in files):
        print(f"warn|{name}=={ver}: no arm64 wheel — compiles from source (needs build tools)")
    else: print(f"block|{name}=={ver}: no arm64 build and no source — upgrade this package")
' | emit
fi
have node     && log "  Node.js $(node -v)"
have php      && log "  PHP $(php -r 'echo PHP_VERSION;')"
have composer && log "  Composer present"
have java     && ok "Java found — runs as-is on an arm64 JDK (check JARs that bundle native .so libraries)."
have go       && log "  Go $(go version | awk '{print $3}') — rebuild Go binaries with GOARCH=arm64."

# ----------------------------------------------------------------------------- 5. containers
hdr "5. Docker"
if have docker && docker info >/dev/null 2>&1; then
  docker images --format '{{.Repository}}:{{.Tag}}' | grep -v '<none>' > "$TMP/images"
  log "  $(wc -l < "$TMP/images") local images."
  if [[ $ONLINE -eq 1 ]]; then
    while read -r img; do
      if docker manifest inspect "$img" 2>/dev/null | grep -q '"arm64"'; then echo "ok|$img has an arm64 version."
      else echo "warn|$img: no arm64 version found — rebuild with 'docker buildx build --platform linux/arm64'."; fi
    done < "$TMP/images" | emit
  else log "  Add --online to check images for arm64 versions."; fi
else log "  Docker not running — skipped."; fi

# ----------------------------------------------------------------------------- 6. services
hdr "6. Services and scheduled jobs"
if have systemctl; then
  log "  $(systemctl list-unit-files --type=service --state=enabled --no-legend 2>/dev/null | wc -l) enabled services."
  for u in /etc/systemd/system/*.service; do
    [[ -f "$u" ]] || continue
    exe=$(grep -m1 '^ExecStart=' "$u" | sed 's/^ExecStart=[-@+!:]*//' | awk '{print $1}')
    [[ -n "$exe" && -f "$exe" ]] || continue
    { dpkg -S "$exe" || rpm -qf "$exe"; } >/dev/null 2>&1 && continue   # from a package: reinstalled on arm64
    head -c 20 "$exe" | od -An -tx1 | tr -d ' \n' | grep -q '^7f454c46.\{28\}3e00' \
      && warn "Service $(basename "$u") runs an x86 program: $exe"
  done
fi
n=0; for u in $(cut -d: -f1 /etc/passwd); do crontab -l -u "$u" >/dev/null 2>&1 && n=$((n+1)); done
log "  $n users have crontabs (contents not read)."

# ----------------------------------------------------------------------------- verdict
rm -rf "$TMP"
hdr "Verdict"
if   [[ $BLOCK -gt 0 ]]; then log "  NOT READY — $BLOCK blocker(s), $WARN warning(s). Resolve the [BLOCK] items first."
elif [[ $WARN  -gt 0 ]]; then log "  FEASIBLE WITH MANUAL STEPS — $WARN item(s) to plan for."
else                          log "  READY — nothing architecture-specific found."; fi
log ""
log "  Report saved: $REPORT"
log "  Need help with the migration? cost@bluecurrentlabs.com"
