#!/bin/sh

set -e

# We can't use bash's time, as it prints the timing information of the
# entire pipeline, meaning we can't redirect it to the log file.
TIME=$(which time)
TIMEFMT="\"time\": { \"real\": %e, \"user\": %U, \"sys\": %S },"

inform()
{
    local level="$1"
    shift

    logger -p "daemon.$level" -I $PPID -t dagger -c $LOGGER_STDOUT "$@"
}

abort()
{
    inform err "Aborting: $*"
    exit 1
}

generate_order()
{
    [ -f "$1/bottom-up-order" ] && [ -f "$1/top-down-order" ] && return

    # The bottom-up order corresponds to a DFS through the DAG, with
    # duplicates removed
    find -L "$1/dag" -depth -mindepth 1 -exec basename {} \; \
	| awk '!seen[$0]++' \
	      >"$1/bottom-up-order"

    # The reverse is a valid top-down order
    sed '1!G;h;$!d' "$1/bottom-up-order" >"$1/top-down-order"
}

action_exec()
{
    local actdir="$2/action/$1"
    local orderfile="$actdir/order"
    local code=0
    local meta=

    generate_order "$2"

    if [ ! -r "$orderfile" ]; then
	orderfile="$2/bottom-up-order"
	inform warn "No order defined for $actdir, falling back to bottom-up"
    fi

    local order=$(cat "$orderfile")

    [ -d "$actdir/@pre"  ] && order="@pre $order"
    [ -d "$actdir/@post" ] && order="$order @post"

    for node in $order; do
	for work in $(find "$actdir/$node" -type f -executable 2>/dev/null | sort); do
	    meta=$work-meta.json

	    cat >$meta <<EOF
{
	"generation": $(basename $2),
	"action": "$1",
	"node": "$node",
	"work": "$(basename $work)",
EOF
	    $TIME -f "$TIMEFMT" -o $meta.time $work >>"$work.log" 2>&1 || code=$?

	    echo -ne "\t" >>$meta
	    # busybox's time(1) will happily write "Command exited
	    # with non-zero status" and similar messages to the
	    # output, even when -f is used. Work around that by only
	    # grabbing the last line, which holds the requested
	    # output.
	    tail -n1 $meta.time >>$meta

	    cat >>$meta <<EOF
	"exitcode": $code
}
EOF
	    [ $code -eq 0 ] || abort "$work failed with exitcode $code"
	done
    done
}

maybe_get_current()
{
    echo $([ -f "$basedir/current" ] && cat "$basedir/current" || true)
    [ ! "$current" ] || [ -d "$basedir/$current" ] \
	|| abort "Current generation does not exist"
}

get_current()
{
    local current=

    current=$(maybe_get_current)
    [ "$current" ] && [ -d "$basedir/$current" ] \
	|| abort "Current generation does not exist"

    echo "$current"
}

get_next()
{
    local next=

    next=$([ -f "$basedir/next" ] && cat "$basedir/next" || true)
    [ "$next" ] && [ -d "$basedir/$next" ] \
	|| abort "Next generation does not exist"

    echo "$next"
}

do_exec()
{
    local action="$1"

    action_exec "$1" "$basedir/$(get_current)"
}

do_abandon()
{
    local next=$(get_next)

    date +%F-%T >"$basedir/$next/ABANDONED"

    mv "$basedir/next" "$basedir/current"
    inform info "Abandoned generation $next"
}

do_evolve()
{
    local current=$(maybe_get_current)
    local next=$(get_next)
    local ar=

    [ "$current" ] && action_exec exit "$basedir/$current"

    action_exec init "$basedir/$next"

    mv "$basedir/next" "$basedir/current"
    inform info "Evolved to generation $next"

    if [ "$current" ]; then
	ar="$current.tar.gz"
	[ -f "$basedir/$current/ABANDONED" ] && ar="$current-ABANDONED.tar.gz"

	(cd "$basedir" && tar caf "$ar" "$current")
	rm -rf "$basedir/$current"
    fi
}

do_prune()
{
    local keep=${1:-10}

    cd "$basedir"
    rm -f old=$(ls -rv *.tar.gz 2>/dev/null | tail "+$((keep + 1))")
}

usage()
{
    cat <<EOF
usage: dagger [-d] [-C <root>] <command> [<args>]

Directed Acyclic Graph Generational Execution Runner

Global options:

  -d
    In addition to the system log, also log messages to stderr.

  -C <root>
    Instead of the current working directory, use <root> as the dagger
    root directory.

Commands:

  abandon
    Abandon a prepared, but not yet applied, next generation.

  evolve
    Run the exit action of the current generation, followed by the
    init action of the next generation.

  exec <action>
    Run the specified action of the current generation.

  prune [<num-generations>]
    Remove all but the last <num-generations>, or 10 by default,
    archived generations.

  help
    Show this message.

EOF
}

basedir=$(pwd)

# Global options
while getopts "dC:" opt; do
    case $opt in
	d)
	    LOGGER_STDOUT=-s
	    ;;
	C)
	    basedir=$OPTARG
	    ;;
	*)
	    usage && exit 1
    esac
done
shift $((OPTIND - 1))

if [ $# -lt 1 ]; then
    usage && exit 1
fi

cmd=$1
shift

case $cmd in
    "help")
	usage && exit 0
	;;
    "abandon")
	do_abandon
	;;
    "evolve")
	do_evolve
	;;
    "exec")
	do_exec "$@"
	;;
    "prune")
	do_prune "$@"
	;;

    *)
	usage && exit 1
esac
