#!/bin/sh
# USM system-package-manager helper for apk-based systems (Alpine).
#
# Implements the USM SPM contract (see usm/README.md, "System package
# manager integration") for Alpine-style systems:
#
#     usm-spm-apk query <usm-ref>...       -> contract JSON on STDOUT
#     usm-spm-apk install <native-name>... -> contract JSONL events on STDOUT
#
# API choice: pure POSIX shell (busybox ash) on purpose -- the Alpine
# base image ships neither python3 nor jq, and the SPM bootstrap must
# stay at zero extra packages (busybox is already there). apk-tools
# >= 2.10 (alpine:latest currently ships apk-tools 3.0.6) expose
# provides namespaces that map USM refs natively, so file-list indexes
# are not needed for the hot types:
#
#     bin:/sbin: X  ->  provides "cmd:X"   (abuild auto-generates for
#                       every packaged executable, /usr/bin and /sbin)
#     lib: X       ->  provides "so:X"    (verbatim soname)
#     pc: X        ->  provides "pc:${X%.pc}" -- apk indexes pkg-config
#                      module names WITHOUT the ".pc" suffix (observed:
#                      glib-dev provides pc:glib-2.0, vala provides
#                      pc:libvala-0.56), so one trailing ".pc" is
#                      stripped from the ref before querying
#
# Providers are found with `apk search -x <provide>` (exact match that
# also matches provides); the from-scratch solo-install closure comes
# from `apk search --recursive <name>` (runs the solver against the
# repositories only -- same semantics as the dnf helper's repo-only
# sack); installed-dependency-count is the overlap of that closure with
# the installed database (`apk info`). Residual resource types (inc:,
# vapi:, gir:, typelib:, gio:, res:, cfg:, man:, info:, locale:,
# libexec:, rootpath:, tag:) are resolved against the installed database only via
# `apk info -W`/directory sampling: APKINDEX carries no file lists, so
# files of not-yet-installed packages are unresolvable and land in
# "not-found" (documented limitation, the same offline stance as the
# emerge helper).
#
# apk has no per-download progress output on a non-tty stdout; it
# prints one "(k/N) Installing pkg (ver)" line per package instead, so
# "package" events are emitted at package-completion granularity
# (progress 1.0) driven by those lines, and begin.total comes from an
# `apk add --simulate` pass against the current system.
#
# Install transactions expand INSTALL_COMPANIONS (currently gcc ->
# musl-dev): apk's gcc package deliberately ships no crt objects, and a
# USM manifest cannot express them as resource refs.
#
# The query subcommand never modifies system state: repository indexes
# are fetched into a throwaway cache directory (at most refreshing
# package-metadata caches). The install subcommand must run as root;
# apk never prompts interactively: unresolved dependencies or bad
# signatures fail instead.
#
# Alpine path table (musl/busybox layout, no lib64): /usr/bin, /sbin,
# /lib, /usr/lib, /usr/include, /usr/share (+/usr/share/pkgconfig).

set -eu

export LC_ALL=C

PROG=usm-spm-apk

EXIT_OK=0
EXIT_FAILURE=1
EXIT_USAGE=2
EXIT_RESOLVE=3
EXIT_DOWNLOAD=4
EXIT_TRANSACTION=5

WORK=""

cleanup() {
    if [ -n "$WORK" ]; then
        rm -rf "$WORK"
    fi
}

warn() {
    printf '%s: %s\n' "$PROG" "$*" >&2
}

die_usage() {
    printf 'usage: %s query <usm-ref>...\n' "$PROG" >&2
    printf '       %s plan <native-name>...\n' "$PROG" >&2
    printf '       %s install <native-name>...\n' "$PROG" >&2
    exit "$EXIT_USAGE"
}

emit() {
    printf '%s\n' "$1"
}

json_escape() {
    printf '%s' "$1" | awk '
        {
            s = $0
            gsub(/\\/, "\\\\", s)
            gsub(/"/, "\\\"", s)
            gsub(/\t/, "\\t", s)
            gsub(/[[:cntrl:]]/, "", s)
            print s
        }'
}

mkwork() {
    WORK=$(mktemp -d "${TMPDIR:-/tmp}/usm-spm-apk.XXXXXX" 2>/dev/null) || {
        WORK="${TMPDIR:-/tmp}/usm-spm-apk.$$"
        (umask 077 && mkdir "$WORK")
    }
    trap cleanup EXIT
}

prime_index_cache() {
    CACHE="$WORK/cache"
    PRIMED=0
    if mkdir -p "$CACHE" && apk update -q --cache-dir "$CACHE" >/dev/null 2>"$WORK/prime.err"; then
        if [ -n "$(ls -A "$CACHE" 2>/dev/null)" ]; then
            PRIMED=1
        fi
    fi
    if [ "$PRIMED" != 1 ]; then
        warn "could not prime a repository index cache; every lookup will fetch indexes"
    fi
}

sp_search() {
    if [ "$PRIMED" = 1 ]; then
        apk search --cache-dir "$CACHE" "$@"
    else
        apk search --no-cache "$@"
    fi
}


apk_diag_line() {
    [ -r "$1" ] || return 0
    sed -n 's/^\(ERROR\|WARNING\):[[:space:]]*//p' "$1" | head -n 1
}

strip_version() {
    sed -e 's/-r[0-9][0-9]*$//' -e 's/-[0-9][0-9A-Za-z._+]*$//'
}

not_found_add() {
    printf '%s\n' "$1" >>"$WORK/nf"
}

map_add() {
    printf '%s\t%s\n' "$1" "$2" >>"$WORK/map"
}

record_providers() {
    _rp_ref=$1
    _rp_prov=$2
    _rp_out=$(sp_search -x -- "$_rp_prov" 2>"$WORK/search.err" || true)
    if { [ -z "$_rp_out" ] && [ -s "$WORK/search.err" ]; } || grep -q '^ERROR' "$WORK/search.err" 2>/dev/null; then
        warn "apk search failed for \"$_rp_prov\": $(apk_diag_line "$WORK/search.err")"
        exit "$EXIT_RESOLVE"
    fi
    if [ -n "$_rp_out" ]; then
        printf '%s\n' "$_rp_out" | strip_version | sort -u >"$WORK/pv"
        while IFS= read -r _rp_nm; do
            [ -n "$_rp_nm" ] || continue
            map_add "$_rp_nm" "$_rp_ref"
        done <"$WORK/pv"
    else
        not_found_add "$_rp_ref"
    fi
}

record_file_owner() {
    _fo_ref=$1
    shift
    _fo_hit=0
    for _fo_p in "$@"; do
        _fo_f=
        if [ -d "$_fo_p" ]; then
            _fo_f=$(find "$_fo_p" -type f 2>/dev/null | head -n 1) || _fo_f=
        elif [ -f "$_fo_p" ]; then
            _fo_f=$_fo_p
        else
            continue
        fi
        [ -n "$_fo_f" ] || continue
        _fo_own=$(apk info -W "$_fo_f" 2>/dev/null) || continue
        _fo_nm=$(printf '%s\n' "$_fo_own" | sed -n 's/^.* is owned by //p' | strip_version)
        [ -n "$_fo_nm" ] || continue
        map_add "$_fo_nm" "$_fo_ref"
        _fo_hit=1
    done
    if [ "$_fo_hit" != 1 ]; then
        not_found_add "$_fo_ref"
    fi
}

handle_ref() {
    _hr_ref=$1
    _hr_type=${_hr_ref%%:*}
    _hr_res=${_hr_ref#*:}
    if [ "$_hr_type" = "$_hr_ref" ] || [ -z "$_hr_res" ]; then
        warn "resource type of \"$_hr_ref\" has no system-package-manager translation"
        not_found_add "$_hr_ref"
        return
    fi
    case $_hr_res in
        *[!A-Za-z0-9._+/-]*)
            warn "resource name of \"$_hr_ref\" has no system-package-manager translation"
            not_found_add "$_hr_ref"
            return
            ;;
    esac
    case $_hr_type in
        bin|sbin)
            record_providers "$_hr_ref" "cmd:$_hr_res"
            ;;
        lib)
            record_providers "$_hr_ref" "so:$_hr_res"
            ;;
        pc)
            record_providers "$_hr_ref" "pc:${_hr_res%.pc}"
            ;;
        libexec)
            record_file_owner "$_hr_ref" "/usr/libexec/$_hr_res"
            ;;
        gir)
            record_file_owner "$_hr_ref" "/usr/share/gir-1.0/$_hr_res"
            ;;
        typelib)
            record_file_owner "$_hr_ref" "/usr/lib/girepository-1.0/$_hr_res"
            ;;
        gio)
            record_file_owner "$_hr_ref" "/usr/lib/gio/modules/$_hr_res"
            ;;
        res)
            record_file_owner "$_hr_ref" "/usr/share/$_hr_res"
            ;;
        cfg)
            record_file_owner "$_hr_ref" "/etc/$_hr_res"
            ;;
        man)
            record_file_owner "$_hr_ref" "/usr/share/man/$_hr_res"
            ;;
        info)
            record_file_owner "$_hr_ref" "/usr/share/info/$_hr_res"
            ;;
        locale)
            record_file_owner "$_hr_ref" "/usr/share/locale/$_hr_res"
            ;;
        inc)
            record_file_owner "$_hr_ref" "/usr/include/$_hr_res"
            ;;
        vapi)
            set -- "/usr/share/vala/vapi/$_hr_res"
            for _hr_d in /usr/share/vala-*/vapi/"$_hr_res"; do
                if [ -f "$_hr_d" ]; then
                    set -- "$@" "$_hr_d"
                fi
            done
            record_file_owner "$_hr_ref" "$@"
            ;;
        rootpath)
            record_file_owner "$_hr_ref" "/$_hr_res"
            ;;
        tag)
            _hr_t=${_hr_res%.tag}
            record_file_owner "$_hr_ref" "/usr/share/usm-tags/$(printf '%s' "$_hr_t" | tr '.' '/').tag"
            ;;
        *)
            warn "resource type of \"$_hr_ref\" has no system-package-manager translation"
            not_found_add "$_hr_ref"
            return
            ;;
    esac
}

counts_for() {
    _cf_name=$1
    _cf_cached=
    if [ -s "$WORK/counts" ]; then
        _cf_cached=$(awk -F'\t' -v n="$_cf_name" '$1 == n { print $2 " " $3; exit }' "$WORK/counts") || _cf_cached=
    fi
    if [ -n "$_cf_cached" ]; then
        printf '%s\n' "$_cf_cached"
        return
    fi
    _cf_cl=$(sp_search --recursive -- "$_cf_name" 2>"$WORK/rec.err" || true)
    if [ -z "$_cf_cl" ] || grep -q '^ERROR' "$WORK/rec.err" 2>/dev/null; then
        warn "could not resolve solo install of $_cf_name, estimating dependency counts"
        if grep -qx -F "$_cf_name" "$WORK/installed" 2>/dev/null; then
            printf '1 1\n'
        else
            printf '1 0\n'
        fi
        return
    fi
    printf '%s\n' "$_cf_cl" | strip_version >"$WORK/cl"
    _cf_dep=$(awk 'END { print NR }' "$WORK/cl")
    _cf_inst=0
    if [ -s "$WORK/installed" ]; then
        _cf_inst=$(grep -x -F -f "$WORK/installed" "$WORK/cl" | awk 'END { print NR }') || _cf_inst=0
    fi
    printf '%s\t%s\t%s\n' "$_cf_name" "$_cf_dep" "$_cf_inst" >>"$WORK/counts"
    printf '%s %s\n' "$_cf_dep" "$_cf_inst"
}

cmd_query() {
    [ $# -ge 1 ] || die_usage
    mkwork
    prime_index_cache
    apk info >"$WORK/installed" 2>/dev/null || : >"$WORK/installed"
    : >"$WORK/map"
    : >"$WORK/nf"
    : >"$WORK/counts"
    : >"$WORK/seen"
    for _q_ref in "$@"; do
        if [ -n "$_q_ref" ] && grep -x -F -q "$_q_ref" "$WORK/seen" 2>/dev/null; then
            continue
        fi
        printf '%s\n' "$_q_ref" >>"$WORK/seen"
        handle_ref "$_q_ref"
    done
    _q_out='{"not-found":['
    _q_first=1
    while IFS= read -r _q_r; do
        [ -n "$_q_r" ] || continue
        if [ "$_q_first" = 1 ]; then _q_first=0; else _q_out="$_q_out,"; fi
        _q_out="$_q_out\"$(json_escape "$_q_r")\""
    done <"$WORK/nf"
    _q_out="$_q_out],\"packages\":["
    _q_first=1
    for _q_nm in $(awk -F'\t' '{ print $1 }' "$WORK/map" | sort -u); do
        if [ "$_q_first" = 1 ]; then _q_first=0; else _q_out="$_q_out,"; fi
        awk -F'\t' -v n="$_q_nm" '$1 == n { print $2 }' "$WORK/map" >"$WORK/tmpres"
        _q_res=""
        _q_rf=1
        while IFS= read -r _q_rr; do
            if [ "$_q_rf" = 1 ]; then _q_rf=0; else _q_res="$_q_res,"; fi
            _q_res="$_q_res\"$(json_escape "$_q_rr")\""
        done <"$WORK/tmpres"
        _q_cnts=$(counts_for "$_q_nm")
        _q_dep=${_q_cnts%% *}
        _q_inst=${_q_cnts##* }
        _q_out="$_q_out{\"name\":\"$(json_escape "$_q_nm")\",\"resources\":[${_q_res}],\"dependency-count\":$_q_dep,\"installed-dependency-count\":$_q_inst}"
    done
    _q_out="$_q_out]}"
    emit "$_q_out"
    exit "$EXIT_OK"
}

classify_failure() {
    if grep -Eqi 'fetch|download|network|temporary failure|connection refused|timed out|untrusted|checksum|signature|mirror|404|503' "$1" 2>/dev/null; then
        printf '%s\n' "$EXIT_DOWNLOAD"
    elif grep -Eq 'unable to select|constraint|conflict|world' "$1" 2>/dev/null; then
        printf '%s\n' "$EXIT_RESOLVE"
    else
        printf '%s\n' "$EXIT_TRANSACTION"
    fi
}

# Packages whose apk split leaves a toolchain unusable alone: gcc ships no
# crt objects (musl-dev owns Scrt1.o/crti.o/libssp_nonshared.a), so linking
# fails without it. USM manifests cannot express crt files as resource
# refs; this table completes the toolchain at install time instead.
INSTALL_COMPANIONS="gcc:musl-dev"

cmd_install() {
    [ $# -ge 1 ] || die_usage
    for _i_n in "$@"; do
        case $_i_n in
            -*) die_usage ;;
        esac
    done
    _i_expanded=""
    for _i_n in "$@"; do
        _i_expanded="$_i_expanded $_i_n"
        for _i_c in $INSTALL_COMPANIONS; do
            if [ "${_i_c%%:*}" = "$_i_n" ]; then
                _i_expanded="$_i_expanded ${_i_c#*:}"
            fi
        done
    done
    set -- $_i_expanded
    mkwork
    _i_simrc=0
    _i_sim=$(apk add --simulate --no-cache --no-progress -- "$@" 2>"$WORK/sim.err") || _i_simrc=$?
    if [ "$_i_simrc" != 0 ]; then
        _i_msg=$(apk_diag_line "$WORK/sim.err")
        [ -n "$_i_msg" ] || _i_msg="apk add --simulate failed with status $_i_simrc"
        emit "{\"type\":\"error\",\"message\":\"$(json_escape "$_i_msg")\"}"
        exit "$(classify_failure "$WORK/sim.err")"
    fi
    _i_total=$(printf '%s\n' "$_i_sim" | sed -n 's/^(\([0-9][0-9]*\)\/\([0-9][0-9]*\)).*/\2/p' | tail -n 1)
    [ -n "$_i_total" ] || _i_total=0
    emit "{\"type\":\"begin\",\"total\":$_i_total}"
    {
        apk add --no-cache --no-progress -- "$@" 2>"$WORK/inst.err"
        printf '%s\n' "$?" >"$WORK/rc"
    } | {
        _i_done=0
        while IFS= read -r _i_line || [ -n "$_i_line" ]; do
            case $_i_line in
                \(*\))
                    _i_kn=${_i_line#"("}
                    _i_kn=${_i_kn%%")"*}
                    _i_k=${_i_kn%%/*}
                    _i_n=${_i_kn##*/}
                    _i_rest=${_i_line#*") "}
                    _i_verb=${_i_rest%% *}
                    _i_pkg=${_i_rest#* }
                    _i_pkg=${_i_pkg%% *}
                    case $_i_verb in
                        Installing|Upgrading|Reinstalling|Downgrading)
                            emit "{\"type\":\"package\",\"name\":\"$(json_escape "$_i_pkg")\",\"current\":$_i_k,\"total\":$_i_n,\"progress\":1.0}"
                            emit "{\"type\":\"package-complete\",\"name\":\"$(json_escape "$_i_pkg")\"}"
                            _i_done=$((_i_done + 1))
                            ;;
                    esac
                    ;;
            esac
        done
        printf '%s\n' "$_i_done" >"$WORK/done"
    }
    _i_rc=$(cat "$WORK/rc" 2>/dev/null) || _i_rc=1
    if [ "$_i_rc" = 0 ]; then
        _i_done=$(cat "$WORK/done" 2>/dev/null) || _i_done=0
        emit "{\"type\":\"complete\",\"status\":\"ok\",\"installed\":$_i_done}"
        exit "$EXIT_OK"
    fi
    _i_msg=$(apk_diag_line "$WORK/inst.err")
    [ -n "$_i_msg" ] || _i_msg="apk add failed with status $_i_rc"
    emit "{\"type\":\"error\",\"message\":\"$(json_escape "$_i_msg")\"}"
    exit "$(classify_failure "$WORK/inst.err")"
}

cmd_plan() {
    [ $# -ge 1 ] || die_usage
    for _p_n in "$@"; do
        case $_p_n in
            -*) die_usage ;;
        esac
    done
    _p_expanded=""
    for _p_n in "$@"; do
        _p_expanded="$_p_expanded $_p_n"
        for _p_c in $INSTALL_COMPANIONS; do
            if [ "${_p_c%%:*}" = "$_p_n" ]; then
                _p_expanded="$_p_expanded ${_p_c#*:}"
            fi
        done
    done
    set -- $_p_expanded
    mkwork
    _p_rc=0
    _p_sim=$(apk add --simulate --no-cache --no-progress -- "$@" 2>"$WORK/plan.err") || _p_rc=$?
    if [ "$_p_rc" != 0 ]; then
        _p_msg=$(apk_diag_line "$WORK/plan.err")
        [ -n "$_p_msg" ] || _p_msg="apk add --simulate failed with status $_p_rc"
        printf '%s: %s\n' "$PROG" "$_p_msg" >&2
        exit "$(classify_failure "$WORK/plan.err")"
    fi
    # simulate output names every package the transaction would touch:
    # "(3/7) Installing musl (1.2.5-r1)"
    _p_json=$(printf '%s\n' "$_p_sim" \
        | sed -n 's/^(\([0-9][0-9]*\)\/\([0-9][0-9]*\)) Installing \([^ ]*\) .*/"\3"/p' \
        | awk 'NR>1{printf ","} {printf "%s", $0}')
    printf '{"packages":[%s]}\n' "$_p_json"
    exit "$EXIT_OK"
}

main() {
    if [ $# -lt 1 ]; then
        die_usage
    fi
    if ! command -v apk >/dev/null 2>&1; then
        if [ "$1" = install ]; then
            emit '{"type":"error","message":"apk not found in PATH"}'
            exit "$EXIT_FAILURE"
        fi
        warn "apk not found in PATH"
        exit "$EXIT_RESOLVE"
    fi
    _m_cmd=$1
    shift
    case $_m_cmd in
        query)
            cmd_query "$@"
            ;;
        plan)
            cmd_plan "$@"
            ;;
        install)
            cmd_install "$@"
            ;;
        -h|--help|help)
            printf 'usage: %s query <usm-ref>...\n' "$PROG"
            printf '       %s install <native-name>...\n' "$PROG"
            exit "$EXIT_OK"
            ;;
        *)
            die_usage
            ;;
    esac
}

main "$@"
