#!/usr/bin/env bash
# ka-build — render PKGBUILD from manifest.lock, build native on host,
# sign+publish via marfrit-publish-arch on hertz.
#
# Phase-1 (issue #34): arch makepkg wrapper. Debian path deferred.
#
# Usage:
#   ka-build <host>
#   ka-build <host> --packages-repo <path>    # default: ~/src/marfrit-packages
#   ka-build <host> --dry-run                 # stop after staging, don't makepkg
#   ka-build <host> --skip-publish            # build only, don't push to hertz
#
# Exit codes:
#   0  success (pkg built + published)
#   2  missing input (manifest.lock, PKGBUILD, ssh target)
#   3  patch drift (resolved.sha256 != PKGBUILD-side file sha256)
#   4  makepkg / sign / publish failure
#   5  manifest parse error

set -euo pipefail

VERSION=1

die() { echo "ka-build: error: $1" >&2; exit "${2:-1}"; }
note() { echo "ka-build: $1"; }

# Defaults
PACKAGES_REPO="${KA_PACKAGES_REPO:-${HOME}/src/marfrit-packages}"
DRY_RUN=0
SKIP_PUBLISH=0
HOST=""

while [ $# -gt 0 ]; do
    case "$1" in
        --packages-repo) PACKAGES_REPO="$2"; shift 2 ;;
        --dry-run) DRY_RUN=1; shift ;;
        --skip-publish) SKIP_PUBLISH=1; shift ;;
        --version) echo "ka-build version $VERSION"; exit 0 ;;
        -h|--help) sed -n '1,30p' "$0" | grep -E '^# ' | sed 's/^# //'; exit 0 ;;
        -*) die "unknown flag: $1" ;;
        *) [ -z "$HOST" ] && HOST="$1" || die "extra arg: $1"; shift ;;
    esac
done

[ -n "$HOST" ] || die "host is required" 2
[ -d "$PACKAGES_REPO" ] || die "--packages-repo not found: $PACKAGES_REPO" 2

# Locate kernel-agent repo root (where bin/ + fleet/ live)
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$script_dir/.." && pwd)"
[ -d "$REPO_ROOT/fleet" ] || die "fleet/ not found relative to $script_dir" 2

manifest="$REPO_ROOT/fleet/${HOST}.yaml"
[ -f "$manifest" ] || die "no manifest for host '$HOST': $manifest" 2

# Read fields from manifest via python (yaml in bash is masochism)
py_read() {
    python3 -c "
import sys, yaml, os
m = yaml.safe_load(open('$manifest'))
keys = '$1'.split('.')
v = m
for k in keys:
    if not isinstance(v, dict) or k not in v: sys.exit('missing key: $1')
    v = v[k]
print(v)
"
}

PKG_NAME="$(py_read package.name)"
BASELINE_REF="$(py_read baseline.ref)"
BUILD_HOST="$(py_read build_host.primary)"

# Locate the most recent ka-promote output
build_dir_root="${KA_BUILD_DIR:-$REPO_ROOT/build}"
promote_out="${build_dir_root}/${HOST}/${BASELINE_REF}"
lock="${promote_out}/manifest.lock"
cumulative="${promote_out}/cumulative.patch"
[ -f "$lock" ] || die "no manifest.lock at $lock — run 'ka-promote $HOST' first" 2
[ -f "$cumulative" ] || die "no cumulative.patch at $cumulative — run 'ka-promote $HOST' first" 2

# Locate the PKGBUILD
pkg_dir="${PACKAGES_REPO}/arch/${PKG_NAME}"
pkgbuild="${pkg_dir}/PKGBUILD"
[ -f "$pkgbuild" ] || die "no PKGBUILD at $pkgbuild (expected from manifest package.name)" 2

note "host=$HOST pkg=$PKG_NAME baseline=$BASELINE_REF build_host=$BUILD_HOST"
note "PKGBUILD: $pkgbuild"
note "manifest.lock: $lock"

# Refuse if PKGBUILD-side patches drifted from kernel-agent patches/.
# manifest.lock.resolved_patches[].sha256 must match PKGBUILD-dir-side
# files of the same basename. (If a patch is in resolved but missing from
# PKGBUILD dir, fail loud — operator needs to sync.)
note "verifying patch consistency between kernel-agent and marfrit-packages..."
drift=0
while IFS=$'\t' read -r basename expected_sha; do
    pkg_side="${pkg_dir}/${basename}"
    if [ ! -f "$pkg_side" ]; then
        echo "  MISSING in PKGBUILD dir: $basename" >&2
        drift=1; continue
    fi
    actual_sha=$(sha256sum "$pkg_side" | cut -d' ' -f1)
    if [ "$actual_sha" != "$expected_sha" ]; then
        echo "  DRIFT: $basename (expected $expected_sha, got $actual_sha)" >&2
        drift=1
    fi
done < <(python3 -c "
import yaml, sys, os
lk = yaml.safe_load(open('$lock'))
for r in lk['resolved_patches']:
    bn = os.path.basename(r['include'])
    print(f\"{bn}\t{r['sha256']}\")
")
[ "$drift" -eq 0 ] || die "patches differ between kernel-agent and marfrit-packages — sync first" 3
note "patches OK ($(python3 -c "import yaml; print(len(yaml.safe_load(open('$lock'))['resolved_patches']))") files)"

if [ "$DRY_RUN" -eq 1 ]; then
    note "--dry-run: stopping before makepkg"
    exit 0
fi

# Stage build dir on the build host via ssh
note "staging build on ${BUILD_HOST}..."
remote_stage="/tmp/ka-build-${HOST}-$$"
ssh "${BUILD_HOST}" "mkdir -p '$remote_stage'"
rsync -a "${pkg_dir}/" "${BUILD_HOST}:${remote_stage}/"

# Run makepkg natively
note "running makepkg --syncdeps --noconfirm --cleanbuild on ${BUILD_HOST}..."
ssh "${BUILD_HOST}" "cd '$remote_stage' && makepkg --syncdeps --noconfirm --cleanbuild --skipchecksums" \
    || die "makepkg failed on ${BUILD_HOST}" 4

# Fetch built packages
note "fetching .pkg.tar.zst from ${BUILD_HOST}..."
local_out="${promote_out}/pkgs"
mkdir -p "$local_out"
rsync -av "${BUILD_HOST}:${remote_stage}/*.pkg.tar.zst" "$local_out/" 2>&1 | tail -5

# Compute b2sums
pkg_b2sum_list=$(cd "$local_out" && for p in *.pkg.tar.zst; do
    [ -f "$p" ] || continue
    printf '%s  %s\n' "$(b2sum "$p" | cut -d' ' -f1)" "$p"
done)
note "built packages:"
echo "$pkg_b2sum_list" | sed 's/^/  /'

# Publish via hertz marfrit-publish-arch (unless --skip-publish)
if [ "$SKIP_PUBLISH" -eq 0 ]; then
    note "publishing to packages.reauktion.de/arch/aarch64/..."
    for p in "$local_out"/*.pkg.tar.zst; do
        [ -f "$p" ] || continue
        base="$(basename "$p")"
        scp -q "$p" "hertz:/tmp/${base}" || die "scp to hertz failed: $base" 4
        ssh hertz "sudo /opt/herding/bin/marfrit-publish-arch aarch64 '/tmp/${base}'" \
            || die "marfrit-publish-arch failed: $base" 4
        ssh hertz "rm -f '/tmp/${base}'"
        note "published: $base"
    done
fi

# Update manifest.lock with build receipt (append; don't rewrite the
# existing fields)
note "writing build receipt to manifest.lock..."
python3 - <<PY
import yaml, os, hashlib
from datetime import datetime, timezone

lock_path = "$lock"
out_dir = "$local_out"
build_host = "$BUILD_HOST"
skipped = $SKIP_PUBLISH

lk = yaml.safe_load(open(lock_path))
epoch = os.environ.get("SOURCE_DATE_EPOCH")
if epoch:
    built_at = datetime.fromtimestamp(int(epoch), tz=timezone.utc).isoformat()
else:
    built_at = datetime.now(tz=timezone.utc).isoformat()

pkgs = []
for fn in sorted(os.listdir(out_dir)):
    if not fn.endswith(".pkg.tar.zst"): continue
    fp = os.path.join(out_dir, fn)
    b2 = hashlib.blake2b(open(fp, "rb").read()).hexdigest()
    pkgs.append({"name": fn, "size": os.path.getsize(fp), "b2sum": b2})

lk["build"] = {
    "built_at": built_at,
    "built_on_host": build_host,
    "ka_build_version": $VERSION,
    "published": (not skipped),
    "packages": pkgs,
}
yaml.dump(lk, open(lock_path, "w"), sort_keys=True, default_flow_style=False)
print(f"  receipt: {len(pkgs)} package(s), built_at={built_at}, published={not skipped}")
PY

note "done."
