#!/usr/bin/env bash
# scripts/aur-local-test
#
# Build AUR packages from the local working tree in a clean extra chroot.
#
# Packages the current working tree (including uncommitted changes) into a
# tarball, temporarily patches each PKGBUILD to use it, runs
# extra-x86_64-build, then restores the PKGBUILD on exit regardless of
# success or failure.
#
# Packages with local AUR deps (e.g. owlry-rune depends on owlry-core) are
# built in topological order and their artifacts injected automatically.
#
# Usage: scripts/aur-local-test [OPTIONS] [PKG...]
# See --help for details.

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel)"
REPO_NAME="$(basename "$REPO_ROOT")"
AUR_DIR="$REPO_ROOT/aur"

# State tracked for cleanup
TMP_TARBALL=""
declare -a PKGBUILD_BACKUPS=()
declare -a PLACED_FILES=()

# Build config
RESET_CHROOT=0
declare -a INPUT_PKGS=()
declare -a EXTRA_INJECT=()  # --inject paths (external AUR deps)

# ─── Output helpers ──────────────────────────────────────────────────────────

die()  { echo "error: $*" >&2; exit 1; }
info() { printf '\033[1;34m==>\033[0m %s\n' "$*"; }
ok()   { printf '\033[1;32m  ->\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m  !\033[0m  %s\n' "$*" >&2; }
fail() { printf '\033[1;31mFAIL\033[0m %s\n' "$*" >&2; }

# ─── Cleanup ─────────────────────────────────────────────────────────────────

cleanup() {
    local code=$?
    local f pkgbuild

    # Remove tarballs placed in aur/ dirs
    for f in "${PLACED_FILES[@]+"${PLACED_FILES[@]}"}"; do
        [[ -f "$f" ]] && rm -f "$f"
    done

    # Restore patched PKGBUILDs from backups
    for f in "${PKGBUILD_BACKUPS[@]+"${PKGBUILD_BACKUPS[@]}"}"; do
        pkgbuild="${f%.bak}"
        [[ -f "$f" ]] && mv "$f" "$pkgbuild"
    done

    [[ -n "$TMP_TARBALL" && -f "$TMP_TARBALL" ]] && rm -f "$TMP_TARBALL"

    exit "$code"
}
trap cleanup EXIT INT TERM

# ─── Usage ───────────────────────────────────────────────────────────────────

usage() {
    cat >&2 <<EOF
Usage: $(basename "$0") [OPTIONS] [PKG...]

Build AUR packages from the local working tree in a clean chroot.
Packages current working tree (incl. uncommitted changes), patches PKGBUILD
source + checksum, runs extra-x86_64-build, then restores on exit.

Packages with local AUR deps are built in topological order and their
.pkg.tar.zst artifacts are injected into dependent builds automatically.

OPTIONS
  -c, --reset           Reset chroot matrix (passes -c to extra-x86_64-build).
                        Only applied to the first package; subsequent builds
                        reuse the already-fresh chroot.
  -a, --all             Build all packages in aur/ (respects dep order).
  -I, --inject FILE     Inject FILE (.pkg.tar.zst) into the chroot before
                        building. For AUR deps not in the official repos
                        (e.g. owlry-core when testing owlry-plugins).
                        Can be repeated.
  -h, --help            Show this help.

EXAMPLES
  # Single package
  $(basename "$0") owlry-core

  # Multiple packages with chroot reset
  $(basename "$0") -c owlry-core owlry-rune

  # All packages in dependency order
  $(basename "$0") --all --reset

  # owlry-plugins: inject owlry-core from sibling repo
  $(basename "$0") -I ../owlry/aur/owlry-core/owlry-core-*.pkg.tar.zst --all
EOF
    exit 1
}

# ─── Argument parsing ────────────────────────────────────────────────────────

while [[ $# -gt 0 ]]; do
    case "$1" in
        -c|--reset)
            RESET_CHROOT=1
            shift ;;
        -a|--all)
            for dir in "$AUR_DIR"/*/; do
                pkg=$(basename "$dir")
                [[ -f "$dir/PKGBUILD" ]] && INPUT_PKGS+=("$pkg")
            done
            shift ;;
        -I|--inject)
            [[ $# -ge 2 ]] || die "--inject requires an argument"
            [[ -f "$2" ]]  || die "inject file not found: $2"
            EXTRA_INJECT+=("$(realpath "$2")")
            shift 2 ;;
        -h|--help) usage ;;
        -*) die "unknown option: $1" ;;
        *)
            if [[ "$1" == *.pkg.tar.zst ]]; then
                [[ -f "$1" ]] || die "inject file not found: $1"
                EXTRA_INJECT+=("$(realpath "$1")")
            else
                INPUT_PKGS+=("$1")
            fi
            shift ;;
    esac
done

[[ ${#INPUT_PKGS[@]} -eq 0 ]] && usage

# ─── Inject deduplication ────────────────────────────────────────────────────

# Extract the package name from a .pkg.tar.zst filename.
# Arch package filenames follow: {pkgname}-{pkgver}-{pkgrel}-{arch}.pkg.tar.zst
# pkgver is guaranteed to have no dashes, so stripping the last three
# dash-separated segments leaves pkgname.
pkg_name_from_file() {
    local base
    base=$(basename "$1" .pkg.tar.zst)
    base="${base%-*}"   # strip arch
    base="${base%-*}"   # strip pkgrel
    base="${base%-*}"   # strip pkgver (no dashes in pkgver by Arch policy)
    echo "$base"
}

# Deduplicate a list of .pkg.tar.zst paths by package name.
# When the same package name appears more than once, keep the highest version
# (determined by sort -V on the filenames) and warn about the dropped ones.
dedup_inject_files() {
    [[ $# -eq 0 ]] && return 0
    local -A best=()
    local f name winner
    for f in "$@"; do
        name=$(pkg_name_from_file "$f")
        if [[ -v "best[$name]" ]]; then
            winner=$(printf '%s\n%s\n' "${best[$name]}" "$f" | sort -V | tail -1)
            if [[ "$winner" == "$f" ]]; then
                warn "Dropping duplicate inject (older): $(basename "${best[$name]}")"
                best[$name]="$f"
            else
                warn "Dropping duplicate inject (older): $(basename "$f")"
            fi
        else
            best[$name]="$f"
        fi
    done
    printf '%s\n' "${best[@]}"
}

# ─── Dependency resolution ───────────────────────────────────────────────────

# Return the names of local AUR packages that PKG depends on.
local_deps_of() {
    local pkg="$1"
    local pkgbuild="$AUR_DIR/$pkg/PKGBUILD"
    [[ -f "$pkgbuild" ]] || return 0

    local dep_line bare
    dep_line=$(grep '^depends=' "$pkgbuild" 2>/dev/null | head -1 || true)
    [[ -z "$dep_line" ]] && return 0

    # Strip depends=, parens, and quotes; split on whitespace
    echo "$dep_line" \
        | sed "s/^depends=//; s/[()\"']/ /g" \
        | tr ' ' '\n' \
        | while IFS= read -r dep; do
            [[ -z "$dep" ]] && continue
            bare="${dep%%[><=]*}"     # strip version constraints
            [[ -d "$AUR_DIR/$bare" ]] && echo "$bare"
          done
}

# Topological sort (DFS) — deps before dependents.
declare -A TOPO_VISITED=()
declare -a TOPO_ORDER=()

topo_visit() {
    local pkg="$1"
    [[ -v "TOPO_VISITED[$pkg]" ]] && return 0
    TOPO_VISITED[$pkg]=1
    local dep
    while IFS= read -r dep; do
        topo_visit "$dep"
    done < <(local_deps_of "$pkg")
    TOPO_ORDER+=("$pkg")
}

resolve_order() {
    TOPO_VISITED=()
    TOPO_ORDER=()
    local pkg
    for pkg in "$@"; do
        topo_visit "$pkg"
    done
}

# ─── Tarball creation ────────────────────────────────────────────────────────

make_tarball() {
    TMP_TARBALL=$(mktemp /tmp/aur-local-XXXXXX.tar.gz)
    info "Packaging ${REPO_NAME} working tree (incl. uncommitted changes)..."
    tar czf "$TMP_TARBALL" \
        --exclude='.git'    \
        --exclude='target'  \
        --transform "s|^\.|${REPO_NAME}|" \
        -C "$REPO_ROOT" .
    ok "Tarball ready: $(du -b "$TMP_TARBALL" | cut -f1 | numfmt --to=iec 2>/dev/null || wc -c < "$TMP_TARBALL") bytes"
}

# ─── PKGBUILD patching ───────────────────────────────────────────────────────

# Patch a package's PKGBUILD to use the local tarball.
# Backs up the original; cleanup() restores it on exit.
patch_pkgbuild() {
    local pkg="$1"
    local pkgbuild="$AUR_DIR/$pkg/PKGBUILD"
    local pkgdir="$AUR_DIR/$pkg"

    # Skip packages with no remote source (meta/group packages)
    if ! grep -q '^source=' "$pkgbuild" || grep -qE '^source=\(\s*\)' "$pkgbuild"; then
        ok "No source URL to patch — skipping tarball injection for $pkg"
        return 0
    fi

    local pkgname pkgver filename hash
    pkgname=$(grep '^pkgname=' "$pkgbuild" | cut -d= -f2- | tr -d "\"'")
    pkgver=$(grep  '^pkgver=' "$pkgbuild"  | cut -d= -f2- | tr -d "\"'")
    filename="${pkgname}-${pkgver}.tar.gz"
    hash=$(b2sum "$TMP_TARBALL" | cut -d' ' -f1)

    # Backup original PKGBUILD
    cp "$pkgbuild" "${pkgbuild}.bak"
    PKGBUILD_BACKUPS+=("${pkgbuild}.bak")

    # Place local tarball where makepkg looks for it
    cp "$TMP_TARBALL" "$pkgdir/$filename"
    PLACED_FILES+=("$pkgdir/$filename")

    # Patch source and checksum lines in-place
    sed -i "s|^source=.*|source=(\"${filename}\")|" "$pkgbuild"
    sed -i "s|^b2sums=.*|b2sums=('${hash}')|"       "$pkgbuild"

    ok "Patched PKGBUILD: source=${filename}, b2sum=${hash:0:12}…"
}

# ─── Build ───────────────────────────────────────────────────────────────────

# built_artifacts[pkg] = path to the .pkg.tar.zst produced in this run.
# Used to inject pkg artifacts into dependent builds.
declare -A BUILT_ARTIFACTS=()

find_artifact() {
    local pkg="$1"
    local pkgver
    # pkgver is the same in patched and original PKGBUILD
    pkgver=$(grep '^pkgver=' "$AUR_DIR/$pkg/PKGBUILD" | cut -d= -f2- | tr -d "\"'" \
             || grep '^pkgver=' "$AUR_DIR/$pkg/PKGBUILD.bak" | cut -d= -f2- | tr -d "\"'")
    ls "$AUR_DIR/$pkg/${pkg}-${pkgver}-"*".pkg.tar.zst" 2>/dev/null \
        | grep -v -- '-debug-' | sort -V | tail -1 || true
}

build_one() {
    local pkg="$1"
    local pkgdir="$AUR_DIR/$pkg"

    info "[$pkg] Patching PKGBUILD..."
    patch_pkgbuild "$pkg"

    # Collect inject args: extra (external) + artifacts of local deps built earlier
    local inject=()
    for f in "${EXTRA_INJECT[@]+"${EXTRA_INJECT[@]}"}"; do
        inject+=("-I" "$f")
    done
    while IFS= read -r dep; do
        if [[ -v "BUILT_ARTIFACTS[$dep]" ]]; then
            inject+=("-I" "${BUILT_ARTIFACTS[$dep]}")
        else
            warn "$pkg depends on $dep (local AUR) which was not built in this run"
            warn "  → Build $dep first or pass: -I path/to/${dep}-*.pkg.tar.zst"
        fi
    done < <(local_deps_of "$pkg")

    # Build args: -c only on the first package, then cleared
    local build_args=()
    if [[ $RESET_CHROOT -eq 1 ]]; then
        build_args+=("-c")
        RESET_CHROOT=0
    fi

    info "[$pkg] Running extra-x86_64-build..."
    (
        cd "$pkgdir"
        if [[ ${#inject[@]} -gt 0 ]]; then
            extra-x86_64-build "${build_args[@]+"${build_args[@]}"}" -- "${inject[@]}"
        else
            extra-x86_64-build "${build_args[@]+"${build_args[@]}"}"
        fi
    )

    # Record artifact for potential injection into dependents
    local artifact
    artifact=$(find_artifact "$pkg")
    if [[ -n "$artifact" ]]; then
        BUILT_ARTIFACTS[$pkg]="$artifact"
        ok "[$pkg] artifact: $(basename "$artifact")"
    fi
}

# ─── Main ────────────────────────────────────────────────────────────────────

# Deduplicate external inject files by package name (keep highest version)
if [[ ${#EXTRA_INJECT[@]} -gt 1 ]]; then
    mapfile -t EXTRA_INJECT < <(dedup_inject_files "${EXTRA_INJECT[@]}")
fi

# Validate all requested packages exist
for pkg in "${INPUT_PKGS[@]}"; do
    [[ -d "$AUR_DIR/$pkg" && -f "$AUR_DIR/$pkg/PKGBUILD" ]] \
        || die "package not found: aur/$pkg/PKGBUILD"
done

# Sort into build order (deps before dependents)
resolve_order "${INPUT_PKGS[@]}"

# Create one tarball, reused for all packages in this run
make_tarball

declare -a FAILED=()

for pkg in "${TOPO_ORDER[@]}"; do
    echo ""
    if build_one "$pkg"; then
        :
    else
        fail "[$pkg]"
        FAILED+=("$pkg")
    fi
done

echo ""
if [[ ${#FAILED[@]} -gt 0 ]]; then
    fail "packages failed: ${FAILED[*]}"
    exit 1
fi

info "All packages built successfully!"
